remnote-bridge 0.1.13 → 0.1.15

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 (42) hide show
  1. package/README.md +147 -28
  2. package/README.zh-CN.md +374 -0
  3. package/dist/cli/commands/health.js +231 -112
  4. package/dist/cli/commands/read-rem-in-tree.js +84 -0
  5. package/dist/cli/config.js +2 -0
  6. package/dist/cli/daemon/registry.js +8 -0
  7. package/dist/cli/handlers/edit-handler.js +14 -0
  8. package/dist/cli/handlers/patch-engine.js +347 -0
  9. package/dist/cli/handlers/read-handler.js +2 -53
  10. package/dist/cli/handlers/rem-field-filter.js +102 -0
  11. package/dist/cli/handlers/tree-edit-handler.js +67 -7
  12. package/dist/cli/handlers/tree-read-handler.js +4 -1
  13. package/dist/cli/handlers/tree-rem-read-handler.js +73 -0
  14. package/dist/cli/main.js +53 -2
  15. package/dist/cli/server/ws-server.js +9 -1
  16. package/dist/mcp/daemon-client.js +22 -2
  17. package/dist/mcp/instructions.js +99 -58
  18. package/dist/mcp/tools/edit-tools.js +7 -2
  19. package/dist/mcp/tools/infra-tools.js +20 -11
  20. package/dist/mcp/tools/read-tools.js +88 -2
  21. package/package.json +1 -1
  22. package/remnote-plugin/dist/index-sandbox.js +24 -24
  23. package/remnote-plugin/dist/index.js +24 -24
  24. package/remnote-plugin/dist/manifest.json +1 -1
  25. package/remnote-plugin/package.json +1 -1
  26. package/remnote-plugin/public/manifest.json +1 -1
  27. package/remnote-plugin/src/bridge/message-router.ts +3 -0
  28. package/remnote-plugin/src/services/read-rem-in-tree.ts +43 -0
  29. package/remnote-plugin/src/services/read-rem.ts +31 -16
  30. package/remnote-plugin/src/services/read-tree.ts +5 -0
  31. package/remnote-plugin/src/settings.ts +1 -1
  32. package/skills/remnote-bridge/SKILL.md +50 -8
  33. package/skills/remnote-bridge/instructions/connect.md +31 -8
  34. package/skills/remnote-bridge/instructions/disconnect.md +5 -0
  35. package/skills/remnote-bridge/instructions/edit-tree.md +117 -51
  36. package/skills/remnote-bridge/instructions/health.md +81 -53
  37. package/skills/remnote-bridge/instructions/overall.md +39 -8
  38. package/skills/remnote-bridge/instructions/read-rem-in-tree.md +100 -0
  39. package/skills/remnote-bridge/instructions/read-rem.md +30 -11
  40. package/skills/remnote-bridge-test/SKILL.md +847 -0
  41. package/skills/remnote-bridge-test/references/regression-suite.md +960 -0
  42. package/skills/remnote-bridge-test/references/verification-guide.md +161 -0
@@ -174,6 +174,58 @@ oldStr 必须在缓存大纲中恰好匹配 1 次
174
174
 
175
175
  ---
176
176
 
177
+ ## 两种写法:模板模式与完整匹配模式
178
+
179
+ 已有行(带 `<!--remId-->` 注释的行)在 oldStr/newStr 中支持两种写法:
180
+
181
+ ### 模板模式(优先使用)
182
+
183
+ 用 `{{remId}}` 引用已有行,系统在 str_replace 前自动展开为完整行内容(不含缩进)。节省 token、减少复制错误。
184
+
185
+ ```
186
+ # 重排
187
+ oldStr: " {{id1_1}}\n {{id1_2}}"
188
+ newStr: " {{id1_2}}\n {{id1_1}}"
189
+
190
+ # 移动(改变缩进 = 改变父节点)
191
+ oldStr: " {{idA}}\n {{idT}}\n {{idB}}"
192
+ newStr: " {{idA}}\n {{idB}}\n {{idT}}"
193
+
194
+ # 删除(必须同时删子行)
195
+ oldStr: " {{idA}}\n {{idA1}}\n {{idB}}"
196
+ newStr: " {{idB}}"
197
+
198
+ # 新增(新增行手动写,已有行用模板)
199
+ oldStr: " {{idZ}}"
200
+ newStr: " 新增行\n {{idZ}}"
201
+ ```
202
+
203
+ **模板规则**:
204
+ - `{{remId}}` 展开为**不含缩进**的完整行内容,缩进由你控制
205
+ - 只匹配纯字母数字(`[a-zA-Z0-9]+`),与 RemNote cloze 语法 `{{text}}` 不冲突
206
+ - 匹配到但不在缓存大纲中的 `{{xxx}}` 原样保留(可能是 cloze),并输出 templateWarnings
207
+ - 新增行没有 remId,不能用模板表示
208
+
209
+ ### 完整匹配模式(回退)
210
+
211
+ 直接从大纲复制已有行的完整内容(含 `<!--remId 元数据-->`)。
212
+
213
+ ```
214
+ # 重排
215
+ oldStr: " 动态数组 <!--id1_1 type:concept-->\n 静态数组 <!--id1_2 type:concept-->"
216
+ newStr: " 静态数组 <!--id1_2 type:concept-->\n 动态数组 <!--id1_1 type:concept-->"
217
+
218
+ # 移动
219
+ oldStr: " 子节点 A <!--idA-->\n 目标行 <!--idT-->\n 子节点 B <!--idB-->"
220
+ newStr: " 子节点 A <!--idA-->\n 子节点 B <!--idB-->\n 目标行 <!--idT-->"
221
+ ```
222
+
223
+ ### ⚠️ 回退策略
224
+
225
+ **优先使用模板模式**。但如果模板模式连续 2+ 次因 ID 错误导致 `old_str not found`,说明当前上下文不足以准确引用 ID——**立即切换到完整匹配模式**(重新 read_tree,从最新大纲复制完整行内容),不要反复重试模板。
226
+
227
+ ---
228
+
177
229
  ## 支持的操作
178
230
 
179
231
  ### 新增行
@@ -181,12 +233,13 @@ oldStr 必须在缓存大纲中恰好匹配 1 次
181
233
  在 newStr 中添加**无 remId 注释**的新行。新行可以使用 Markdown 前缀和箭头分隔符来设置属性。
182
234
 
183
235
  ```
184
- oldStr:
185
- 子节点 A <!--idA-->
236
+ # 模板模式
237
+ oldStr: " {{idA}}"
238
+ newStr: " 新增节点\n {{idA}}"
186
239
 
187
- newStr:
188
- 新增节点
189
- 子节点 A <!--idA-->
240
+ # 完整匹配模式
241
+ oldStr: " 子节点 A <!--idA-->"
242
+ newStr: " 新增节点\n 子节点 A <!--idA-->"
190
243
  ```
191
244
 
192
245
  #### 新增行的 Markdown 前缀
@@ -266,12 +319,13 @@ newStr:
266
319
  示例:
267
320
 
268
321
  ```
269
- oldStr:
270
- 子节点 A <!--idA-->
322
+ # 模板模式
323
+ oldStr: " {{idA}}"
324
+ newStr: " <!--portal refs:refId1,refId2-->\n {{idA}}"
271
325
 
272
- newStr:
273
- <!--portal refs:refId1,refId2-->
274
- 子节点 A <!--idA-->
326
+ # 完整匹配模式
327
+ oldStr: " 子节点 A <!--idA-->"
328
+ newStr: " <!--portal refs:refId1,refId2-->\n 子节点 A <!--idA-->"
275
329
  ```
276
330
 
277
331
  #### 嵌套新增
@@ -279,11 +333,13 @@ newStr:
279
333
  新增行下面可以再嵌套新增行,通过缩进表示父子关系:
280
334
 
281
335
  ```
282
- newStr:
283
- 父节点 ↓
284
- 答案行 1
285
- 答案行 2
286
- 子节点 A <!--idA-->
336
+ # 模板模式
337
+ oldStr: " {{idA}}"
338
+ newStr: " 父节点 ↓\n 答案行 1\n 答案行 2\n {{idA}}"
339
+
340
+ # 完整匹配模式
341
+ oldStr: " 子节点 A <!--idA-->"
342
+ newStr: " 父节点 ↓\n 答案行 1\n 答案行 2\n 子节点 A <!--idA-->"
287
343
  ```
288
344
 
289
345
  嵌套新增行的父 ID 通过内部占位标记 `__new_N__` 管理,创建顺序保证从浅到深。
@@ -293,14 +349,13 @@ newStr:
293
349
  新行**不能**插在一个有子节点的 Rem 和它的 children 之间,否则 children 会被新行"劫持",触发 `children_captured` 错误。
294
350
 
295
351
  ```
296
- 错误:插在父 Rem 和 children 之间
297
- 水分子 <!--idA-->
298
- 新行 ← children 被解析为新行的子节点!
299
- 化学式 H₂O <!--idB role:card-item-->
352
+ 错误(模板):
353
+ oldStr: " {{idA}}" newStr: " {{idA}}\n 新行" ← idA 有子节点,新行劫持 children!
354
+ 错误(完整匹配):
355
+ oldStr: " 水分子 <!--idA-->" newStr: " 水分子 ↓ <!--idA-->\n 新行" ← 同理
300
356
 
301
- 正确:插在所有兄弟末尾
302
- 极性 ... <!--idZ role:card-item-->
303
- 新行 ← 不影响任何已有节点
357
+ 正确:插在末尾
358
+ oldStr: " {{idZ}}" newStr: " {{idZ}}\n 新行"
304
359
  ```
305
360
 
306
361
  #### 两步操作:创建新节点并移入已有 children
@@ -317,13 +372,13 @@ newStr:
317
372
  从 newStr 中移除带 remId 的行。**必须同时删除该行的所有可见子行**,否则报 orphan_detected 错误。
318
373
 
319
374
  ```
320
- oldStr:
321
- 子节点 A <!--idA-->
322
- 孙节点 A1 <!--idA1-->
323
- 子节点 B <!--idB-->
375
+ # 模板模式
376
+ oldStr: " {{idA}}\n {{idA1}}\n {{idB}}"
377
+ newStr: " {{idB}}"
324
378
 
325
- newStr:
326
- 子节点 B <!--idB-->
379
+ # 完整匹配模式
380
+ oldStr: " 子节点 A <!--idA-->\n 孙节点 A1 <!--idA1-->\n 子节点 B <!--idB-->"
381
+ newStr: " 子节点 B <!--idB-->"
327
382
  ```
328
383
 
329
384
  删除操作按深度**从深到浅**执行(先删子后删父),确保 RemNote SDK 不会拒绝操作。
@@ -333,15 +388,13 @@ newStr:
333
388
  改变行的缩进级别或位置,使其移动到新的父节点下:
334
389
 
335
390
  ```
336
- oldStr:
337
- 子节点 A <!--idA-->
338
- 目标行 <!--idT-->
339
- 子节点 B <!--idB-->
391
+ # 模板模式
392
+ oldStr: " {{idA}}\n {{idT}}\n {{idB}}"
393
+ newStr: " {{idA}}\n {{idB}}\n {{idT}}"
340
394
 
341
- newStr:
342
- 子节点 A <!--idA-->
343
- 子节点 B <!--idB-->
344
- 目标行 <!--idT-->
395
+ # 完整匹配模式
396
+ oldStr: " 子节点 A <!--idA-->\n 目标行 <!--idT-->\n 子节点 B <!--idB-->"
397
+ newStr: " 子节点 A <!--idA-->\n 子节点 B <!--idB-->\n 目标行 <!--idT-->"
345
398
  ```
346
399
 
347
400
  ### 重排行
@@ -349,15 +402,13 @@ newStr:
349
402
  调换同级行的顺序:
350
403
 
351
404
  ```
352
- oldStr:
353
- 子节点 A <!--idA-->
354
- 子节点 B <!--idB-->
355
- 子节点 C <!--idC-->
405
+ # 模板模式
406
+ oldStr: " {{idA}}\n {{idB}}\n {{idC}}"
407
+ newStr: " {{idC}}\n {{idA}}\n {{idB}}"
356
408
 
357
- newStr:
358
- 子节点 C <!--idC-->
359
- 子节点 A <!--idA-->
360
- 子节点 B <!--idB-->
409
+ # 完整匹配模式
410
+ oldStr: " 子节点 A <!--idA-->\n 子节点 B <!--idB-->\n 子节点 C <!--idC-->"
411
+ newStr: " 子节点 C <!--idC-->\n 子节点 A <!--idA-->\n 子节点 B <!--idB-->"
361
412
  ```
362
413
 
363
414
  ---
@@ -458,8 +509,9 @@ RemNote SDK 存在已知 bug:
458
509
  4. daemon TreeEditHandler:
459
510
  ├─ 防线 1: cache.get('tree:' + remId) 存在?
460
511
  ├─ 防线 2: 用缓存的 depth/maxNodes/maxSiblings 重新 read-tree → 对比
461
- ├─ 防线 3: countOccurrences(cachedOutline, oldStr) === 1?
462
- ├─ modifiedOutline = cachedOutline.replace(oldStr, newStr)
512
+ ├─ 模板展开: {{remId}} 缓存中对应行的完整内容(不含缩进)
513
+ ├─ 防线 3: countOccurrences(cachedOutline, expandedOldStr) === 1?
514
+ ├─ modifiedOutline = cachedOutline.replace(expandedOldStr, expandedNewStr)
463
515
  ├─ 解析新旧大纲为树(parseOutline)
464
516
  ├─ 对比差异(diffTrees)
465
517
  │ ├─ 根节点校验
@@ -482,28 +534,42 @@ RemNote SDK 存在已知 bug:
482
534
 
483
535
  ---
484
536
 
485
- ## 常见使用模式
537
+ ## 常见使用模式(JSON 模式)
538
+
539
+ > 优先使用模板模式;连续 2+ 次 `old_str not found` 则回退到完整匹配模式。
486
540
 
487
541
  ### 在指定位置插入新行
488
542
 
489
543
  ```bash
490
- remnote-bridge edit-tree kLr --old-str ' 子节点 A <!--idA-->' --new-str ' 新增行\n 子节点 A <!--idA-->'
544
+ # 模板模式
545
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{idA}}","newStr":" 新增行\n {{idA}}"}'
546
+
547
+ # 完整匹配模式
548
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" 子节点 A <!--idA-->","newStr":" 新增行\n 子节点 A <!--idA-->"}'
491
549
  ```
492
550
 
493
551
  ### 删除一个叶子节点
494
552
 
495
553
  ```bash
496
- remnote-bridge edit-tree kLr --old-str ' 叶子节点 <!--leaf-->\n' --new-str ''
554
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{leaf}}\n","newStr":""}'
497
555
  ```
498
556
 
499
557
  ### 调换两个兄弟的顺序
500
558
 
501
559
  ```bash
502
- remnote-bridge edit-tree kLr --old-str ' 节点 A <!--idA-->\n 节点 B <!--idB-->' --new-str ' 节点 B <!--idB-->\n 节点 A <!--idA-->'
560
+ # 模板模式
561
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{idA}}\n {{idB}}","newStr":" {{idB}}\n {{idA}}"}'
562
+
563
+ # 完整匹配模式
564
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" 节点 A <!--idA-->\n 节点 B <!--idB-->","newStr":" 节点 B <!--idB-->\n 节点 A <!--idA-->"}'
503
565
  ```
504
566
 
505
567
  ### 将节点移到另一个父节点下
506
568
 
507
569
  ```bash
508
- remnote-bridge edit-tree kLr --old-str ' 旧父 <!--oldP-->\n 目标 <!--target-->\n 新父 <!--newP-->' --new-str ' 旧父 <!--oldP-->\n 新父 <!--newP-->\n 目标 <!--target-->'
570
+ # 模板模式
571
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{oldP}}\n {{target}}\n {{newP}}","newStr":" {{oldP}}\n {{newP}}\n {{target}}"}'
572
+
573
+ # 完整匹配模式
574
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" 旧父 <!--oldP-->\n 目标 <!--target-->\n 新父 <!--newP-->","newStr":" 旧父 <!--oldP-->\n 新父 <!--newP-->\n 目标 <!--target-->"}'
509
575
  ```
@@ -6,65 +6,64 @@
6
6
 
7
7
  ## 功能
8
8
 
9
- `health` 分两步检查指定实例的系统状态:
9
+ `health` 检查系统状态,支持两种模式:
10
10
 
11
+ 1. **全量模式**(默认):遍历注册表所有活跃实例,逐个查询三层状态
12
+ 2. **单实例模式**(`--instance` / `--headless`):只查询指定实例
13
+
14
+ 每个实例的检查分两步:
11
15
  1. **本地检查**:通过注册表查找实例,确认 daemon 进程是否存活
12
16
  2. **远程检查**:通过 WS 连接 daemon,获取 Plugin 连接状态和 SDK 就绪状态
13
17
 
14
- ### 多实例支持
18
+ ### 孪生连接
15
19
 
16
- 通过 `--instance <name>` 指定要检查的实例。不指定时检查 `default` 实例。
17
-
18
- ```bash
19
- remnote-bridge health --instance work
20
- ```
20
+ 每个实例的 Plugin 连接会标记是否为**孪生连接**(`plugin.isTwin`)。孪生连接表示 Plugin `twinSlotIndex` 与 daemon 的槽位索引匹配,优先级更高——孪生连接可以抢占非孪生连接。
21
21
 
22
22
  ---
23
23
 
24
24
  ## 用法
25
25
 
26
- ### 人类模式
26
+ ### 全量模式(默认)
27
27
 
28
28
  ```bash
29
29
  remnote-bridge health
30
30
  ```
31
31
 
32
- 输出示例(全部健康):
32
+ 输出所有活跃实例的状态:
33
33
 
34
34
  ```
35
- 守护进程 运行中(PID: 12345,实例: default,槽位: 0,已运行 5 分钟)
36
- Plugin 已连接
35
+ === 实例: default(槽位 0)===
36
+ 守护进程 运行中(PID: 12345,已运行 5 分钟)
37
+ ✅ Plugin 已连接(孪生)
37
38
  ✅ SDK 就绪
38
-
39
39
  超时: 25 分钟后自动关闭
40
- ```
41
-
42
- 输出示例(部分不健康):
43
-
44
- ```
45
- ✅ 守护进程 运行中(PID: 12345,实例: work,槽位: 1,已运行 2 分钟)
46
- ❌ Plugin 未连接
47
- ❌ SDK 未就绪
48
40
 
41
+ === 实例: headless(槽位 1)===
42
+ ✅ 守护进程 运行中(PID: 12346,已运行 2 分钟)
43
+ ✅ Plugin 已连接(非孪生)
44
+ ✅ SDK 就绪
45
+ ✅ Chrome running
49
46
  超时: 28 分钟后自动关闭
50
47
  ```
51
48
 
52
- 输出示例(daemon 未运行):
49
+ 无活跃实例时:
53
50
 
54
51
  ```
55
- 守护进程 未运行
56
- ❌ Plugin 未连接
57
- ❌ SDK 不可用
58
-
59
- 提示: 执行 `remnote-bridge connect` 启动守护进程
52
+ 没有活跃的实例。执行 `remnote-bridge connect` 启动守护进程。
60
53
  ```
61
54
 
62
- ### JSON 模式
55
+ ### 单实例模式
63
56
 
64
57
  ```bash
65
- remnote-bridge --json health
58
+ # 指定实例
59
+ remnote-bridge --instance work health
60
+
61
+ # 检查 headless 实例
62
+ remnote-bridge --headless health
66
63
  ```
67
64
 
65
+ 输出格式与之前相同,但只显示一个实例。
66
+
68
67
  ### Headless 诊断模式
69
68
 
70
69
  ```bash
@@ -81,7 +80,43 @@ remnote-bridge health --reload
81
80
 
82
81
  ## JSON 输出
83
82
 
84
- ### 全部健康
83
+ ### 全量模式
84
+
85
+ ```json
86
+ {
87
+ "ok": true,
88
+ "command": "health",
89
+ "exitCode": 0,
90
+ "instances": [
91
+ {
92
+ "instance": "default",
93
+ "slotIndex": 0,
94
+ "daemon": { "running": true, "pid": 12345, "reachable": true, "uptime": 300 },
95
+ "plugin": { "connected": true, "isTwin": true },
96
+ "sdk": { "ready": true },
97
+ "timeoutRemaining": 1500
98
+ },
99
+ {
100
+ "instance": "headless",
101
+ "slotIndex": 1,
102
+ "daemon": { "running": true, "pid": 12346, "reachable": true, "uptime": 120 },
103
+ "plugin": { "connected": true, "isTwin": true },
104
+ "sdk": { "ready": true },
105
+ "timeoutRemaining": 1680,
106
+ "headless": {
107
+ "status": "running",
108
+ "chromeConnected": true,
109
+ "pageUrl": "http://localhost:29111",
110
+ "reloadCount": 0,
111
+ "lastError": null,
112
+ "recentConsoleErrors": []
113
+ }
114
+ }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ ### 单实例模式 — 全部健康
85
120
 
86
121
  ```json
87
122
  {
@@ -91,39 +126,36 @@ remnote-bridge health --reload
91
126
  "instance": "default",
92
127
  "slotIndex": 0,
93
128
  "daemon": { "running": true, "pid": 12345, "reachable": true, "uptime": 300 },
94
- "plugin": { "connected": true },
129
+ "plugin": { "connected": true, "isTwin": true },
95
130
  "sdk": { "ready": true },
96
131
  "timeoutRemaining": 1500
97
132
  }
98
133
  ```
99
134
 
100
- ### Plugin 未连接
135
+ ### 单实例模式 — daemon 未运行
101
136
 
102
137
  ```json
103
138
  {
104
139
  "ok": false,
105
140
  "command": "health",
106
- "exitCode": 1,
107
- "instance": "work",
108
- "slotIndex": 1,
109
- "daemon": { "running": true, "pid": 12345, "reachable": true, "uptime": 120 },
141
+ "exitCode": 2,
142
+ "instance": "default",
143
+ "daemon": { "running": false },
110
144
  "plugin": { "connected": false },
111
145
  "sdk": { "ready": false },
112
- "timeoutRemaining": 1680
146
+ "error": "守护进程未运行(实例: default),请先执行 remnote-bridge connect"
113
147
  }
114
148
  ```
115
149
 
116
- ### daemon 未运行
150
+ ### 全量模式 — 无活跃实例
117
151
 
118
152
  ```json
119
153
  {
120
154
  "ok": false,
121
155
  "command": "health",
122
156
  "exitCode": 2,
123
- "instance": "default",
124
- "daemon": { "running": false },
125
- "plugin": { "connected": false },
126
- "sdk": { "ready": false }
157
+ "instances": [],
158
+ "error": "没有活跃的实例,请执行 remnote-bridge connect 启动守护进程"
127
159
  }
128
160
  ```
129
161
 
@@ -135,6 +167,7 @@ remnote-bridge health --reload
135
167
  |--------|----------|------|
136
168
  | **daemon** | 注册表查找 + `kill(pid, 0)` 探活 | 守护进程是否在运行且可达 |
137
169
  | **plugin** | daemon 内部的 `pluginConnected` 状态 | RemNote Plugin 是否已通过 WS 连接到 daemon |
170
+ | **plugin.isTwin** | Plugin hello 握手中的 `twinSlotIndex` | 是否为孪生连接(匹配 daemon 槽位索引) |
138
171
  | **sdk** | Plugin 的 hello 握手中的 `sdkReady` 字段 | RemNote SDK 是否就绪(知识库已加载,可调用 API) |
139
172
 
140
173
  ### 三层关系
@@ -151,9 +184,9 @@ daemon 运行 → Plugin 连接 → SDK 就绪
151
184
 
152
185
  | 退出码 | 含义 | 触发条件 |
153
186
  |--------|------|----------|
154
- | 0 | 全部健康 | daemon 运行 + Plugin 已连接 + SDK 就绪 |
187
+ | 0 | 全部健康 | 所有实例三层均通过 |
155
188
  | 1 | 部分不健康 | daemon 运行但 Plugin 未连接或 SDK 未就绪 |
156
- | 2 | 不可达 | daemon 未运行,或运行但 WS 连接失败 |
189
+ | 2 | 不可达 | 无活跃实例,或 daemon 不可达 |
157
190
 
158
191
  ---
159
192
 
@@ -161,11 +194,13 @@ daemon 运行 → Plugin 连接 → SDK 就绪
161
194
 
162
195
  | 字段 | 类型 | 说明 |
163
196
  |------|------|------|
197
+ | `instances` | array | 全量模式下所有实例的状态数组 |
164
198
  | `daemon.running` | boolean | 进程是否存活 |
165
199
  | `daemon.pid` | number | 进程 ID(仅运行时) |
166
200
  | `daemon.reachable` | boolean | WS 连接是否成功(仅运行时) |
167
201
  | `daemon.uptime` | number | 运行秒数(仅可达时) |
168
202
  | `plugin.connected` | boolean | Plugin WS 连接是否建立 |
203
+ | `plugin.isTwin` | boolean | 是否为孪生连接 |
169
204
  | `sdk.ready` | boolean | RemNote SDK 是否就绪 |
170
205
  | `timeoutRemaining` | number | 距自动关闭的剩余秒数(仅可达时) |
171
206
 
@@ -173,19 +208,12 @@ daemon 运行 → Plugin 连接 → SDK 就绪
173
208
 
174
209
  ## Headless 模式附加输出
175
210
 
176
- ### health 基础输出(headless 模式下额外字段)
211
+ ### health 基础输出(headless 实例额外字段)
177
212
 
178
- headless 模式下 `health` 基础输出额外包含 `headless` 对象:
213
+ headless 实例额外包含 `headless` 对象:
179
214
 
180
215
  ```json
181
216
  {
182
- "ok": true,
183
- "command": "health",
184
- "exitCode": 0,
185
- "daemon": { "running": true, "pid": 12345, "reachable": true, "uptime": 300 },
186
- "plugin": { "connected": true },
187
- "sdk": { "ready": true },
188
- "timeoutRemaining": 1500,
189
217
  "headless": {
190
218
  "status": "running",
191
219
  "chromeConnected": true,
@@ -227,7 +255,7 @@ headless 模式下 `health` 基础输出额外包含 `headless` 对象:
227
255
 
228
256
  | 症状 | 可能原因 | 解决方案 |
229
257
  |------|----------|----------|
230
- | daemon 未运行 | 未执行 connect / 已超时关闭 | 执行 `connect` |
258
+ | 无活跃实例 | 未执行 connect / 已超时关闭 | 执行 `connect` |
231
259
  | daemon 运行但不可达 | WS 端口被占用或配置不匹配 | 检查 `~/.remnote-bridge/slots.json` 中的端口配置 |
232
260
  | Plugin 未连接(标准模式) | RemNote 未打开 / Plugin 未安装 / URL 不匹配 | 打开 RemNote,确认 Plugin 中的 WS URL 设置 |
233
261
  | Plugin 未连接(headless 模式) | Chrome 页面加载异常 | `health --diagnose` 查看截图和状态,`health --reload` 重载页面 |
@@ -44,6 +44,7 @@ Agent 的核心任务是将用户的自然语言请求翻译为 CLI 命令。以
44
44
  | "我现在在看什么"、"当前页面" | 用户当前焦点/页面 | `read-context` |
45
45
  | "展开这个主题"、"看看下面有什么" | Rem 子树 | `read-tree <remId>` |
46
46
  | "这个笔记的详细信息" | Rem 的完整属性 | `read-rem <remId>` |
47
+ | "展开子树并获取所有属性" | 子树大纲 + 节点属性 | `read-rem-in-tree <remId>` |
47
48
  | "搜索 X"、"查找关于 X 的内容" | 全文搜索 | `search <query>` |
48
49
 
49
50
  #### 修改 / 写入
@@ -189,6 +190,8 @@ RemNote SDK → 知识库
189
190
 
190
191
  一次**会话(Session)= 守护进程的生命周期**。
191
192
 
193
+ **标准模式(推荐)**——用户在自己的浏览器中操作 RemNote,Agent 可感知用户上下文:
194
+
192
195
  ```
193
196
  connect → daemon 启动
194
197
 
@@ -200,6 +203,10 @@ disconnect → daemon 关闭 → 会话结束,缓存清空
200
203
  ```
201
204
 
202
205
  > **重要**:`connect` 成功只意味着 daemon 已启动,Plugin 并未自动连接。首次使用需用户在 RemNote「开发你的插件」中填入对应的 Plugin 服务地址;非首次只需刷新 RemNote 页面。必须引导用户完成此步后再用 `health` 确认就绪。
206
+ >
207
+ > **⚠️ 防幻觉红线**:本插件是**开发者插件**,通过「开发你的插件」加载本地 URL。**禁止**告诉用户去插件市场/商店搜索安装(插件不在市场中);**禁止**编造"Settings → Plugins"等不存在的路径。
208
+
209
+ **Headless 模式(不推荐日常使用)**——通过后台 Chrome 自动连接,但**会丢失用户上下文**(`read-context` 返回 headless 实例的上下文,不是用户浏览器的)。仅在以下场景使用:用户明确要求在服务器/无 GUI 环境运行、用户明确不想参与操作(全自动化)、用户不在 RemNote 前面。详见 `connect.md`。
203
210
 
204
211
  `connect` 启动三个服务,端口由槽位自动分配:
205
212
 
@@ -239,7 +246,7 @@ remnote-bridge disconnect --instance work
239
246
  | 2 | 29120 | 29121 | 29122 |
240
247
  | 3 | 29130 | 29131 | 29132 |
241
248
 
242
- **实例名解析优先级**:CLI `--instance` > 环境变量 `REMNOTE_BRIDGE_INSTANCE` > 默认值 `default`。
249
+ **实例名解析优先级**:CLI `--instance` > 环境变量 `REMNOTE_BRIDGE_INSTANCE` > 默认值 `default`。`headless` 是保留实例名,不可用于 `--instance`(会报错),必须使用 `--headless` 全局选项。
243
250
 
244
251
  **Plugin 自动发现**:Plugin 启动后通过 `/api/discovery` 获取其孪生 daemon 的连接信息(WS 端口、槽位索引等),自动建立连接。一个 Plugin 可同时连接最多 4 个 daemon。
245
252
 
@@ -282,6 +289,7 @@ remnote-bridge disconnect --instance work
282
289
  | `read-context` | 当前上下文视图 | mode + 参数 | 否 | `read-context.md` |
283
290
  | `read-tree` | 读取子树为 Markdown 大纲 | remId + 展开参数 | 是(`tree:`) | `read-tree.md` |
284
291
  | `read-rem` | 读取单个 Rem 的 JSON 属性 | remId | 是(`rem:`) | `read-rem.md` |
292
+ | `read-rem-in-tree` | 子树大纲 + 节点属性一次获取 | remId + 展开参数 + 过滤参数 | 是(`tree:` + `rem:`) | `read-rem-in-tree.md` |
285
293
  | `search` | 全文搜索 | query | 否 | `search.md` |
286
294
 
287
295
  #### 写入命令
@@ -309,10 +317,17 @@ Agent 需要根据用户意图选择正确的读取命令:
309
317
  ├─ 某个具体 Rem 的子树 → read-tree <remId>
310
318
  │ 完整展开子树(支持深度/节点预算控制)
311
319
  │ 结果缓存,供 edit-tree 使用
320
+ │ ⚠️ 如果需要读取子树后对其中多个节点执行 edit-rem,请改用 read-rem-in-tree
312
321
 
313
322
  ├─ 某个 Rem 的详细属性 → read-rem <remId>
314
323
  │ 返回 51 字段的 RemObject JSON
315
324
  │ 结果缓存,供 edit-rem 使用
325
+ │ ⚠️ 如果需要读取多个 Rem 属性(≥3 个)且在同一子树下,请改用 read-rem-in-tree
326
+
327
+ ├─ 子树结构 + 每个节点的详细属性 → read-rem-in-tree <remId>
328
+ │ read-tree + read-rem 的合体,一次调用同时获取大纲和 RemObject
329
+ │ 同时建立 tree 和 rem 双重缓存,供 edit-tree 和 edit-rem 使用
330
+ │ 默认 maxNodes=50(比 read-tree 的 200 低,因每节点开销大)
316
331
 
317
332
  └─ 按关键词搜索 → search <query>
318
333
  全文搜索,返回匹配的 Rem 列表
@@ -327,6 +342,7 @@ Agent 需要根据用户意图选择正确的读取命令:
327
342
  | "我现在在编辑什么" | `read-context --mode focus` | 鱼眼视图,焦点处详细 |
328
343
  | "当前页面的内容" | `read-context --mode page` | 以页面为根展开 |
329
344
  | "展开某个主题的细节" | `read-tree <id>` | 完整子树,可缓存供编辑 |
345
+ | "展开子树并查看每个节点属性" | `read-rem-in-tree <id>` | 大纲 + RemObject,双重缓存 |
330
346
 
331
347
  #### read-globe 特性
332
348
 
@@ -390,14 +406,28 @@ Agent 需要根据用户意图选择正确的读取命令:
390
406
  4. read-globe ← 了解知识库结构(首次探索)
391
407
  或 read-context ← 了解用户当前上下文
392
408
  5. search "关键词" ← 定位目标 Rem(中文搜索可能需单字策略,详见 search.md)
393
- 6. read-tree <id> 展开目标区域的子树
394
- 7. read-rem <id> 读取详细属性(编辑前必需)
395
- 8. edit-rem <id> ... ← 修改 Rem 属性
409
+ 6a. [单节点] read-tree <id> + read-rem <id> 各自建立缓存
410
+ 6b. [多节点] read-rem-in-tree <id> 一次建立双重缓存(推荐 ≥3 个节点需修改时)
411
+ 7. edit-rem <id> ... ← 修改 Rem 属性
396
412
  或 edit-tree <id> ...← 修改树结构
397
413
  9. disconnect ← 结束会话
398
414
  ```
399
415
 
400
- **注意**:步骤 7`edit-rem` 的强制前置条件,步骤 6 是 `edit-tree` 的强制前置条件。跳过会触发防线 1 错误。步骤 2 是必须的——connect 后不引导用户加载插件就直接调用业务命令,会报"Plugin 未连接"错误。
416
+ **注意**:步骤 6a/6b 是 edit-rem / edit-tree 的强制前置条件。跳过会触发防线 1 错误。步骤 2 是必须的——connect 后不引导用户加载插件就直接调用业务命令,会报"Plugin 未连接"错误。
417
+
418
+ ### 4.5 批量标注工作流(课本划重点场景)
419
+
420
+ 当需要对一棵子树中的多个节点进行富文本标注(行级高亮、行内荧光、粗体等)时:
421
+
422
+ 1. read-rem-in-tree <id> --maxNodes 50 -- 一次获取大纲 + 所有 RemObject
423
+ 2. 从 remObjects 中定位需要标注的节点
424
+ 3. 对每个目标节点 edit-rem 设置格式:
425
+ - 行级高亮:changes.highlightColor = "Yellow"/"Red"/...
426
+ - 行内荧光:changes.text = [..., {"h": 3, "i": "m", "text": "关键词"}, ...]
427
+ - 粗体:changes.text = [..., {"b": true, "i": "m", "text": "核心概念"}, ...]
428
+ 4. 如需结构变更(如新增/移动节点),直接 edit-tree(tree 缓存已就绪)
429
+
430
+ 关键:read-rem-in-tree 同时建立了 tree 和 rem 两种缓存,后续 edit-tree 和 edit-rem 都无需再单独 read。
401
431
 
402
432
  ---
403
433
 
@@ -465,6 +495,7 @@ daemon → CLI 响应:
465
495
  | `read_rem` | read-rem | readRem() | 直接转发 |
466
496
  | `edit_rem` | edit-rem | — | daemon handler 编排 |
467
497
  | `read_tree` | read-tree | readTree() | 直接转发 |
498
+ | `read_rem_in_tree` | read-rem-in-tree | readRemInTree() | daemon handler 编排 + Plugin 转发 |
468
499
  | `edit_tree` | edit-tree | — | daemon handler 编排 |
469
500
  | `read_globe` | read-globe | readGlobe() | 直接转发 |
470
501
  | `read_context` | read-context | readContext() | 直接转发 |
@@ -690,9 +721,9 @@ read-tree / read-globe / read-context 的输出核心是 Markdown 大纲文本
690
721
 
691
722
  | 前缀 | 用途 | 写入命令 |
692
723
  |:-----|:-----|:---------|
693
- | `rem:{remId}` | RemObject 对象 | read-rem |
694
- | `tree:{remId}` | Markdown 大纲 | read-tree |
695
- | `tree-depth:{remId}` 等 | read-tree 参数 | read-tree |
724
+ | `rem:{remId}` | RemObject 对象 | read-rem, read-rem-in-tree |
725
+ | `tree:{remId}` | Markdown 大纲 | read-tree, read-rem-in-tree |
726
+ | `tree-depth:{remId}` 等 | read-tree 参数 | read-tree, read-rem-in-tree |
696
727
 
697
728
  - LRU 淘汰:上限 200 条目
698
729
  - disconnect 关闭 daemon 时缓存自动消失