remnote-bridge 0.1.13 → 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 (37) hide show
  1. package/README.md +141 -28
  2. package/README.zh-CN.md +368 -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/patch-engine.js +347 -0
  8. package/dist/cli/handlers/read-handler.js +2 -53
  9. package/dist/cli/handlers/rem-field-filter.js +102 -0
  10. package/dist/cli/handlers/tree-edit-handler.js +67 -7
  11. package/dist/cli/handlers/tree-read-handler.js +4 -1
  12. package/dist/cli/handlers/tree-rem-read-handler.js +73 -0
  13. package/dist/cli/main.js +53 -2
  14. package/dist/cli/server/ws-server.js +9 -1
  15. package/dist/mcp/daemon-client.js +22 -2
  16. package/dist/mcp/instructions.js +54 -7
  17. package/dist/mcp/tools/edit-tools.js +7 -2
  18. package/dist/mcp/tools/infra-tools.js +20 -11
  19. package/dist/mcp/tools/read-tools.js +88 -2
  20. package/package.json +1 -1
  21. package/remnote-plugin/dist/index-sandbox.js +24 -24
  22. package/remnote-plugin/dist/index.js +24 -24
  23. package/remnote-plugin/src/bridge/message-router.ts +3 -0
  24. package/remnote-plugin/src/services/read-rem-in-tree.ts +43 -0
  25. package/remnote-plugin/src/services/read-rem.ts +15 -0
  26. package/remnote-plugin/src/services/read-tree.ts +5 -0
  27. package/skills/remnote-bridge/SKILL.md +37 -4
  28. package/skills/remnote-bridge/instructions/connect.md +12 -1
  29. package/skills/remnote-bridge/instructions/disconnect.md +5 -0
  30. package/skills/remnote-bridge/instructions/edit-tree.md +71 -2
  31. package/skills/remnote-bridge/instructions/health.md +81 -53
  32. package/skills/remnote-bridge/instructions/overall.md +33 -8
  33. package/skills/remnote-bridge/instructions/read-rem-in-tree.md +100 -0
  34. package/skills/remnote-bridge/instructions/read-rem.md +30 -11
  35. package/skills/remnote-bridge-test/SKILL.md +847 -0
  36. package/skills/remnote-bridge-test/references/regression-suite.md +960 -0
  37. 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` |
@@ -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. 标准工作流
@@ -315,9 +348,9 @@ setup 只需执行一次。之后每次连接直接用 `connect --headless`。
315
348
  4. read-globe -- 了解知识库结构(首次探索)
316
349
  或 read-context -- 了解用户当前上下文
317
350
  5. search "关键词" -- 定位目标 Rem(结果不进缓存!)
318
- 6. read-tree <id> -- 展开子树 → 写入缓存(edit-tree 的前置)
319
- 7. read-rem <id> -- 读取属性 写入缓存(edit-rem 的前置)
320
- 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 -- 执行修改
321
354
  9. disconnect -- 结束会话(缓存全部清空,幂等)
322
355
  ```
323
356
 
@@ -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
  ---
@@ -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` 重载页面 |