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
@@ -11,6 +11,7 @@ import type { ReactRNPlugin } from '@remnote/plugin-sdk';
11
11
  import type { BridgeRequest } from './websocket-client';
12
12
  import { readRem } from '../services/read-rem';
13
13
  import { readTree } from '../services/read-tree';
14
+ import { readRemInTree } from '../services/read-rem-in-tree';
14
15
  import { readGlobe } from '../services/read-globe';
15
16
  import { readContext } from '../services/read-context';
16
17
  import { writeRemFields } from '../services/write-rem-fields';
@@ -36,6 +37,8 @@ export function createMessageRouter(plugin: ReactRNPlugin): (request: BridgeRequ
36
37
  return readRem(plugin, request.payload as { remId: string; includePowerup?: boolean });
37
38
  case 'read_tree':
38
39
  return readTree(plugin, request.payload as { remId: string; depth?: number; maxNodes?: number; maxSiblings?: number; ancestorLevels?: number; includePowerup?: boolean });
40
+ case 'read_rem_in_tree':
41
+ return readRemInTree(plugin, request.payload as { remId: string; depth?: number; maxNodes?: number; maxSiblings?: number; ancestorLevels?: number; includePowerup?: boolean });
39
42
  case 'write_rem_fields':
40
43
  return writeRemFields(plugin, request.payload as { remId: string; changes: Record<string, unknown> });
41
44
  case 'create_rem':
@@ -0,0 +1,43 @@
1
+ /**
2
+ * read-rem-in-tree service — read_tree + 对每个节点 buildRemObject
3
+ *
4
+ * 同态命名:read_rem_in_tree (action) → read-rem-in-tree.ts (文件) → readRemInTree (函数)
5
+ */
6
+
7
+ import type { ReactRNPlugin } from '@remnote/plugin-sdk';
8
+ import type { RemObject } from '../types';
9
+ import { readTree, type ReadTreePayload, type ReadTreeResult } from './read-tree';
10
+ import { buildRemObject } from './read-rem';
11
+
12
+ export interface ReadRemInTreePayload extends ReadTreePayload {}
13
+
14
+ export interface ReadRemInTreeResult extends ReadTreeResult {
15
+ /** remId → RemObject 的扁平映射 */
16
+ remObjects: Record<string, RemObject & { powerupFiltered?: { tags: number; children: number } }>;
17
+ }
18
+
19
+ /**
20
+ * 读取 Rem 子树大纲 + 对树中每个节点构建完整 RemObject。
21
+ *
22
+ * @throws Error — Rem 不存在、节点数超限
23
+ */
24
+ export async function readRemInTree(
25
+ plugin: ReactRNPlugin,
26
+ payload: ReadRemInTreePayload,
27
+ ): Promise<ReadRemInTreeResult> {
28
+ // 1. 获取 outline + nodeRemIds(复用 readTree)
29
+ const treeResult = await readTree(plugin, payload);
30
+
31
+ // 2. 用 readTree 遍历时收集的 nodeRemIds,直接并行 buildRemObject
32
+ const includePowerup = payload.includePowerup ?? false;
33
+ const remObjects: ReadRemInTreeResult['remObjects'] = {};
34
+
35
+ await Promise.all(treeResult.nodeRemIds.map(async (id) => {
36
+ const rem = await plugin.rem.findOne(id);
37
+ if (rem) {
38
+ remObjects[id] = await buildRemObject(plugin, rem, { includePowerup });
39
+ }
40
+ }));
41
+
42
+ return { ...treeResult, remObjects };
43
+ }
@@ -33,6 +33,21 @@ export async function readRem(
33
33
  throw new Error(`Rem not found: ${payload.remId}`);
34
34
  }
35
35
 
36
+ return buildRemObject(plugin, rem, { includePowerup });
37
+ }
38
+
39
+ /**
40
+ * 从已有 Rem 对象组装完整 RemObject(跳过 findOne)。
41
+ *
42
+ * 供 readRem 和 readRemInTree 共享。
43
+ */
44
+ export async function buildRemObject(
45
+ plugin: ReactRNPlugin,
46
+ rem: Rem,
47
+ options: { includePowerup?: boolean },
48
+ ): Promise<RemObject & { powerupFiltered?: { tags: number; children: number } }> {
49
+ const { includePowerup = false } = options;
50
+
36
51
  // 并行获取所有异步字段
37
52
  const [
38
53
  isDocument,
@@ -43,6 +43,8 @@ export interface ReadTreeResult {
43
43
  depth: number;
44
44
  nodeCount: number;
45
45
  outline: string;
46
+ /** 树中所有节点的 remId 列表(遍历顺序) */
47
+ nodeRemIds: string[];
46
48
  /** 祖先链(从直接父亲到最远祖先,由近及远) */
47
49
  ancestors?: AncestorInfo[];
48
50
  powerupFiltered?: { tags: number; children: number };
@@ -75,6 +77,7 @@ export async function readTree(
75
77
  let totalFilteredTags = 0;
76
78
  let totalFilteredChildren = 0;
77
79
  const budget = { remaining: maxNodes };
80
+ const nodeRemIds: string[] = [];
78
81
 
79
82
  /**
80
83
  * 递归构建 OutlineNode 树。
@@ -86,6 +89,7 @@ export async function readTree(
86
89
  async function buildNode(rem: Rem, currentDepth: number, maxDepth: number): Promise<OutlineNode> {
87
90
  nodeCount++;
88
91
  budget.remaining--;
92
+ nodeRemIds.push(rem._id);
89
93
 
90
94
  const allChildren = await rem.getChildrenRem();
91
95
  const children = includePowerup ? allChildren : await filterNoisyChildren(allChildren);
@@ -179,6 +183,7 @@ export async function readTree(
179
183
  depth,
180
184
  nodeCount,
181
185
  outline,
186
+ nodeRemIds,
182
187
  };
183
188
 
184
189
  // 祖先链构建
@@ -18,6 +18,7 @@ description: "RemNote 知识库操作指南。通过 remnote-bridge 命令行工
18
18
  | read-rem | `instructions/read-rem.md` |
19
19
  | edit-rem | `instructions/edit-rem.md` |
20
20
  | read-tree | `instructions/read-tree.md` |
21
+ | read-rem-in-tree | `instructions/read-rem-in-tree.md` |
21
22
  | edit-tree | `instructions/edit-tree.md` |
22
23
  | read-globe | `instructions/read-globe.md` |
23
24
  | read-context | `instructions/read-context.md` |
@@ -92,7 +93,7 @@ RemNote 推荐的知识结构化方法:
92
93
  | **Tag** | `##` | Rem 的 tags 数组 | read-rem 的 `tags` 字段 |
93
94
  | **Portal** | `((` | 嵌入实时视图(**编辑同步**) | read-tree 标记 `type:portal refs:id1,id2` |
94
95
 
95
- Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的 Rem 不会被 read-tree 自动展开,需要对 refs 中的 ID 单独 read-tree。Portal 引用列表可通过 `edit-rem` 修改(str_replace 简化 JSON 中的 `portalDirectlyIncludedRem` 数组)。
96
+ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的 Rem 不会被 read-tree 自动展开,需要对 refs 中的 ID 单独 read-tree。Portal 引用列表可通过 `edit-rem` 修改(直接修改 changes 中的 `portalDirectlyIncludedRem` 数组)。
96
97
 
97
98
  #### Portal 操作速查
98
99
 
@@ -100,7 +101,7 @@ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的
100
101
  |:-----|:-----|:-----|
101
102
  | 创建 Portal | `edit-tree` | 新增行 `<!--portal refs:id1,id2-->` |
102
103
  | 删除 Portal | `edit-tree` | 从大纲中移除 Portal 行(与删除普通行相同) |
103
- | 修改引用列表(增删引用的 Rem) | `edit-rem` | str_replace 简化 JSON 中的 `portalDirectlyIncludedRem` 数组 |
104
+ | 修改引用列表(增删引用的 Rem) | `edit-rem` | 直接修改 changes 中的 `portalDirectlyIncludedRem` 数组 |
104
105
  | 移动 Portal(换父节点/位置) | `edit-tree` | 与移动普通行相同 |
105
106
  | 读取 Portal | `read-rem` | 自动输出 8 字段简化 JSON |
106
107
 
@@ -184,7 +185,7 @@ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的
184
185
  { "i": "a", "onlyAudio": true, "url": "https://example.com/audio.mp3" }
185
186
  ```
186
187
 
187
- > **注意**:在 RemObject 的格式化 JSON 中,数组内的对象会展开为多行(每个 key 一行,缩进 4+2 空格)。以上为简写——构造 edit-rem 的 oldStr/newStr 时必须使用实际的多行格式。
188
+ > **注意**:在 RemObject 的格式化 JSON 中,数组内的对象会展开为多行(每个 key 一行,缩进 4+2 空格)。以上为简写——构造 edit-rem 的 changes 中 RichText 值时,直接传入数组即可。
188
189
 
189
190
  #### highlightColor(Rem 级别)vs h(RichText 行内)
190
191
 
@@ -195,7 +196,7 @@ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的
195
196
 
196
197
  #### 序列化确定性
197
198
 
198
- RichText 对象内部按 **key 字母序排列**(`sortRichTextKeys()`),确保 JSON 始终一致。`_id` 中的 `_`(U+005F)排在所有小写字母(`a`=U+0061)之前。构造 edit-rem 的 oldStr 时必须保持相同的 key 顺序,否则匹配失败。
199
+ RichText 对象内部按 **key 字母序排列**(`sortRichTextKeys()`),确保 JSON 始终一致。`_id` 中的 `_`(U+005F)排在所有小写字母(`a`=U+0061)之前。防线 2(乐观并发检测)依赖此确定性序列化来比较缓存与最新数据。
199
200
 
200
201
  ### Powerup 机制与噪音过滤
201
202
 
@@ -222,6 +223,7 @@ RemNote 格式设置(fontSize、highlightColor 等)底层通过 Powerup 机
222
223
  用户当前在看什么("当前页面") → read-context
223
224
  某个 Rem 的子树("展开这个主题") → read-tree <remId>
224
225
  某个 Rem 的详细属性("详细信息") → read-rem <remId>
226
+ 子树 + 每个节点属性("批量标注") → read-rem-in-tree <remId>
225
227
  按关键词搜索("搜索 X") → search <query>(中文搜索有限制,见下方说明)
226
228
  ```
227
229
 
@@ -233,8 +235,9 @@ RemNote 格式设置(fontSize、highlightColor 等)底层通过 Powerup 机
233
235
  | 我在编辑什么 | `read-context --mode focus` | 鱼眼视图(焦点 depth=3,siblings depth=1,叔伯 depth=0),**无缓存** |
234
236
  | 当前页面内容 | `read-context --mode page` | 均匀展开,**无缓存** |
235
237
  | 展开某主题细节 | `read-tree <id>` | 完整子树,**有缓存**供 edit-tree |
238
+ | 展开子树 + 每个节点属性 | `read-rem-in-tree <id>` | 大纲 + RemObject,**双重缓存**供 edit-tree 和 edit-rem |
236
239
 
237
- **重要**:read-globe、read-context、search 都**不写入缓存**,不能替代 read-tree/read-rem 作为 edit 的前置条件。read-context 需要用户在 RemNote 中有焦点(focus 模式)或打开页面(page 模式)。
240
+ **重要**:read-globe、read-context、search 都**不写入缓存**,不能替代 read-tree/read-rem/read-rem-in-tree 作为 edit 的前置条件。read-context 需要用户在 RemNote 中有焦点(focus 模式)或打开页面(page 模式)。
238
241
 
239
242
  ### 修改:用户想改什么?
240
243
 
@@ -246,6 +249,36 @@ Rem 的属性(文本、类型、格式、标签) → edit-rem (前置
246
249
 
247
250
  **关键区分**:`edit-rem` 修改 Rem 的**属性**,`edit-tree` 修改 Rem 之间的**结构关系**。`edit-tree` **禁止修改行内容**。
248
251
 
252
+ ### read-rem-in-tree:何时用、怎么用
253
+
254
+ `read-rem-in-tree` = `read-tree` + 批量 `read-rem` 的合体。一次调用同时获取子树大纲和每个节点的 RemObject(含完整 RichText),同时建立 `tree:` 和 `rem:` 双重缓存。
255
+
256
+ **何时用**:当你需要对一棵子树中的**多个节点**做属性修改(edit-rem)时。典型场景:
257
+
258
+ - 批量标注重点(给多个节点加高亮/粗体)
259
+ - 批量修改类型(把多个普通 Rem 改为 concept)
260
+ - 读取子树后需要检查多个节点的 RichText 再决定怎么改
261
+
262
+ **何时不用**:只需要看大纲结构(用 `read-tree`),或只改 1-2 个节点属性(用 `read-tree` + `read-rem`)。
263
+
264
+ **用法示例**(课本划重点场景):
265
+
266
+ ```bash
267
+ # 1. 一次获取大纲 + 所有节点属性(默认 maxNodes=50)
268
+ read-rem-in-tree --json '{"remId":"kLr...","depth":3,"maxNodes":30}'
269
+
270
+ # 返回:outline(大纲文本)+ remObjects(每个节点的 RemObject)
271
+
272
+ # 2. 从 remObjects 中找到要标注的节点,直接 edit-rem(rem 缓存已就绪)
273
+ edit-rem --json '{"remId":"ABC","changes":{"highlightColor":"Yellow"}}'
274
+ edit-rem --json '{"remId":"DEF","changes":{"text":["关键词",{"b":true,"h":1,"i":"m","text":"重点"}]}}'
275
+
276
+ # 3. 如需结构变更,直接 edit-tree(tree 缓存已就绪)
277
+ edit-tree --json '{"remId":"kLr...","oldStr":"...","newStr":"..."}'
278
+ ```
279
+
280
+ **对比效率**:修改 10 个节点时,`read-rem-in-tree` = 1 次调用;`read-tree` + 10×`read-rem` = 11 次调用。
281
+
249
282
  ---
250
283
 
251
284
  ## 3. 标准工作流
@@ -274,20 +307,15 @@ Rem 的属性(文本、类型、格式、标签) → edit-rem (前置
274
307
 
275
308
  #### 首次使用(setup)
276
309
 
277
- setup 会弹出 Chrome 窗口,用户需要完成两件事:
278
- 1. **登录 RemNote**
279
- 2. **配置 dev plugin**:插件图标 → 开发你的插件 → 填入 connect 输出的 Plugin 服务地址(如 `http://localhost:29101`)
310
+ setup 会弹出 Chrome 窗口,用户只需 **登录 RemNote**,然后**彻底退出 Chrome**(macOS 必须 Cmd+Q,仅关窗口不够)。
280
311
 
281
- 完成后**彻底退出 Chrome**(macOS 必须 Cmd+Q,仅关窗口不够)。
312
+ setup 只负责保存登录凭证——配置 dev plugin 是 connect 之后的事(见标准模式说明)。
282
313
 
283
314
  **Agent 交互方式**:
284
315
  ```
285
316
  1. 调用 setup
286
317
  2. 立即告知用户:
287
- "已打开 Chrome 浏览器。请完成以下操作:
288
- 1. 登录 RemNote
289
- 2. 在 RemNote 中配置开发插件:点击左下角插件图标 → 开发你的插件 → 输入 connect 输出的 Plugin 服务地址
290
- 3. 完成后彻底退出 Chrome(macOS 请按 Cmd+Q)"
318
+ "已打开 Chrome 浏览器。请登录 RemNote,完成后彻底退出 Chrome(macOS 请按 Cmd+Q)"
291
319
  3. 等待 setup 返回(阻塞,最长 10 分钟)
292
320
  4. 成功 → 进入下一步 connect --headless
293
321
  ```
@@ -320,9 +348,9 @@ setup 只需执行一次。之后每次连接直接用 `connect --headless`。
320
348
  4. read-globe -- 了解知识库结构(首次探索)
321
349
  或 read-context -- 了解用户当前上下文
322
350
  5. search "关键词" -- 定位目标 Rem(结果不进缓存!)
323
- 6. read-tree <id> -- 展开子树 → 写入缓存(edit-tree 的前置)
324
- 7. read-rem <id> -- 读取属性 写入缓存(edit-rem 的前置)
325
- 8. edit-rem / edit-tree -- 执行修改
351
+ 6a. [单节点] read-tree <id> + read-rem <id> -- 各自建立缓存
352
+ 6b. [多节点] read-rem-in-tree <id> -- 一次建立双重缓存(推荐 ≥3 个节点需修改时)
353
+ 7. edit-rem / edit-tree -- 执行修改
326
354
  9. disconnect -- 结束会话(缓存全部清空,幂等)
327
355
  ```
328
356
 
@@ -337,14 +365,16 @@ Agent 应始终使用 JSON 模式调用命令。
337
365
  ```bash
338
366
  # 正确:位置参数 = JSON 字符串
339
367
  remnote-bridge read-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","fields":["text","type"]}'
340
- remnote-bridge search --json '{"query":"机器学习","numResults":10}'
368
+ remnote-bridge search --json '{"query":"机器学习","limit":10}'
369
+ remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","changes":{"type":"concept"}}'
370
+
371
+ # 人类模式:
372
+ remnote-bridge edit-rem kLrIOHJLyMd8Y2lyA --changes '{"type":"concept"}'
341
373
 
342
374
  # 错误:禁止混用裸 remId + --json
343
375
  remnote-bridge read-rem kLrIOHJLyMd8Y2lyA --json # 会失败!
344
376
  ```
345
377
 
346
- 注意:search 的 JSON 参数名是 `numResults`(不是 `limit`)。
347
-
348
378
  ### 中文搜索限制(重要)
349
379
 
350
380
  search 调用 RemNote SDK 官方搜索方法,其分词基于空格分割。**中文、日文、韩文等无空格分词的语言搜索效果差**——SDK 将多字词拆为单字符 token 匹配,导致返回 0 结果或不相关结果。RemNote 本地桌面版已优化此问题,Web 版未优化。
@@ -364,7 +394,7 @@ search 调用 RemNote SDK 官方搜索方法,其分词基于空格分割。**
364
394
 
365
395
  ---
366
396
 
367
- ## 5. 安全机制:三道防线
397
+ ## 5. 安全机制
368
398
 
369
399
  ### 防线 1:缓存存在性
370
400
 
@@ -374,9 +404,11 @@ search 调用 RemNote SDK 官方搜索方法,其分词基于空格分割。**
374
404
 
375
405
  edit 时从 SDK 重新读取最新数据,与缓存严格比较。被外部修改则拒绝编辑且**不更新缓存**——迫使 Agent 重新 read。
376
406
 
377
- ### 防线 3:str_replace 精确匹配
407
+ ### 防线 3:str_replace 精确匹配(仅 edit-tree)
378
408
 
379
- `oldStr` 必须在目标文本中恰好匹配 1 次。
409
+ `oldStr` 必须在大纲文本中恰好匹配 1 次。**仅适用于 edit-tree**。
410
+
411
+ edit-rem 使用字段直接修改(changes 对象),不经过 str_replace,改为**字段白名单校验**(非法字段产生警告,枚举值非法则拒绝)。
380
412
 
381
413
  ### 缓存更新规则
382
414
 
@@ -384,7 +416,8 @@ edit 时从 SDK 重新读取最新数据,与缓存严格比较。被外部修
384
416
  |:-----|:---------|:-----------|
385
417
  | 写入成功 | 从 SDK 重新读取 → 更新缓存 | 可继续编辑 |
386
418
  | 防线 2 拒绝 / 部分写入失败 | **不更新缓存** | 必须重新 read |
387
- | 防线 3 拒绝 / JSON 语法错误 | 缓存保持不变 | 调整 oldStr/newStr 后**直接重试** |
419
+ | 枚举值非法(edit-rem) | 缓存保持不变 | 检查允许的值范围后重试 |
420
+ | 防线 3 拒绝 / JSON 语法错误(edit-tree) | 缓存保持不变 | 调整 oldStr/newStr 后**直接重试** |
388
421
  | 操作执行中异常(edit-tree) | 已执行的操作保留(**无回滚**),不更新缓存 | 必须重新 read-tree |
389
422
 
390
423
  ---
@@ -542,20 +575,21 @@ SDK bug 自动修复:移动行进入多行闪卡父节点时自动设 `isCardI
542
575
 
543
576
  ---
544
577
 
545
- ## 8. edit-rem str_replace 要点
578
+ ## 8. edit-rem 字段修改要点
546
579
 
547
- 操作对象是 `JSON.stringify(remObject, null, 2)` 的文本(缩进 2 空格的 JSON)。
580
+ edit-rem 通过 `changes` 对象直接指定要修改的字段及其目标值,无需 str_replace。
548
581
 
549
- ### 使用技巧
582
+ ### 使用方式
550
583
 
551
- 1. **包含字段名**避免模糊匹配:`"\"type\": \"concept\""` 而非 `"concept"`
552
- 2. 替换后必须是合法 JSON
553
- 3. 修改 RichText 时注意 key 字母序:
584
+ ```bash
585
+ # JSON 模式
586
+ edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","changes":{"type":"concept","text":["新标题"]}}'
554
587
 
588
+ # 人类模式
589
+ edit-rem kLrIOHJLyMd8Y2lyA --changes '{"type":"concept","text":["新标题"]}'
555
590
  ```
556
- oldStr: "\"text\": [\n \"Hello\"\n ]"
557
- newStr: "\"text\": [\n \"World\"\n ]"
558
- ```
591
+
592
+ `changes` 对象中只需包含要修改的字段,未提及的字段保持不变。
559
593
 
560
594
  ### 21 个可编辑字段
561
595
 
@@ -576,9 +610,9 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
576
610
  | `fontSize` | `null` → 调用 `setFontSize(undefined)`(恢复普通大小) |
577
611
  | `todoStatus` | 依赖 `isTodo=true` 才生效;`null` 被跳过(清除 todo 应设 `isTodo=false`) |
578
612
  | `type` | 不可设为 `portal`(只能通过 SDK `createPortal()` 创建) |
579
- | `parent` + `positionAmongstSiblings` | 共享同一 SDK 调用 `setParent(parentId, position)`,**应在同一次 str_replace 中同时修改** |
613
+ | `parent` + `positionAmongstSiblings` | 共享同一 SDK 调用 `setParent(parentId, position)`,**应在同一次 changes 中同时修改** |
580
614
  | `tags` / `sources` | **Diff 机制**:对比当前 vs 目标数组,逐项 add/remove。必须列出完整目标数组,缺少的会被删除 |
581
- | `portalDirectlyIncludedRem` | Portal 专用可写。**Diff 机制**:对比当前 vs 目标数组,逐项 addToPortal/removeFromPortal。仅 type=portal 时可修改。edit-rem 对 Portal 使用 8 字段简化 JSON |
615
+ | `portalDirectlyIncludedRem` | Portal 专用可写。**Diff 机制**:对比当前 vs 目标数组,逐项 addToPortal/removeFromPortal。仅 type=portal 时可修改 |
582
616
 
583
617
  ### 常用只读字段(修改只产生警告,不生效)
584
618
 
@@ -608,16 +642,15 @@ aliases, descendants, siblingRem, isTable, portalType, propertyType
608
642
  | Plugin 未连接 | RemNote 未打开 | 打开 RemNote(health 三层:daemon→Plugin→SDK 链式依赖) |
609
643
  | has not been read yet | 未先 read | 执行对应 read 命令(search 结果不算 read!) |
610
644
  | has been modified since last read | 被外部修改 | 重新 read(必须,不可直接重试) |
611
- | old_str not found | oldStr 不精确 | 检查引号、空格、换行、RichText key 顺序 |
612
- | old_str matches N locations | oldStr 不够具体 | 扩大 oldStr 范围,包含更多上下文 |
613
- | invalid JSON | newStr 破坏了 JSON 结构 | 检查引号、逗号、括号完整性(可直接重试) |
645
+ | Invalid value for 'field' | 枚举字段值不合法(edit-rem) | 检查该字段允许的值范围 |
646
+ | old_str not found | oldStr 不精确(edit-tree) | 检查引号、空格、换行 |
647
+ | old_str matches N locations | oldStr 不够具体(edit-tree) | 扩大 oldStr 范围,包含更多上下文 |
614
648
  | content_modified | edit-tree 中改了行内容 | 用 edit-rem |
615
649
  | orphan_detected | 删了父但留了子 | 同时删除所有子行 |
616
650
  | folded_delete | 删了有隐藏子节点的行 | 用更大 depth 重新 read-tree |
617
651
  | elided_modified | 删/改了省略占位符 | 用更大 depth/maxSiblings 重新 read-tree |
618
652
  | indent_skip | 缩进跳级(如 0→4 空格) | 每级 2 空格,不可跳级 |
619
653
  | children_captured | 新行劫持已有子节点 | 把新行插到兄弟末尾 |
620
- | old_str not found in simplified Portal JSON | Portal 编辑时 oldStr 不匹配简化 JSON | 检查 Portal 简化 JSON 格式 |
621
654
  | Rem not found | remId 无效或已删除 | 用 search 重新定位 |
622
655
 
623
656
  ---
@@ -43,7 +43,9 @@ remnote-bridge --json connect --instance work
43
43
  | 2 | 29120 | 29121 | 29122 |
44
44
  | 3 | 29130 | 29131 | 29132 |
45
45
 
46
- **实例名解析优先级**:CLI `--instance` 参数 > 环境变量 `REMNOTE_BRIDGE_INSTANCE` > 默认值 `default`。Headless 模式下固定为 `headless`。
46
+ **实例名解析优先级**:CLI `--instance` 参数 > 环境变量 `REMNOTE_BRIDGE_INSTANCE` > 默认值 `default`。
47
+
48
+ **⚠️ `headless` 是保留实例名**:`--instance headless` 会直接报错。headless 模式必须使用专用的 `--headless` 全局选项(见下方 Headless 模式章节)。
47
49
 
48
50
  **首次使用多实例时**,用户需在 RemNote 中为每个实例分别配置 dev plugin URL(对应各自的 Plugin 服务端口)。
49
51
 
@@ -74,6 +76,15 @@ remnote-bridge --json connect --headless
74
76
 
75
77
  Headless 模式下 Plugin 可能需要 10-30 秒才能连接到 daemon,使用 `health` 确认就绪。
76
78
 
79
+ `--headless` 是全局选项,headless 会话中**所有命令都需要带上**:
80
+
81
+ ```bash
82
+ remnote-bridge --headless connect # 启动
83
+ remnote-bridge --headless health # 检查
84
+ remnote-bridge --headless read-rem --json '{"remId":"..."}' # 业务命令
85
+ remnote-bridge --headless disconnect # 结束
86
+ ```
87
+
77
88
  排查工具:`health --diagnose`(截图+状态+console 错误)、`health --reload`(重载 Chrome 页面)。
78
89
 
79
90
  ---
@@ -18,12 +18,17 @@
18
18
 
19
19
  通过 `--instance <name>` 指定要停止的实例。不指定时停止 `default` 实例。
20
20
 
21
+ `--headless` 是全局选项,用于停止 headless 模式启动的实例。
22
+
21
23
  ```bash
22
24
  # 停止默认实例
23
25
  remnote-bridge disconnect
24
26
 
25
27
  # 停止指定实例
26
28
  remnote-bridge disconnect --instance work
29
+
30
+ # 停止 headless 实例
31
+ remnote-bridge --headless disconnect
27
32
  ```
28
33
 
29
34
  ---