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.
- package/README.md +141 -28
- package/README.zh-CN.md +368 -0
- package/dist/cli/commands/health.js +231 -112
- package/dist/cli/commands/read-rem-in-tree.js +84 -0
- package/dist/cli/config.js +2 -0
- package/dist/cli/daemon/registry.js +8 -0
- package/dist/cli/handlers/patch-engine.js +347 -0
- package/dist/cli/handlers/read-handler.js +2 -53
- package/dist/cli/handlers/rem-field-filter.js +102 -0
- package/dist/cli/handlers/tree-edit-handler.js +67 -7
- package/dist/cli/handlers/tree-read-handler.js +4 -1
- package/dist/cli/handlers/tree-rem-read-handler.js +73 -0
- package/dist/cli/main.js +53 -2
- package/dist/cli/server/ws-server.js +9 -1
- package/dist/mcp/daemon-client.js +22 -2
- package/dist/mcp/instructions.js +54 -7
- package/dist/mcp/tools/edit-tools.js +7 -2
- package/dist/mcp/tools/infra-tools.js +20 -11
- package/dist/mcp/tools/read-tools.js +88 -2
- package/package.json +1 -1
- package/remnote-plugin/dist/index-sandbox.js +24 -24
- package/remnote-plugin/dist/index.js +24 -24
- package/remnote-plugin/src/bridge/message-router.ts +3 -0
- package/remnote-plugin/src/services/read-rem-in-tree.ts +43 -0
- package/remnote-plugin/src/services/read-rem.ts +15 -0
- package/remnote-plugin/src/services/read-tree.ts +5 -0
- package/skills/remnote-bridge/SKILL.md +37 -4
- package/skills/remnote-bridge/instructions/connect.md +12 -1
- package/skills/remnote-bridge/instructions/disconnect.md +5 -0
- package/skills/remnote-bridge/instructions/edit-tree.md +71 -2
- package/skills/remnote-bridge/instructions/health.md +81 -53
- package/skills/remnote-bridge/instructions/overall.md +33 -8
- package/skills/remnote-bridge/instructions/read-rem-in-tree.md +100 -0
- package/skills/remnote-bridge/instructions/read-rem.md +30 -11
- package/skills/remnote-bridge-test/SKILL.md +847 -0
- package/skills/remnote-bridge-test/references/regression-suite.md +960 -0
- 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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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`。
|
|
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
|
-
├─
|
|
462
|
-
├─
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
✅
|
|
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
|
-
|
|
49
|
+
无活跃实例时:
|
|
53
50
|
|
|
54
51
|
```
|
|
55
|
-
|
|
56
|
-
❌ Plugin 未连接
|
|
57
|
-
❌ SDK 不可用
|
|
58
|
-
|
|
59
|
-
提示: 执行 `remnote-bridge connect` 启动守护进程
|
|
52
|
+
没有活跃的实例。执行 `remnote-bridge connect` 启动守护进程。
|
|
60
53
|
```
|
|
61
54
|
|
|
62
|
-
###
|
|
55
|
+
### 单实例模式
|
|
63
56
|
|
|
64
57
|
```bash
|
|
65
|
-
|
|
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
|
-
###
|
|
135
|
+
### 单实例模式 — daemon 未运行
|
|
101
136
|
|
|
102
137
|
```json
|
|
103
138
|
{
|
|
104
139
|
"ok": false,
|
|
105
140
|
"command": "health",
|
|
106
|
-
"exitCode":
|
|
107
|
-
"instance": "
|
|
108
|
-
"
|
|
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
|
-
"
|
|
146
|
+
"error": "守护进程未运行(实例: default),请先执行 remnote-bridge connect"
|
|
113
147
|
}
|
|
114
148
|
```
|
|
115
149
|
|
|
116
|
-
###
|
|
150
|
+
### 全量模式 — 无活跃实例
|
|
117
151
|
|
|
118
152
|
```json
|
|
119
153
|
{
|
|
120
154
|
"ok": false,
|
|
121
155
|
"command": "health",
|
|
122
156
|
"exitCode": 2,
|
|
123
|
-
"
|
|
124
|
-
"
|
|
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 | 全部健康 |
|
|
187
|
+
| 0 | 全部健康 | 所有实例三层均通过 |
|
|
155
188
|
| 1 | 部分不健康 | daemon 运行但 Plugin 未连接或 SDK 未就绪 |
|
|
156
|
-
| 2 | 不可达 | daemon
|
|
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
|
|
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
|
-
|
|
|
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` 重载页面 |
|