remnote-bridge 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +30 -6
  2. package/dist/cli/commands/connect.js +31 -2
  3. package/dist/cli/commands/health.js +111 -1
  4. package/dist/cli/commands/setup.js +112 -0
  5. package/dist/cli/daemon/daemon.js +101 -20
  6. package/dist/cli/daemon/headless-browser.js +291 -0
  7. package/dist/cli/daemon/static-server.js +84 -0
  8. package/dist/cli/handlers/edit-handler.js +89 -3
  9. package/dist/cli/handlers/read-handler.js +16 -0
  10. package/dist/cli/handlers/tree-edit-handler.js +59 -28
  11. package/dist/cli/handlers/tree-parser.js +110 -3
  12. package/dist/cli/main.js +22 -6
  13. package/dist/cli/server/ws-server.js +62 -1
  14. package/dist/mcp/daemon-client.js +4 -1
  15. package/dist/mcp/instructions.js +97 -12
  16. package/dist/mcp/resources/edit-rem-guide.js +53 -0
  17. package/dist/mcp/resources/edit-tree-guide.js +60 -0
  18. package/dist/mcp/resources/error-reference.js +8 -1
  19. package/dist/mcp/resources/outline-format.js +29 -1
  20. package/dist/mcp/resources/rem-object-fields.js +6 -4
  21. package/dist/mcp/resources/separator-flashcard.js +5 -5
  22. package/dist/mcp/tools/infra-tools.js +39 -9
  23. package/package.json +5 -1
  24. package/remnote-plugin/dist/bridge-icon.svg +8 -0
  25. package/remnote-plugin/dist/bridge_widget-sandbox.js +65 -0
  26. package/remnote-plugin/dist/bridge_widget.js +65 -0
  27. package/remnote-plugin/dist/index-sandbox.css +591 -0
  28. package/remnote-plugin/dist/index-sandbox.js +64 -0
  29. package/remnote-plugin/dist/index.css +591 -0
  30. package/remnote-plugin/dist/index.html +9 -0
  31. package/remnote-plugin/dist/index.js +64 -0
  32. package/remnote-plugin/dist/manifest.json +22 -0
  33. package/remnote-plugin/src/bridge/message-router.ts +11 -0
  34. package/remnote-plugin/src/services/add-to-portal.ts +40 -0
  35. package/remnote-plugin/src/services/create-portal.ts +47 -0
  36. package/remnote-plugin/src/services/remove-from-portal.ts +40 -0
  37. package/remnote-plugin/src/services/write-rem-fields.ts +39 -0
  38. package/remnote-plugin/src/types.ts +7 -4
  39. package/skills/remnote-bridge/SKILL.md +90 -8
  40. package/skills/remnote-bridge/instructions/connect.md +48 -10
  41. package/skills/remnote-bridge/instructions/disconnect.md +1 -1
  42. package/skills/remnote-bridge/instructions/edit-rem.md +67 -4
  43. package/skills/remnote-bridge/instructions/edit-tree.md +100 -10
  44. package/skills/remnote-bridge/instructions/health.md +67 -1
  45. package/skills/remnote-bridge/instructions/overall.md +19 -4
  46. package/skills/remnote-bridge/instructions/read-rem.md +5 -2
  47. package/skills/remnote-bridge/instructions/setup.md +130 -0
@@ -198,6 +198,10 @@ remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","oldStr":"\"concept
198
198
  │ ├─ 与缓存 JSON 严格比较
199
199
  │ └─ 不匹配 → 抛出错误(不更新缓存,迫使 AI re-read)
200
200
 
201
+ ├─ Portal 检测:type === 'portal'?
202
+ │ ├─ 是 → 进入 Portal 专用路径(简化 JSON 上执行 str_replace)
203
+ │ └─ 否 → 继续普通 Rem 路径
204
+
201
205
  ├─ 防线 3: str_replace 精确匹配
202
206
  │ ├─ countOccurrences(cachedJson, oldStr)
203
207
  │ ├─ 0 次 → 抛出"未找到"错误
@@ -338,11 +342,68 @@ edit-rem(remId, oldStr, newStr)
338
342
 
339
343
  ---
340
344
 
345
+ ## Portal 编辑专用路径
346
+
347
+ 当 edit-rem 检测到被编辑的 Rem 是 Portal(`type === 'portal'`)时,自动切换到 Portal 专用编辑路径。
348
+
349
+ ### 简化 JSON 作为操作目标
350
+
351
+ **问题**:缓存中存储完整 51 字段 JSON,但 AI 看到的是 9 字段简化 JSON。oldStr 来自简化输出,在完整 JSON 上匹配不到。
352
+
353
+ **方案**:Portal 路径在**简化 JSON**(9 字段)上执行 str_replace:
354
+
355
+ 1. 防线 1 + 2:不变(完整 JSON 对比)
356
+ 2. **str_replace**:将缓存的完整 JSON 转换为简化 JSON,在简化 JSON 上执行 str_replace
357
+ 3. 解析 str_replace 后的简化 JSON,推导变更字段
358
+ 4. 调用写入
359
+
360
+ ### Portal 简化 JSON 格式
361
+
362
+ ```json
363
+ {
364
+ "id": "abc123",
365
+ "type": "portal",
366
+ "portalType": "portal",
367
+ "portalDirectlyIncludedRem": ["remId1", "remId2"],
368
+ "parent": "parentId",
369
+ "positionAmongstSiblings": 3,
370
+ "children": ["remId1", "remId2"],
371
+ "createdAt": 1709000000000,
372
+ "updatedAt": 1709000000000
373
+ }
374
+ ```
375
+
376
+ ### Portal 可写字段
377
+
378
+ | 字段 | 写入方式 |
379
+ |:-----|:---------|
380
+ | `portalDirectlyIncludedRem` | diff 数组,新增调 `addToPortal()`,移除调 `removeFromPortal()` |
381
+ | `parent` | 调 `setParent()` |
382
+ | `positionAmongstSiblings` | 调 `setParent(parent, position)` |
383
+
384
+ ### Portal 只读字段
385
+
386
+ id、type、portalType、children、createdAt、updatedAt — 修改只产生警告。
387
+
388
+ ### Portal 编辑示例
389
+
390
+ ```bash
391
+ # 添加一个引用
392
+ edit-rem abc123 --old-str '"portalDirectlyIncludedRem": ["remId1", "remId2"]' \
393
+ --new-str '"portalDirectlyIncludedRem": ["remId1", "remId2", "remId3"]'
394
+
395
+ # 移除一个引用
396
+ edit-rem abc123 --old-str '"portalDirectlyIncludedRem": ["remId1", "remId2"]' \
397
+ --new-str '"portalDirectlyIncludedRem": ["remId1"]'
398
+ ```
399
+
400
+ ---
401
+
341
402
  ## 可编辑字段约束表
342
403
 
343
- RemObject 51 个字段中,20 个可编辑(RW),31 个只读(R + R-F)。
404
+ RemObject 51 个字段中,21 个可编辑(RW),30 个只读(R + R-F)。
344
405
 
345
- 以下为 20 个可编辑字段及其写入约束:
406
+ 以下为 21 个可编辑字段及其写入约束:
346
407
 
347
408
  | 字段 | SDK setter | 值类型 | 约束 / 特殊处理 |
348
409
  |------|-----------|--------|-----------------|
@@ -366,6 +427,7 @@ RemObject 51 个字段中,20 个可编辑(RW),31 个只读(R + R-F)
366
427
  | `tags` | `rem.addTag()` / `rem.removeTag()` | string[] | **Diff 机制**:对比当前 vs 目标,增删差异项 |
367
428
  | `sources` | `rem.addSource()` / `rem.removeSource()` | string[] | **Diff 机制**:对比当前 vs 目标,增删差异项 |
368
429
  | `positionAmongstSiblings` | `rem.setParent(parent, position)` | number \| null | 与 `parent` 联动(见下方说明) |
430
+ | `portalDirectlyIncludedRem` | `rem.addToPortal()` / `rem.removeFromPortal()` | string[] | **Portal-W Diff 机制**:仅 type=portal 时可修改。对比当前 vs 目标数组,逐项增删 |
369
431
 
370
432
  ### parent + positionAmongstSiblings 联动
371
433
 
@@ -400,13 +462,13 @@ for id in currentSet:
400
462
 
401
463
  ## 只读字段列表
402
464
 
403
- 以下 31 个字段在 str_replace 中被修改时,**只产生警告,不执行写入**:
465
+ 以下 30 个字段在 str_replace 中被修改时,**只产生警告,不执行写入**:
404
466
 
405
467
  ```
406
468
  id,
407
469
  children,
408
470
  isTable,
409
- portalType, portalDirectlyIncludedRem,
471
+ portalType,
410
472
  propertyType,
411
473
  aliases,
412
474
  remsBeingReferenced, deepRemsBeingReferenced, remsReferencingThis,
@@ -652,4 +714,5 @@ newStr: "\"text\": [\n \"光合作用需要\",\n {\n \"cId\": \"cloz
652
714
  | `invalid JSON` | 替换后的文本不是合法 JSON | 检查 newStr 的引号、逗号、括号完整性 |
653
715
  | `Failed to update field` | SDK setter 调用失败 | 检查字段值是否在允许范围内(如 type 不能设为 portal) |
654
716
  | `Field '...' is read-only and was ignored` | 修改了只读字段 | 该字段只能读取,不可通过 edit-rem 修改 |
717
+ | `old_str not found in the simplified Portal JSON` | Portal 编辑时 oldStr 在简化 JSON 中不匹配 | 检查 oldStr 是否匹配 9 字段简化 JSON 格式(而非完整 51 字段 JSON) |
655
718
  | `守护进程未运行` | daemon 未启动 | 执行 `remnote-bridge connect` |
@@ -201,16 +201,78 @@ newStr:
201
201
  | `` `text` `` | 创建代码块 |
202
202
  | `---` | 创建分隔线 |
203
203
 
204
+ 前缀可组合叠加,解析顺序为 Header → Todo → Code。例如 `## - [ ] text` 创建 H2 + 未完成待办。
205
+
204
206
  #### 新增行的箭头分隔符
205
207
 
206
- | 箭头 | 格式 | 效果 |
207
- |------|------|------|
208
- | ` → ` | `问题 → 答案` | 创建 forward 闪卡(单行) |
209
- | ` ← ` | `问题 ← 答案` | 创建 backward 闪卡(单行) |
210
- | ` ↔ ` | `问题 ↔ 答案` | 创建 both 闪卡(单行) |
211
- | ` ↓` | `问题 ↓` | 创建 forward 多行闪卡(子节点为答案) |
212
- | ` ↑` | `问题 ↑` | 创建 backward 多行闪卡 |
213
- | ` ↕` | `问题 ↕` | 创建 both 多行闪卡 |
208
+ 箭头分隔符分为两类:**中间箭头**(有 backText)和**尾部箭头**(无 backText,子节点为答案)。
209
+
210
+ **中间箭头**(格式:`问题 {箭头} 答案`):
211
+
212
+ | 箭头 | 效果 |
213
+ |------|------|
214
+ | ` ` | forward 闪卡(单行) |
215
+ | ` ` | backward 闪卡(单行) |
216
+ | ` ↔ ` | both 闪卡(单行) |
217
+ | ` ↓ ` | forward 多行闪卡(带 backText) |
218
+ | ` ↑ ` | backward 多行闪卡(带 backText) |
219
+ | ` ↕ ` | both 多行闪卡(带 backText) |
220
+
221
+ **尾部箭头**(格式:`问题 {箭头}`,子节点为答案):
222
+
223
+ | 箭头 | 效果 |
224
+ |------|------|
225
+ | ` ↓` | forward 多行闪卡 |
226
+ | ` ↑` | backward 多行闪卡 |
227
+ | ` ↕` | both 多行闪卡 |
228
+
229
+ > 已知限制:使用 indexOf 匹配第一个箭头,如果新增行内容本身包含箭头字符(如 `A → B → C`),会被误切割为 text + backText。
230
+
231
+ #### 新增行的元数据注释
232
+
233
+ 在新增行末尾添加 HTML 注释可设置 Rem 属性(type、isDocument、tag)。注意:这是**不含 remId 的纯元数据注释**,与已有行的行尾标记(`<!--remId metadata-->`)格式不同。
234
+
235
+ 语法:`<!--token1 token2 ...-->`
236
+
237
+ | token | 效果 |
238
+ |-------|------|
239
+ | `type:concept` | 设置 Rem 类型为 concept |
240
+ | `type:descriptor` | 设置 Rem 类型为 descriptor |
241
+ | `doc` | 设置 isDocument = true |
242
+ | `tag:Name(tagRemId)` | 添加标签(括号内为标签 Rem 的 ID) |
243
+
244
+ 示例:
245
+
246
+ ```
247
+ 新概念节点 <!--type:concept doc-->
248
+ 新描述节点 <!--type:descriptor tag:MyTag(tagId123)-->
249
+ ```
250
+
251
+ 多个 token 之间用空格分隔,可任意组合。
252
+
253
+ #### Portal 新增行
254
+
255
+ 在 newStr 中使用 Portal 专用注释创建新的 Portal:
256
+
257
+ ```
258
+ <!--portal refs:remId1,remId2-->
259
+ <!--portal-->
260
+ ```
261
+
262
+ - `refs:` 后面跟逗号分隔的 Rem ID 列表,指定 Portal 引用的目标 Rem
263
+ - 不带 `refs:` 则创建空 Portal
264
+ - Portal 创建分两步执行:先调用 `create_portal` 创建空 Portal 并设置父节点/位置,再逐个调用 `add_to_portal` 添加引用
265
+
266
+ 示例:
267
+
268
+ ```
269
+ oldStr:
270
+ 子节点 A <!--idA-->
271
+
272
+ newStr:
273
+ <!--portal refs:refId1,refId2-->
274
+ 子节点 A <!--idA-->
275
+ ```
214
276
 
215
277
  #### 嵌套新增
216
278
 
@@ -226,6 +288,30 @@ newStr:
226
288
 
227
289
  嵌套新增行的父 ID 通过内部占位标记 `__new_N__` 管理,创建顺序保证从浅到深。
228
290
 
291
+ #### ⚠️ 插入位置:必须在兄弟末尾
292
+
293
+ 新行**不能**插在一个有子节点的 Rem 和它的 children 之间,否则 children 会被新行"劫持",触发 `children_captured` 错误。
294
+
295
+ ```
296
+ ❌ 错误:插在父 Rem 和 children 之间
297
+ 水分子 ↓ <!--idA-->
298
+ 新行 ← children 被解析为新行的子节点!
299
+ 化学式 → H₂O <!--idB role:card-item-->
300
+
301
+ ✅ 正确:插在所有兄弟末尾
302
+ 极性 → ... <!--idZ role:card-item-->
303
+ 新行 ← 不影响任何已有节点
304
+ ```
305
+
306
+ #### 两步操作:创建新节点并移入已有 children
307
+
308
+ 如果需要"创建一个新父节点,把已有的 children 移到它下面",必须分两步完成:
309
+
310
+ 1. **第一次 edit-tree**:在兄弟末尾创建新节点
311
+ 2. **第二次 edit-tree**:新节点已获得 remId,将已有行的缩进改到新节点下(走正常 move 逻辑)
312
+
313
+ 这是因为新增行没有 remId,diff 算法无法区分"移动到新父节点"和"重新创建"。
314
+
229
315
  ### 删除行
230
316
 
231
317
  从 newStr 中移除带 remId 的行。**必须同时删除该行的所有可见子行**,否则报 orphan_detected 错误。
@@ -286,6 +372,7 @@ newStr:
286
372
  | 删除行但保留子节点 | `orphan_detected` | Cannot delete {id} because it has children that were not removed. | 同时删除所有子行 |
287
373
  | 删除/修改省略占位符 | `elided_modified` | Cannot delete or modify elided region directly. | 用更大的 depth/maxSiblings 重新 read-tree 展开 |
288
374
  | 缩进跳级 | `indent_skip` | 缩进跳级:行 ... 的缩进级别为 N,但找不到上一级的父节点。 | 检查缩进是否正确(每级 2 空格) |
375
+ | 新行劫持已有子节点 | `children_captured` | New line "..." accidentally captured existing children (...). | 把新行插到兄弟末尾,不要插在父 Rem 和 children 之间 |
289
376
 
290
377
  ---
291
378
 
@@ -304,7 +391,8 @@ diff 算法生成的操作按以下顺序排列并执行(在 `diffTrees` 中
304
391
 
305
392
  | 操作 | Plugin action | payload |
306
393
  |------|---------------|---------|
307
- | create | `create_rem` + `write_rem_fields` | content, parentId, position + Markdown 属性 |
394
+ | create(普通 Rem) | `create_rem` + `write_rem_fields` | content, parentId, position + Markdown 属性 + 元数据(type/doc/tag) |
395
+ | create(Portal) | `create_portal` + 逐个 `add_to_portal` | parentId, position + portalRefs |
308
396
  | delete | `delete_rem` | remId |
309
397
  | move | `move_rem` + 条件性 `write_rem_fields` | remId, newParentId, position |
310
398
  | reorder | `reorder_children` | parentId, order[] |
@@ -376,12 +464,14 @@ RemNote SDK 存在已知 bug:
376
464
  ├─ 对比差异(diffTrees)
377
465
  │ ├─ 根节点校验
378
466
  │ ├─ 省略行防线
467
+ │ ├─ 子节点劫持检测(children_captured)
379
468
  │ ├─ 内容变更检测
380
469
  │ ├─ 折叠删除防线
381
470
  │ ├─ 孤儿检测
382
471
  │ └─ 生成操作列表(create → move → reorder → delete)
383
472
  ├─ 逐项执行操作(forwardToPlugin 调用原子操作)
384
- │ ├─ create: create_rem + write_rem_fields
473
+ │ ├─ create(普通): create_rem + write_rem_fields(Markdown 属性 + 元数据)
474
+ │ ├─ create(Portal): create_portal + 逐个 add_to_portal
385
475
  │ ├─ move: move_rem + isCardItem 同步
386
476
  │ ├─ reorder: reorder_children
387
477
  │ └─ delete: delete_rem
@@ -57,6 +57,18 @@ remnote-bridge health
57
57
  remnote-bridge --json health
58
58
  ```
59
59
 
60
+ ### Headless 诊断模式
61
+
62
+ ```bash
63
+ # 截图 + 详细状态 + console 错误 + 排查建议
64
+ remnote-bridge health --diagnose
65
+
66
+ # 重载 headless Chrome 页面
67
+ remnote-bridge health --reload
68
+ ```
69
+
70
+ `--diagnose` 和 `--reload` 不能同时使用,仅在 headless 模式下可用。
71
+
60
72
  ---
61
73
 
62
74
  ## JSON 输出
@@ -149,11 +161,65 @@ daemon 运行 → Plugin 连接 → SDK 就绪
149
161
 
150
162
  ---
151
163
 
164
+ ## Headless 模式附加输出
165
+
166
+ ### health 基础输出(headless 模式下额外字段)
167
+
168
+ headless 模式下 `health` 基础输出额外包含 `headless` 对象:
169
+
170
+ ```json
171
+ {
172
+ "ok": true,
173
+ "command": "health",
174
+ "exitCode": 0,
175
+ "daemon": { "running": true, "pid": 12345, "reachable": true, "uptime": 300 },
176
+ "plugin": { "connected": true },
177
+ "sdk": { "ready": true },
178
+ "timeoutRemaining": 1500,
179
+ "headless": {
180
+ "status": "running",
181
+ "chromeConnected": true,
182
+ "pageUrl": "http://localhost:8080",
183
+ "reloadCount": 0,
184
+ "lastError": null,
185
+ "recentConsoleErrors": []
186
+ }
187
+ }
188
+ ```
189
+
190
+ ### --diagnose 输出
191
+
192
+ ```json
193
+ {
194
+ "ok": true,
195
+ "command": "health",
196
+ "mode": "diagnose",
197
+ "headless": { "status": "running", "chromeConnected": true, "pageUrl": "...", "reloadCount": 0, "lastError": null, "recentConsoleErrors": [] },
198
+ "screenshotPath": "/Users/xxx/.remnote-bridge/headless-screenshot-1234567890.png",
199
+ "pluginConnected": true,
200
+ "sdkReady": true
201
+ }
202
+ ```
203
+
204
+ ### --reload 输出
205
+
206
+ ```json
207
+ {
208
+ "ok": true,
209
+ "command": "health",
210
+ "mode": "reload"
211
+ }
212
+ ```
213
+
214
+ ---
215
+
152
216
  ## 常见问题诊断
153
217
 
154
218
  | 症状 | 可能原因 | 解决方案 |
155
219
  |------|----------|----------|
156
220
  | daemon 未运行 | 未执行 connect / 已超时关闭 | 执行 `connect` |
157
221
  | daemon 运行但不可达 | WS 端口被占用或配置不匹配 | 检查 `.remnote-bridge.json` 中的 `wsPort` |
158
- | Plugin 未连接 | RemNote 未打开 / Plugin 未安装 / URL 不匹配 | 打开 RemNote,确认 Plugin 中的 WS URL 设置 |
222
+ | Plugin 未连接(标准模式) | RemNote 未打开 / Plugin 未安装 / URL 不匹配 | 打开 RemNote,确认 Plugin 中的 WS URL 设置 |
223
+ | Plugin 未连接(headless 模式) | Chrome 页面加载异常 | `health --diagnose` 查看截图和状态,`health --reload` 重载页面 |
159
224
  | SDK 未就绪 | 知识库加载中 / Plugin 异常 | 等待几秒后重试,或刷新 RemNote 页面 |
225
+ | Chrome 状态 crashed | headless Chrome 崩溃或断开 | `health --reload` 尝试恢复,或 disconnect + connect --headless 重启 |
@@ -71,7 +71,7 @@ Rem 有两个**独立维度**的类型:
71
71
  | `concept` | 概念定义 | 文字**加粗** | `edit-rem` 设 `type: "concept"` |
72
72
  | `descriptor` | 描述/属性 | 文字*斜体* | `edit-rem` 设 `type: "descriptor"` |
73
73
  | `default` | 普通 Rem | 正常字重 | `edit-rem` 设 `type: "default"` |
74
- | `portal` | 嵌入引用容器 | 紫色左边框 | **只读**,不可通过 CLI 设置 |
74
+ | `portal` | 嵌入引用容器 | 紫色左边框 | 不可通过 setType 创建(只能通过 createPortal),但引用列表可通过 edit-rem 修改 |
75
75
 
76
76
  #### isDocument 字段(页面语义)
77
77
 
@@ -120,7 +120,17 @@ Rem 有两个**独立维度**的类型:
120
120
  |:-----|:---------|:-----|:---------|:-----------|
121
121
  | **Reference** | `[[` | RichText 中的引用元素 | 否(只是指针) | read-rem/read-tree 中显示为 `[[文本]]` |
122
122
  | **Tag** | `##` | 将 Rem 作为标签附加 | 否(分类标记) | read-rem 的 `tags` 数组 |
123
- | **Portal** | `((` | 嵌入 Rem 的实时视图 | 是(编辑同步) | read-tree 标记为 `type:portal` |
123
+ | **Portal** | `((` | 嵌入 Rem 的实时视图 | 是(编辑同步) | read-tree 标记为 `type:portal`;引用列表可通过 edit-rem 修改 |
124
+
125
+ #### Portal 操作速查
126
+
127
+ | 操作 | 命令 | 方式 |
128
+ |:-----|:-----|:-----|
129
+ | 创建 Portal | `edit-tree` | 新增行 `<!--portal refs:id1,id2-->` |
130
+ | 删除 Portal | `edit-tree` | 从大纲中移除 Portal 行(与删除普通行相同) |
131
+ | 修改引用列表(增删引用的 Rem) | `edit-rem` | str_replace 简化 JSON 中的 `portalDirectlyIncludedRem` 数组 |
132
+ | 移动 Portal(换父节点/位置) | `edit-tree` | 与移动普通行相同 |
133
+ | 读取 Portal | `read-rem` | 自动输出 9 字段简化 JSON |
124
134
 
125
135
  ### 2.6 Powerup 机制简述
126
136
 
@@ -196,7 +206,7 @@ disconnect → daemon 关闭 → 会话结束,缓存清空
196
206
  | 服务 | 默认端口 | 用途 |
197
207
  |:-----|:---------|:-----|
198
208
  | WS Server | 3002 | CLI ↔ daemon ↔ Plugin 通信 |
199
- | webpack-dev-server | 8080 | 热加载 Plugin 到 RemNote |
209
+ | Plugin 服务 | 8080 | 加载 Plugin 到 RemNote(默认静态服务器,`--dev` 时为 webpack-dev-server) |
200
210
  | ConfigServer | 3003 | HTTP 配置界面 |
201
211
 
202
212
  超时机制:daemon 默认 **30 分钟无活动**自动关闭。每次收到 CLI 请求时重置计时器。
@@ -427,6 +437,9 @@ daemon → CLI 响应:
427
437
  | `delete_rem` | —(内部) | deleteRem() | edit-tree 的原子删除 |
428
438
  | `move_rem` | —(内部) | moveRem() | edit-tree 的原子移动 |
429
439
  | `reorder_children` | —(内部) | reorderChildren() | edit-tree 的原子重排 |
440
+ | `create_portal` | —(内部) | createPortal() | edit-tree Portal 创建 |
441
+ | `add_to_portal` | —(内部) | addToPortal() | Portal 引用管理 |
442
+ | `remove_from_portal` | —(内部) | removeFromPortal() | Portal 引用管理 |
430
443
 
431
444
  `edit_rem` 和 `edit_tree` 在 daemon handler 中编排完成后,调用 `write_rem_fields`、`create_rem` 等原子操作。
432
445
 
@@ -458,7 +471,7 @@ RemObject 是本项目对 RemNote Rem 的标准化表示,包含 51 个字段
458
471
  关联: tags [RW], sources [RW], aliases [R], remsReferencingThis [R]
459
472
  位置: positionAmongstSiblings [RW]
460
473
  时间: createdAt [R], updatedAt [R]
461
- Portal:portalType [R], portalDirectlyIncludedRem [R]
474
+ Portal:portalType [R], portalDirectlyIncludedRem [Portal-W]
462
475
  ```
463
476
 
464
477
  ### 7.3 RichText 格式
@@ -703,6 +716,7 @@ edit-tree 使用 str_replace 对 Markdown 大纲进行结构编辑。详细文
703
716
  | 删除有隐藏子节点的行 | `folded_delete` | 用更大的 depth 重新 read-tree |
704
717
  | 删除节点但保留子节点 | `orphan_detected` | 必须同时删除所有子行 |
705
718
  | 删除/修改省略占位符 | `elided_modified` | 用更大参数重新 read-tree 展开 |
719
+ | 新行劫持已有子节点 | `children_captured` | 把新行插到兄弟末尾,不要插在父 Rem 和 children 之间 |
706
720
 
707
721
  ### 10.3 新增行格式
708
722
 
@@ -800,6 +814,7 @@ Agent 遇到错误时的诊断和恢复指南:
800
814
  | Content modification not allowed | edit-tree 中修改了已有行内容 | 使用 `edit-rem` 修改内容 |
801
815
  | orphan_detected | 删除了父节点但保留了子节点 | 同时删除所有子行 |
802
816
  | folded_delete | 删除了有隐藏子节点的行 | 用更大 depth 重新 read-tree |
817
+ | children_captured | 新行插在父 Rem 和子节点之间,劫持了已有 children | 把新行插到兄弟末尾 |
803
818
 
804
819
  ### 数据问题
805
820
 
@@ -9,7 +9,7 @@
9
9
  `read-rem` 通过 Rem ID 读取一个 Rem 的所有可获取属性,返回标准化的 RemObject。读取结果会被缓存在 daemon 内存中,供后续 `edit-rem` 使用。
10
10
 
11
11
  核心能力:
12
- - 返回 51 个字段的完整 Rem 数据(默认 34 个,`--full` 时 51 个)
12
+ - 返回 51 个字段的完整 Rem 数据(默认 34 个,Portal 简化 9 个,`--full` 时 51 个)
13
13
  - 支持 `--fields` 指定字段子集
14
14
  - 支持 Powerup 噪音过滤(默认过滤)
15
15
  - 自动缓存,为 `edit-rem` 建立编辑基础
@@ -159,6 +159,7 @@ remnote-bridge read-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA"}'
159
159
  ├─ 字段过滤:
160
160
  │ ├─ --full → 返回全部 51 字段
161
161
  │ ├─ --fields → 返回指定字段 + id
162
+ │ ├─ type=portal → Portal 简化模式(返回 9 个关键字段)
162
163
  │ └─ 默认 → 排除 R-F 字段(返回 34 字段)
163
164
  └─ 附加 _cacheOverridden 元数据(若之前有缓存)
164
165
  4. CLI 格式化输出(人类模式 pretty-print / JSON 模式单行)
@@ -237,7 +238,7 @@ RemObject 共 51 个字段,按读写权限分为三类:
237
238
  | 字段 | 类型 | 权限 | 说明 |
238
239
  |------|------|:----:|------|
239
240
  | `portalType` | `PortalType \| null` | R | Portal 子类型。仅 type=portal 时有值 |
240
- | `portalDirectlyIncludedRem` | `string[]` | R | Portal 直接包含的 Rem ID |
241
+ | `portalDirectlyIncludedRem` | `string[]` | Portal-W | Portal 直接包含的 Rem ID。**Portal 专用可写**:仅 type=portal 时可通过 edit-rem 修改(Diff 机制:addToPortal/removeFromPortal) |
241
242
 
242
243
  ### 属性类型
243
244
 
@@ -377,6 +378,7 @@ text | number | date | checkbox | single_select | multi_select | url | image | t
377
378
  | 模式 | 输出字段数 | 说明 |
378
379
  |------|:----------:|------|
379
380
  | 默认 | 34 | RW + R 字段,覆盖常用场景 |
381
+ | Portal 简化 | 9 | type=portal 时自动使用(id、type、portalType、portalDirectlyIncludedRem、parent、positionAmongstSiblings、children、createdAt、updatedAt)。`--full` / `--fields` 可覆盖 |
380
382
  | `--full` | 51 | 全部字段(含 R-F 低频字段) |
381
383
  | `--fields` | 自选 + id | 仅返回指定字段(始终包含 id) |
382
384
 
@@ -420,6 +422,7 @@ localUpdatedAt, lastPracticed
420
422
  | `tags` | `rem.addTag()` / `rem.removeTag()` | **Diff 机制**:对比当前 vs 目标,增删差异项。必须列出完整目标数组,缺少的会被删除 |
421
423
  | `sources` | `rem.addSource()` / `rem.removeSource()` | **Diff 机制**:同 tags |
422
424
  | `positionAmongstSiblings` | `rem.setParent(parent, position)` | 与 `parent` 联动(见下方说明) |
425
+ | `portalDirectlyIncludedRem` | `rem.addToPortal()` / `rem.removeFromPortal()` | string[] | **Portal-W Diff 机制**:仅 type=portal 时可修改。对比当前 vs 目标数组,逐项增删。调用方向:在被引用 Rem 上调用,参数是 Portal Rem |
423
426
 
424
427
  ### parent + positionAmongstSiblings 联动
425
428
 
@@ -0,0 +1,130 @@
1
+ # setup
2
+
3
+ > 启动 Chrome 浏览器让用户登录 RemNote,保存登录凭证到本地 profile。这是 headless 模式(`connect --headless`)的前置步骤。
4
+
5
+ ---
6
+
7
+ ## 功能
8
+
9
+ `setup` 启动一个有界面的 Chrome 窗口(使用独立 profile 目录 `~/.remnote-bridge/chrome-profile`),打开 RemNote 登录页面。用户在浏览器中完成登录后关闭 Chrome,命令写入 `.setup-done` 标记并返回。
10
+
11
+ 后续 `connect --headless` 使用相同 profile 目录,复用已保存的登录凭证,实现免登录的 headless 连接。
12
+
13
+ ---
14
+
15
+ ## 前置条件
16
+
17
+ - 需要**桌面环境(GUI)**,无 GUI 环境会报错
18
+ - 需要系统安装 Chrome/Chromium
19
+
20
+ ---
21
+
22
+ ## 用法
23
+
24
+ ### 人类模式
25
+
26
+ ```bash
27
+ remnote-bridge setup
28
+ ```
29
+
30
+ 输出示例:
31
+
32
+ ```
33
+ 正在启动 Chrome...
34
+ 请在浏览器中登录 RemNote,完成后关闭浏览器窗口。
35
+ setup 完成!
36
+ profile 目录: /Users/xxx/.remnote-bridge/chrome-profile
37
+ 现在可以使用 `remnote-bridge connect --headless` 启动无头连接。
38
+ ```
39
+
40
+ ### JSON 模式
41
+
42
+ ```bash
43
+ remnote-bridge --json setup
44
+ ```
45
+
46
+ ---
47
+
48
+ ## JSON 输出
49
+
50
+ ### 首次 setup
51
+
52
+ ```json
53
+ {
54
+ "ok": true,
55
+ "command": "setup",
56
+ "profileDir": "/Users/xxx/.remnote-bridge/chrome-profile",
57
+ "alreadyDone": false
58
+ }
59
+ ```
60
+
61
+ ### 已完成 setup
62
+
63
+ ```json
64
+ {
65
+ "ok": true,
66
+ "command": "setup",
67
+ "profileDir": "/Users/xxx/.remnote-bridge/chrome-profile",
68
+ "alreadyDone": true
69
+ }
70
+ ```
71
+
72
+ ### 失败
73
+
74
+ ```json
75
+ {
76
+ "ok": false,
77
+ "command": "setup",
78
+ "error": "未检测到桌面环境(无 DISPLAY/WAYLAND_DISPLAY),setup 需要 GUI 才能登录"
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## AI Agent 使用流程
85
+
86
+ setup 会弹出 Chrome 窗口,用户需要完成两件事:登录 RemNote + 配置 dev plugin。
87
+
88
+ ### 交互步骤
89
+
90
+ 1. 调用 `setup`
91
+ 2. **立即告知用户**:
92
+ > 已打开 Chrome 浏览器。请完成以下操作:
93
+ > 1. 登录 RemNote
94
+ > 2. 在 RemNote 中配置开发插件:点击左下角插件图标 → 开发你的插件 → 输入 `http://localhost:8080`
95
+ > 3. 完成后彻底退出 Chrome(macOS 请按 Cmd+Q,仅关窗口不够)
96
+ 3. 等待 `setup` 命令返回(阻塞式,超时 600 秒)
97
+ 4. 收到成功 → 继续执行 `connect --headless`
98
+
99
+ ### setup 之后
100
+
101
+ `setup` 只需执行一次。登录凭证和 plugin 配置都已保存,之后每次只需 `connect --headless` 即可自动连接,无需用户操作。
102
+
103
+ 如果后续 headless 模式下 Plugin 始终不连接,可能是 RemNote 登录 session 过期,需重新 setup(删除 `~/.remnote-bridge/chrome-profile/.setup-done` 后重新执行)。
104
+
105
+ ---
106
+
107
+ ## 幂等性
108
+
109
+ 已完成 setup 后重复调用返回 `ok: true, alreadyDone: true`,不会再次打开 Chrome。
110
+
111
+ 如需重新登录(切换账号),删除 `~/.remnote-bridge/chrome-profile/.setup-done` 文件后重新执行。
112
+
113
+ ---
114
+
115
+ ## 退出码
116
+
117
+ | 退出码 | 含义 |
118
+ |--------|------|
119
+ | 0 | 成功(首次完成或已完成) |
120
+ | 1 | 失败(无 GUI、无 Chrome、超时等) |
121
+
122
+ ---
123
+
124
+ ## 产生的文件
125
+
126
+ | 文件 | 位置 | 说明 |
127
+ |------|------|------|
128
+ | Chrome profile | `~/.remnote-bridge/chrome-profile/` | Chrome 用户数据(含登录凭证) |
129
+ | `.setup-done` | `~/.remnote-bridge/chrome-profile/.setup-done` | setup 完成标记(JSON,含时间戳) |
130
+