remnote-bridge 0.1.12 → 0.1.14

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 (50) hide show
  1. package/README.md +141 -28
  2. package/README.zh-CN.md +368 -0
  3. package/dist/cli/commands/edit-rem.js +5 -5
  4. package/dist/cli/commands/health.js +231 -112
  5. package/dist/cli/commands/read-rem-in-tree.js +84 -0
  6. package/dist/cli/commands/read-rem.js +3 -1
  7. package/dist/cli/config.js +2 -0
  8. package/dist/cli/daemon/registry.js +8 -0
  9. package/dist/cli/handlers/edit-handler.js +49 -140
  10. package/dist/cli/handlers/patch-engine.js +347 -0
  11. package/dist/cli/handlers/read-handler.js +5 -57
  12. package/dist/cli/handlers/rem-cache.js +10 -5
  13. package/dist/cli/handlers/rem-field-filter.js +102 -0
  14. package/dist/cli/handlers/tree-edit-handler.js +67 -7
  15. package/dist/cli/handlers/tree-read-handler.js +4 -1
  16. package/dist/cli/handlers/tree-rem-read-handler.js +73 -0
  17. package/dist/cli/main.js +71 -12
  18. package/dist/cli/server/ws-server.js +9 -1
  19. package/dist/mcp/daemon-client.js +22 -2
  20. package/dist/mcp/format.js +43 -0
  21. package/dist/mcp/index.js +0 -55
  22. package/dist/mcp/instructions.js +447 -284
  23. package/dist/mcp/resources/edit-rem-guide.js +37 -157
  24. package/dist/mcp/resources/edit-tree-guide.js +1 -1
  25. package/dist/mcp/resources/error-reference.js +9 -13
  26. package/dist/mcp/resources/rem-object-fields.js +3 -3
  27. package/dist/mcp/tools/edit-tools.js +76 -10
  28. package/dist/mcp/tools/infra-tools.js +30 -33
  29. package/dist/mcp/tools/read-tools.js +221 -26
  30. package/package.json +1 -1
  31. package/remnote-plugin/dist/index-sandbox.js +24 -24
  32. package/remnote-plugin/dist/index.js +24 -24
  33. package/remnote-plugin/src/bridge/message-router.ts +3 -0
  34. package/remnote-plugin/src/services/read-rem-in-tree.ts +43 -0
  35. package/remnote-plugin/src/services/read-rem.ts +15 -0
  36. package/remnote-plugin/src/services/read-tree.ts +5 -0
  37. package/skills/remnote-bridge/SKILL.md +71 -38
  38. package/skills/remnote-bridge/instructions/connect.md +12 -1
  39. package/skills/remnote-bridge/instructions/disconnect.md +5 -0
  40. package/skills/remnote-bridge/instructions/edit-rem.md +105 -347
  41. package/skills/remnote-bridge/instructions/edit-tree.md +71 -2
  42. package/skills/remnote-bridge/instructions/health.md +81 -53
  43. package/skills/remnote-bridge/instructions/overall.md +55 -21
  44. package/skills/remnote-bridge/instructions/read-rem-in-tree.md +100 -0
  45. package/skills/remnote-bridge/instructions/read-rem.md +35 -16
  46. package/skills/remnote-bridge/instructions/search.md +4 -4
  47. package/skills/remnote-bridge/instructions/setup.md +5 -6
  48. package/skills/remnote-bridge-test/SKILL.md +847 -0
  49. package/skills/remnote-bridge-test/references/regression-suite.md +960 -0
  50. package/skills/remnote-bridge-test/references/verification-guide.md +161 -0
@@ -174,6 +174,66 @@ oldStr 必须在缓存大纲中恰好匹配 1 次
174
174
 
175
175
  ---
176
176
 
177
+ ## 行引用模板 `{{remId}}`
178
+
179
+ 在 oldStr/newStr 中使用 `{{remId}}` 引用缓存大纲中已有行的完整内容(不含缩进)。系统在 str_replace 前自动展开。不含 `{{}}` 的传统写法完全兼容。
180
+
181
+ ### 动机
182
+
183
+ AI 构造 oldStr/newStr 时需精确复制已有行(含 17+ 字符 Rem ID 和元数据标记),导致 token 浪费和复制错误。`{{remId}}` 让 AI 只写 ID,系统自动替换为完整行内容。
184
+
185
+ ### 展开规则
186
+
187
+ | 输入 | 展开为 |
188
+ |------|--------|
189
+ | `{{remId}}` | 该 remId 对应行的去缩进完整内容(含 `<!--remId 元数据-->`) |
190
+ | ` {{remId}}` | AI 写的缩进 + 展开后的完整内容 |
191
+ | 不含 `{{}}` 的文本 | 原样不变 |
192
+
193
+ ### 示例
194
+
195
+ **重排(对比传统写法)**
196
+
197
+ ```
198
+ # 传统写法(~250 tokens)
199
+ oldStr: " 动态数组 <!--id1_1 type:concept-->\n 静态数组 <!--id1_2 type:concept-->"
200
+ newStr: " 静态数组 <!--id1_2 type:concept-->\n 动态数组 <!--id1_1 type:concept-->"
201
+
202
+ # 模板写法(~50 tokens)
203
+ oldStr: " {{id1_1}}\n {{id1_2}}"
204
+ newStr: " {{id1_2}}\n {{id1_1}}"
205
+ ```
206
+
207
+ **移动(改变缩进)**
208
+
209
+ ```
210
+ oldStr: " {{idA}}\n {{idT}}\n {{idB}}"
211
+ newStr: " {{idA}}\n {{idB}}\n {{idT}}"
212
+ ```
213
+
214
+ **删除(模板用于上下文定位)**
215
+
216
+ ```
217
+ oldStr: " {{idA}}\n {{idA1}}\n {{idB}}"
218
+ newStr: " {{idB}}"
219
+ ```
220
+
221
+ **新增 + 模板混用**
222
+
223
+ ```
224
+ oldStr: " {{idZ}}"
225
+ newStr: " 新增行\n {{idZ}}"
226
+ ```
227
+
228
+ ### 限制
229
+
230
+ - 只匹配纯字母数字(`[a-zA-Z0-9]+`),与 RemNote cloze 语法 `{{text}}` 不冲突(cloze 含中文/空格/标点不会被匹配)
231
+ - 匹配到但不在缓存大纲中的 `{{xxx}}` 原样保留(可能是 cloze),并输出 templateWarnings
232
+ - `{{remId}}` 不含缩进,缩进由 AI 控制(move 操作会改变缩进)
233
+ - 新增行没有 remId,不能用模板表示
234
+
235
+ ---
236
+
177
237
  ## 支持的操作
178
238
 
179
239
  ### 新增行
@@ -458,8 +518,9 @@ RemNote SDK 存在已知 bug:
458
518
  4. daemon TreeEditHandler:
459
519
  ├─ 防线 1: cache.get('tree:' + remId) 存在?
460
520
  ├─ 防线 2: 用缓存的 depth/maxNodes/maxSiblings 重新 read-tree → 对比
461
- ├─ 防线 3: countOccurrences(cachedOutline, oldStr) === 1?
462
- ├─ modifiedOutline = cachedOutline.replace(oldStr, newStr)
521
+ ├─ 模板展开: {{remId}} 缓存中对应行的完整内容(不含缩进)
522
+ ├─ 防线 3: countOccurrences(cachedOutline, expandedOldStr) === 1?
523
+ ├─ modifiedOutline = cachedOutline.replace(expandedOldStr, expandedNewStr)
463
524
  ├─ 解析新旧大纲为树(parseOutline)
464
525
  ├─ 对比差异(diffTrees)
465
526
  │ ├─ 根节点校验
@@ -499,11 +560,19 @@ remnote-bridge edit-tree kLr --old-str ' 叶子节点 <!--leaf-->\n' --new-st
499
560
  ### 调换两个兄弟的顺序
500
561
 
501
562
  ```bash
563
+ # 传统写法
502
564
  remnote-bridge edit-tree kLr --old-str ' 节点 A <!--idA-->\n 节点 B <!--idB-->' --new-str ' 节点 B <!--idB-->\n 节点 A <!--idA-->'
565
+
566
+ # 模板写法(JSON 模式)
567
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{idA}}\n {{idB}}","newStr":" {{idB}}\n {{idA}}"}'
503
568
  ```
504
569
 
505
570
  ### 将节点移到另一个父节点下
506
571
 
507
572
  ```bash
573
+ # 传统写法
508
574
  remnote-bridge edit-tree kLr --old-str ' 旧父 <!--oldP-->\n 目标 <!--target-->\n 新父 <!--newP-->' --new-str ' 旧父 <!--oldP-->\n 新父 <!--newP-->\n 目标 <!--target-->'
575
+
576
+ # 模板写法(JSON 模式)
577
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{oldP}}\n {{target}}\n {{newP}}","newStr":" {{oldP}}\n {{newP}}\n {{target}}"}'
509
578
  ```
@@ -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
  #### 修改 / 写入
@@ -128,7 +129,7 @@ Rem 有两个**独立维度**的类型:
128
129
  |:-----|:-----|:-----|
129
130
  | 创建 Portal | `edit-tree` | 新增行 `<!--portal refs:id1,id2-->` |
130
131
  | 删除 Portal | `edit-tree` | 从大纲中移除 Portal 行(与删除普通行相同) |
131
- | 修改引用列表(增删引用的 Rem) | `edit-rem` | str_replace 简化 JSON 中的 `portalDirectlyIncludedRem` 数组 |
132
+ | 修改引用列表(增删引用的 Rem) | `edit-rem` | 直接修改 changes 中的 `portalDirectlyIncludedRem` 数组 |
132
133
  | 移动 Portal(换父节点/位置) | `edit-tree` | 与移动普通行相同 |
133
134
  | 读取 Portal | `read-rem` | 自动输出 8 字段简化 JSON |
134
135
 
@@ -239,7 +240,7 @@ remnote-bridge disconnect --instance work
239
240
  | 2 | 29120 | 29121 | 29122 |
240
241
  | 3 | 29130 | 29131 | 29132 |
241
242
 
242
- **实例名解析优先级**:CLI `--instance` > 环境变量 `REMNOTE_BRIDGE_INSTANCE` > 默认值 `default`。
243
+ **实例名解析优先级**:CLI `--instance` > 环境变量 `REMNOTE_BRIDGE_INSTANCE` > 默认值 `default`。`headless` 是保留实例名,不可用于 `--instance`(会报错),必须使用 `--headless` 全局选项。
243
244
 
244
245
  **Plugin 自动发现**:Plugin 启动后通过 `/api/discovery` 获取其孪生 daemon 的连接信息(WS 端口、槽位索引等),自动建立连接。一个 Plugin 可同时连接最多 4 个 daemon。
245
246
 
@@ -282,13 +283,14 @@ remnote-bridge disconnect --instance work
282
283
  | `read-context` | 当前上下文视图 | mode + 参数 | 否 | `read-context.md` |
283
284
  | `read-tree` | 读取子树为 Markdown 大纲 | remId + 展开参数 | 是(`tree:`) | `read-tree.md` |
284
285
  | `read-rem` | 读取单个 Rem 的 JSON 属性 | remId | 是(`rem:`) | `read-rem.md` |
286
+ | `read-rem-in-tree` | 子树大纲 + 节点属性一次获取 | remId + 展开参数 + 过滤参数 | 是(`tree:` + `rem:`) | `read-rem-in-tree.md` |
285
287
  | `search` | 全文搜索 | query | 否 | `search.md` |
286
288
 
287
289
  #### 写入命令
288
290
 
289
291
  | 命令 | 功能 | 前置条件 | 安全机制 | 详细文档 |
290
292
  |:-----|:-----|:---------|:---------|:---------|
291
- | `edit-rem` | str_replace 编辑 Rem JSON 字段 | 先 `read-rem` | 三道防线 | `edit-rem.md` |
293
+ | `edit-rem` | 直接修改 Rem 属性字段 | 先 `read-rem` | 两道防线 + 字段白名单 | `edit-rem.md` |
292
294
  | `edit-tree` | str_replace 编辑树结构 | 先 `read-tree` | 三道防线 + diff | `edit-tree.md` |
293
295
 
294
296
  ### 4.2 探索决策指南
@@ -309,10 +311,17 @@ Agent 需要根据用户意图选择正确的读取命令:
309
311
  ├─ 某个具体 Rem 的子树 → read-tree <remId>
310
312
  │ 完整展开子树(支持深度/节点预算控制)
311
313
  │ 结果缓存,供 edit-tree 使用
314
+ │ ⚠️ 如果需要读取子树后对其中多个节点执行 edit-rem,请改用 read-rem-in-tree
312
315
 
313
316
  ├─ 某个 Rem 的详细属性 → read-rem <remId>
314
317
  │ 返回 51 字段的 RemObject JSON
315
318
  │ 结果缓存,供 edit-rem 使用
319
+ │ ⚠️ 如果需要读取多个 Rem 属性(≥3 个)且在同一子树下,请改用 read-rem-in-tree
320
+
321
+ ├─ 子树结构 + 每个节点的详细属性 → read-rem-in-tree <remId>
322
+ │ read-tree + read-rem 的合体,一次调用同时获取大纲和 RemObject
323
+ │ 同时建立 tree 和 rem 双重缓存,供 edit-tree 和 edit-rem 使用
324
+ │ 默认 maxNodes=50(比 read-tree 的 200 低,因每节点开销大)
316
325
 
317
326
  └─ 按关键词搜索 → search <query>
318
327
  全文搜索,返回匹配的 Rem 列表
@@ -327,6 +336,7 @@ Agent 需要根据用户意图选择正确的读取命令:
327
336
  | "我现在在编辑什么" | `read-context --mode focus` | 鱼眼视图,焦点处详细 |
328
337
  | "当前页面的内容" | `read-context --mode page` | 以页面为根展开 |
329
338
  | "展开某个主题的细节" | `read-tree <id>` | 完整子树,可缓存供编辑 |
339
+ | "展开子树并查看每个节点属性" | `read-rem-in-tree <id>` | 大纲 + RemObject,双重缓存 |
330
340
 
331
341
  #### read-globe 特性
332
342
 
@@ -390,14 +400,28 @@ Agent 需要根据用户意图选择正确的读取命令:
390
400
  4. read-globe ← 了解知识库结构(首次探索)
391
401
  或 read-context ← 了解用户当前上下文
392
402
  5. search "关键词" ← 定位目标 Rem(中文搜索可能需单字策略,详见 search.md)
393
- 6. read-tree <id> 展开目标区域的子树
394
- 7. read-rem <id> 读取详细属性(编辑前必需)
395
- 8. edit-rem <id> ... ← 修改 Rem 属性
403
+ 6a. [单节点] read-tree <id> + read-rem <id> 各自建立缓存
404
+ 6b. [多节点] read-rem-in-tree <id> 一次建立双重缓存(推荐 ≥3 个节点需修改时)
405
+ 7. edit-rem <id> ... ← 修改 Rem 属性
396
406
  或 edit-tree <id> ...← 修改树结构
397
407
  9. disconnect ← 结束会话
398
408
  ```
399
409
 
400
- **注意**:步骤 7`edit-rem` 的强制前置条件,步骤 6 是 `edit-tree` 的强制前置条件。跳过会触发防线 1 错误。步骤 2 是必须的——connect 后不引导用户加载插件就直接调用业务命令,会报"Plugin 未连接"错误。
410
+ **注意**:步骤 6a/6b 是 edit-rem / edit-tree 的强制前置条件。跳过会触发防线 1 错误。步骤 2 是必须的——connect 后不引导用户加载插件就直接调用业务命令,会报"Plugin 未连接"错误。
411
+
412
+ ### 4.5 批量标注工作流(课本划重点场景)
413
+
414
+ 当需要对一棵子树中的多个节点进行富文本标注(行级高亮、行内荧光、粗体等)时:
415
+
416
+ 1. read-rem-in-tree <id> --maxNodes 50 -- 一次获取大纲 + 所有 RemObject
417
+ 2. 从 remObjects 中定位需要标注的节点
418
+ 3. 对每个目标节点 edit-rem 设置格式:
419
+ - 行级高亮:changes.highlightColor = "Yellow"/"Red"/...
420
+ - 行内荧光:changes.text = [..., {"h": 3, "i": "m", "text": "关键词"}, ...]
421
+ - 粗体:changes.text = [..., {"b": true, "i": "m", "text": "核心概念"}, ...]
422
+ 4. 如需结构变更(如新增/移动节点),直接 edit-tree(tree 缓存已就绪)
423
+
424
+ 关键:read-rem-in-tree 同时建立了 tree 和 rem 两种缓存,后续 edit-tree 和 edit-rem 都无需再单独 read。
401
425
 
402
426
  ---
403
427
 
@@ -410,7 +434,7 @@ Agent 需要根据用户意图选择正确的读取命令:
410
434
  ```bash
411
435
  read-rem kLrIOHJLyMd8Y2lyA --fields text,type
412
436
  read-tree kLrIOHJLyMd8Y2lyA --depth 2
413
- edit-rem kLrIOHJLyMd8Y2lyA --old-str '"concept"' --new-str '"descriptor"'
437
+ edit-rem kLrIOHJLyMd8Y2lyA --changes '{"type":"descriptor"}'
414
438
  ```
415
439
 
416
440
  - 位置参数 = remId 或关键词
@@ -422,7 +446,7 @@ edit-rem kLrIOHJLyMd8Y2lyA --old-str '"concept"' --new-str '"descriptor"'
422
446
  ```bash
423
447
  read-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","fields":["text","type"]}'
424
448
  read-tree --json '{"remId":"kLrIOHJLyMd8Y2lyA","depth":2}'
425
- edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","oldStr":"\"concept\"","newStr":"\"descriptor\""}'
449
+ edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","changes":{"type":"descriptor"}}'
426
450
  ```
427
451
 
428
452
  - **位置参数 = JSON 字符串**,所有参数打包在 JSON 对象中
@@ -465,6 +489,7 @@ daemon → CLI 响应:
465
489
  | `read_rem` | read-rem | readRem() | 直接转发 |
466
490
  | `edit_rem` | edit-rem | — | daemon handler 编排 |
467
491
  | `read_tree` | read-tree | readTree() | 直接转发 |
492
+ | `read_rem_in_tree` | read-rem-in-tree | readRemInTree() | daemon handler 编排 + Plugin 转发 |
468
493
  | `edit_tree` | edit-tree | — | daemon handler 编排 |
469
494
  | `read_globe` | read-globe | readGlobe() | 直接转发 |
470
495
  | `read_context` | read-context | readContext() | 直接转发 |
@@ -571,7 +596,7 @@ Portal:portalType [R], portalDirectlyIncludedRem [Portal-W]
571
596
  { "i": "a", "onlyAudio": true, "url": "..." } // 音频
572
597
  ```
573
598
 
574
- > 在 RemObject 格式化 JSON 中,数组内对象展开为多行。构造 edit-rem 的 oldStr/newStr 必须用多行格式。
599
+ > 在 RemObject 格式化 JSON 中,数组内对象展开为多行。构造 edit-tree 的 oldStr/newStr 或 edit-rem 的 changes 中 RichText 值时,需注意多行格式。
575
600
 
576
601
  **⚠️ highlightColor vs h — 两种完全不同的高亮**:
577
602
 
@@ -582,7 +607,7 @@ Portal:portalType [R], portalDirectlyIncludedRem [Portal-W]
582
607
 
583
608
  `h` 颜色值:0=无, 1=Red, 2=Orange, 3=Yellow, 4=Green, 5=Purple, 6=Blue, 7=Gray, 8=Brown, 9=Pink。
584
609
 
585
- **序列化确定性**:RichText 对象内部按 key 字母序排列(`sortRichTextKeys()`)。`_id` 的 `_`(U+005F)排在所有小写字母之前。这对 edit-rem 的 str_replace 和并发检测至关重要。
610
+ **序列化确定性**:RichText 对象内部按 key 字母序排列(`sortRichTextKeys()`)。`_id` 的 `_`(U+005F)排在所有小写字母之前。这对乐观并发检测和 edit-tree 的 str_replace 至关重要。
586
611
 
587
612
  ---
588
613
 
@@ -690,17 +715,17 @@ read-tree / read-globe / read-context 的输出核心是 Markdown 大纲文本
690
715
 
691
716
  | 前缀 | 用途 | 写入命令 |
692
717
  |:-----|:-----|:---------|
693
- | `rem:{remId}` | RemObject JSON | read-rem |
694
- | `tree:{remId}` | Markdown 大纲 | read-tree |
695
- | `tree-depth:{remId}` 等 | read-tree 参数 | read-tree |
718
+ | `rem:{remId}` | RemObject 对象 | read-rem, read-rem-in-tree |
719
+ | `tree:{remId}` | Markdown 大纲 | read-tree, read-rem-in-tree |
720
+ | `tree-depth:{remId}` 等 | read-tree 参数 | read-tree, read-rem-in-tree |
696
721
 
697
722
  - LRU 淘汰:上限 200 条目
698
723
  - disconnect 关闭 daemon 时缓存自动消失
699
724
  - **没有 TTL**——三道防线的并发检测已能捕获所有陈旧数据
700
725
 
701
- ### 9.2 三道防线
726
+ ### 9.2 安全防线
702
727
 
703
- `edit-rem` 和 `edit-tree` 使用 str_replace 语义编辑数据。为防止数据损坏,实施三道防线:
728
+ `edit-rem` 和 `edit-tree` 编辑数据时,实施多道防线防止数据损坏:
704
729
 
705
730
  #### 防线 1:缓存存在性检查
706
731
 
@@ -718,7 +743,16 @@ edit 时重新从 SDK 读取最新数据 → 与缓存严格比较
718
743
 
719
744
  如果 Rem 在 read 之后被外部修改(用户在 RemNote UI 中编辑、其他 Agent 修改等),数据不一致时拒绝编辑,**且不更新缓存**——迫使 Agent 重新 read。
720
745
 
721
- #### 防线 3:str_replace 精确匹配
746
+ #### edit-rem 防线 3:字段白名单校验
747
+
748
+ ```
749
+ changes 中的字段必须在 RW 白名单内,枚举值必须合法
750
+ ```
751
+
752
+ - 只读字段(R / R-F)写入时**警告跳过**,不阻断其他字段
753
+ - 枚举值非法时**报错拒绝**(如 `type: "invalid"`)
754
+
755
+ #### edit-tree 防线 3:str_replace 精确匹配
722
756
 
723
757
  ```
724
758
  oldStr 必须在目标文本中恰好匹配 1 次
@@ -733,7 +767,8 @@ oldStr 必须在目标文本中恰好匹配 1 次
733
767
  | 场景 | 缓存行为 |
734
768
  |:-----|:---------|
735
769
  | 写入全部成功 | 从 SDK 重新读取最新状态 → **更新缓存** |
736
- | 防线拒绝 | **不更新缓存**(迫使 Agent 重新 read) |
770
+ | 防线拒绝(缓存缺失 / 并发冲突) | **不更新缓存**(迫使 Agent 重新 read) |
771
+ | 枚举值非法 | **报错拒绝**,不更新缓存 |
737
772
  | 部分写入失败 | **不更新缓存** |
738
773
 
739
774
  ---
@@ -854,13 +889,12 @@ Agent 遇到错误时的诊断和恢复指南:
854
889
  | has not been read yet | 未先执行 read-rem / read-tree | 执行对应 read 命令后重试 |
855
890
  | has been modified since last read | Rem 在 read 和 edit 之间被外部修改 | 重新执行 read 获取最新状态后重试 |
856
891
 
857
- ### str_replace 错误
892
+ ### edit-tree str_replace 错误
858
893
 
859
894
  | 错误 | 原因 | 恢复 |
860
895
  |:-----|:-----|:-----|
861
- | old_str not found | oldStr 在目标文本中不存在 | 检查 oldStr 是否精确匹配(含引号、空格、换行) |
896
+ | old_str not found | oldStr 在目标文本中不存在 | 检查 oldStr 是否精确匹配(含空格、换行、缩进) |
862
897
  | old_str matches N locations | oldStr 匹配到多个位置 | 扩大 oldStr 范围,包含更多上下文以唯一定位 |
863
- | invalid JSON | 替换后的文本不是合法 JSON | 检查 newStr 的引号、逗号、括号完整性 |
864
898
 
865
899
  ### edit-tree 专用错误
866
900
 
@@ -0,0 +1,100 @@
1
+ # read-rem-in-tree — 子树大纲 + 节点属性一次获取
2
+
3
+ > `read_tree` + `read_rem` 的完美结合体。一次调用同时获取 Markdown 大纲和每个节点的完整 RemObject JSON,建立双重缓存。
4
+
5
+ ---
6
+
7
+ ## 适用场景
8
+
9
+ - 需要同时查看子树结构和节点详细属性(如批量编辑前的全量读取)
10
+ - 一次调用建立 tree 缓存(供 `edit_tree`)和 rem 缓存(供 `edit_rem`)
11
+ - 替代连续调用 `read_tree` + 多次 `read_rem` 的场景
12
+
13
+ ## 不适用场景
14
+
15
+ - 只需大纲不需属性 → 用 `read_tree`(更轻量)
16
+ - 只需单个 Rem 属性 → 用 `read_rem`
17
+ - 大规模子树(>50 节点) → 每节点 40+ SDK 调用,性能开销大
18
+
19
+ ---
20
+
21
+ ## 命令格式
22
+
23
+ ```bash
24
+ # 人类模式
25
+ remnote-bridge read-rem-in-tree <remId> [options]
26
+
27
+ # JSON 模式
28
+ remnote-bridge --json read-rem-in-tree '{"remId":"...","depth":3,"maxNodes":50}'
29
+ ```
30
+
31
+ ## 参数
32
+
33
+ | 参数 | 类型 | 默认值 | 说明 |
34
+ |:-----|:-----|:-------|:-----|
35
+ | `remId` | string | **必需** | 子树根节点的 Rem ID |
36
+ | `depth` | number | 3 | 递归展开深度(-1 = 无限) |
37
+ | `maxNodes` | number | **50** | 全局节点总预算(注意比 read_tree 的 200 低) |
38
+ | `maxSiblings` | number | 20 | 单个父节点下最大可见子节点数 |
39
+ | `ancestorLevels` | number | 0 | 向上追溯祖先层数(上限 10) |
40
+ | `fields` | string[] | - | RemObject 字段过滤(只返回指定子集) |
41
+ | `full` | boolean | false | 返回全部 51 个 RemObject 字段 |
42
+ | `includePowerup` | boolean | false | 包含 Powerup 系统数据 |
43
+
44
+ ## 输出
45
+
46
+ ### JSON 模式
47
+
48
+ ```jsonc
49
+ {
50
+ "ok": true,
51
+ "command": "read-rem-in-tree",
52
+ "data": {
53
+ "rootId": "kLr...",
54
+ "depth": 3,
55
+ "nodeCount": 15,
56
+ "outline": "# 数据结构 <!--kLr type:concept doc-->\n ...",
57
+ "remObjects": {
58
+ "kLr": { "id": "kLr", "text": [...], "type": "concept", ... },
59
+ "ABC": { "id": "ABC", "text": [...], ... }
60
+ }
61
+ },
62
+ "ancestors": [...], // 可选
63
+ "cacheOverridden": {...}, // 可选
64
+ "powerupFiltered": {...} // 可选
65
+ }
66
+ ```
67
+
68
+ ### 核心字段
69
+
70
+ - `outline`:Markdown 大纲文本,与 `read_tree` 输出格式完全一致
71
+ - `remObjects`:扁平 map `{ remId → RemObject }`,每个 RemObject 与 `read_rem` 输出一致
72
+ - 默认启用 Token Slimming(省略默认值字段)
73
+ - `fields` / `full` 参数控制过滤行为
74
+
75
+ ## 缓存行为
76
+
77
+ | 缓存 Key | 内容 | 用途 |
78
+ |:---------|:-----|:-----|
79
+ | `tree:{remId}` | outline 大纲文本 | `edit_tree` 前置缓存 |
80
+ | `tree-depth:{remId}` 等 | 读取参数 | `edit_tree` 乐观并发检测 |
81
+ | `rem:{nodeRemId}` | 完整 RemObject(N 个) | `edit_rem` 前置缓存 |
82
+
83
+ 总缓存条目:1(tree) + 3(参数) + N(rem) ≈ N+4。注意 LRU 上限 200。
84
+
85
+ ## 典型工作流
86
+
87
+ ```
88
+ read_rem_in_tree → 一次获取全部信息
89
+
90
+ edit_tree 结构编辑(tree 缓存已就绪)
91
+ +
92
+ edit_rem 属性编辑(rem 缓存已就绪)
93
+ ```
94
+
95
+ ## 关联工具
96
+
97
+ - `read_tree`:只需大纲,更轻量
98
+ - `read_rem`:只需单个 Rem 属性
99
+ - `edit_tree`:结构编辑(需先 read_tree 或 read_rem_in_tree)
100
+ - `edit_rem`:属性编辑(需先 read_rem 或 read_rem_in_tree)