remnote-bridge 0.1.14 → 0.1.16

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.
@@ -7,7 +7,7 @@
7
7
  "version": {
8
8
  "major": 0,
9
9
  "minor": 2,
10
- "patch": 0
10
+ "patch": 1
11
11
  },
12
12
  "theme": [],
13
13
  "enableOnMobile": false,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": true,
3
3
  "name": "unofficial-remnote-bridge-plugin",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "license": "MIT",
6
6
  "description": "RemNote 桥接层:嵌入 RemNote 的 WebSocket 桥接插件",
7
7
  "scripts": {
@@ -7,7 +7,7 @@
7
7
  "version": {
8
8
  "major": 0,
9
9
  "minor": 2,
10
- "patch": 0
10
+ "patch": 1
11
11
  },
12
12
  "theme": [],
13
13
  "enableOnMobile": false,
@@ -211,7 +211,7 @@ export async function buildRemObject(
211
211
  portalType: remTypeToString(rem.type as number) === 'portal'
212
212
  ? portalTypeToString(portalType as number)
213
213
  : null,
214
- portalDirectlyIncludedRem: portalDirectlyIncludedRems.map(r => r._id),
214
+ portalDirectlyIncludedRem: portalDirectlyIncludedRems.map(r => r._id).sort(),
215
215
 
216
216
  // 属性类型
217
217
  propertyType: (propertyType as PropertyTypeValue | undefined) ?? null,
@@ -220,27 +220,27 @@ export async function buildRemObject(
220
220
  enablePractice,
221
221
  practiceDirection: practiceDirection as RemObject['practiceDirection'],
222
222
 
223
- // 关联 — 直接关系
224
- tags: filteredTagRems.map(r => r._id),
225
- sources: sourceRems.map(r => r._id),
226
- aliases: aliasRems.map(r => r._id),
223
+ // 关联 — 直接关系(排序保证确定性序列化)
224
+ tags: filteredTagRems.map(r => r._id).sort(),
225
+ sources: sourceRems.map(r => r._id).sort(),
226
+ aliases: aliasRems.map(r => r._id).sort(),
227
227
 
228
228
  // 关联 — 引用关系
229
- remsBeingReferenced: refsBeingReferenced.map(r => r._id),
230
- deepRemsBeingReferenced: deepRefsBeingReferenced.map(r => r._id),
231
- remsReferencingThis: refsReferencingThis.map(r => r._id),
229
+ remsBeingReferenced: refsBeingReferenced.map(r => r._id).sort(),
230
+ deepRemsBeingReferenced: deepRefsBeingReferenced.map(r => r._id).sort(),
231
+ remsReferencingThis: refsReferencingThis.map(r => r._id).sort(),
232
232
 
233
233
  // 关联 — 标签体系
234
- taggedRem: taggedRems.map(r => r._id),
235
- ancestorTagRem: ancestorTagRems.map(r => r._id),
236
- descendantTagRem: descendantTagRems.map(r => r._id),
234
+ taggedRem: taggedRems.map(r => r._id).sort(),
235
+ ancestorTagRem: ancestorTagRems.map(r => r._id).sort(),
236
+ descendantTagRem: descendantTagRems.map(r => r._id).sort(),
237
237
 
238
238
  // 关联 — 层级遍历
239
- descendants: descendantRems.map(r => r._id),
240
- siblingRem: siblingRems.map(r => r._id),
241
- portalsAndDocumentsIn: portalsAndDocsIn.map(r => r._id),
242
- allRemInDocumentOrPortal: allRemInDocOrPortal.map(r => r._id),
243
- allRemInFolderQueue: allRemInFolderQ.map(r => r._id),
239
+ descendants: descendantRems.map(r => r._id).sort(),
240
+ siblingRem: siblingRems.map(r => r._id).sort(),
241
+ portalsAndDocumentsIn: portalsAndDocsIn.map(r => r._id).sort(),
242
+ allRemInDocumentOrPortal: allRemInDocOrPortal.map(r => r._id).sort(),
243
+ allRemInFolderQueue: allRemInFolderQ.map(r => r._id).sort(),
244
244
 
245
245
  // 位置 / 统计
246
246
  positionAmongstSiblings: position ?? null,
@@ -7,7 +7,7 @@
7
7
  * 依赖方向:services/rem-builder → utils/tree-serializer(单向)
8
8
  */
9
9
 
10
- import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
10
+ import type { ReactRNPlugin, PluginRem as Rem, RichTextInterface } from '@remnote/plugin-sdk';
11
11
  import type { SerializableRem } from '../utils/tree-serializer';
12
12
  import { filterNoisyTags } from './powerup-filter';
13
13
 
@@ -21,7 +21,7 @@ export async function safeToMarkdown(
21
21
  richText: unknown[],
22
22
  ): Promise<string> {
23
23
  try {
24
- return await plugin.richText.toMarkdown(richText);
24
+ return await plugin.richText.toMarkdown(richText as RichTextInterface);
25
25
  } catch {
26
26
  return richTextFallback(richText);
27
27
  }
@@ -93,6 +93,8 @@ export async function buildFullSerializableRem(
93
93
  isTodo,
94
94
  todoStatus,
95
95
  isCode,
96
+ isQuote,
97
+ isListItem,
96
98
  hasDvPowerup,
97
99
  portalIncludedRems,
98
100
  ] = await Promise.all([
@@ -114,6 +116,8 @@ export async function buildFullSerializableRem(
114
116
  rem.isTodo(),
115
117
  rem.getTodoStatus(),
116
118
  rem.isCode(),
119
+ rem.isQuote(),
120
+ rem.isListItem(),
117
121
  rem.hasPowerup('dv'),
118
122
  rem.type === 6 ? rem.getPortalDirectlyIncludedRem() : Promise.resolve([]),
119
123
  ]);
@@ -151,6 +155,8 @@ export async function buildFullSerializableRem(
151
155
  isTodo,
152
156
  todoStatus: (todoStatus as 'Finished' | 'Unfinished' | null) ?? null,
153
157
  isCode,
158
+ isQuote,
159
+ isListItem,
154
160
  isDivider,
155
161
  isTopLevel: rem.parent === null,
156
162
  };
@@ -5,7 +5,7 @@
5
5
  * 多 daemon 连接:Plugin 同时连接 ALL_WS_PORTS 对应的 4 个槽位。
6
6
  */
7
7
 
8
- export const DEFAULT_PLUGIN_VERSION = '0.2.0';
8
+ export const DEFAULT_PLUGIN_VERSION = '0.2.1';
9
9
 
10
10
  /** 4 个固定 WS 端口,对应 4 个 daemon 槽位 */
11
11
  export const ALL_WS_PORTS = [29100, 29110, 29120, 29130] as const;
@@ -65,6 +65,8 @@ export interface SerializableRem {
65
65
  isTodo: boolean;
66
66
  todoStatus: 'Finished' | 'Unfinished' | null;
67
67
  isCode: boolean;
68
+ isQuote: boolean;
69
+ isListItem: boolean;
68
70
  isDivider: boolean;
69
71
  /** 是否为知识库顶级 Rem(无父节点) */
70
72
  isTopLevel?: boolean;
@@ -131,6 +133,12 @@ function buildLineContent(rem: SerializableRem): string {
131
133
  // Code 包裹(最内层)
132
134
  if (rem.isCode) baseContent = '`' + baseContent + '`';
133
135
 
136
+ // ListItem 前缀(有序列表)
137
+ if (rem.isListItem) baseContent = '1. ' + baseContent;
138
+
139
+ // Quote 前缀(引用块)
140
+ if (rem.isQuote) baseContent = '> ' + baseContent;
141
+
134
142
  // Todo 前缀
135
143
  if (rem.isTodo) {
136
144
  const cb = rem.todoStatus === 'Finished' ? '- [x] ' : '- [ ] ';
@@ -207,6 +215,8 @@ export function createMinimalSerializableRem(
207
215
  isTodo: false,
208
216
  todoStatus: null,
209
217
  isCode: false,
218
+ isQuote: false,
219
+ isListItem: false,
210
220
  isDivider: false,
211
221
  ...overrides,
212
222
  };
@@ -283,7 +283,9 @@ edit-tree --json '{"remId":"kLr...","oldStr":"...","newStr":"..."}'
283
283
 
284
284
  ## 3. 标准工作流
285
285
 
286
- ### ⚠️ 标准模式:connect 后需要用户配合
286
+ ### ⚠️ 标准模式(推荐):connect 后需要用户配合
287
+
288
+ **标准模式是推荐的日常使用方式**。用户在自己的浏览器中操作 RemNote,Agent 可以通过 `read-context` 感知用户正在浏览的页面和焦点位置,实现真正的协作。
287
289
 
288
290
  `connect` 成功只意味着 daemon 和 Plugin 服务已启动,**Plugin 并未自动连接**。用户必须在 RemNote 中完成操作,Plugin 才能连接到 daemon:
289
291
 
@@ -299,11 +301,18 @@ edit-tree --json '{"remId":"kLr...","oldStr":"...","newStr":"..."}'
299
301
 
300
302
  **你必须**:执行 `connect` 后,**立即告知用户需要完成上述操作**,不要直接调用业务命令。引导用户完成后,用 `health` 确认三层就绪再继续。
301
303
 
302
- ### Headless 模式:自动连接
304
+ ### Headless 模式(特殊场景,不推荐日常使用)
305
+
306
+ 通过 setup(一次性)+ headless Chrome 实现自动连接,后续 connect 无需用户介入。
307
+
308
+ **⚠️ 不推荐日常使用**。Headless Chrome 是后台独立实例,**会丢失用户上下文**——`read-context` 返回的是 headless Chrome 的上下文,不是用户浏览器的。Agent 无法感知用户正在浏览和操作的页面,协作体验大打折扣。
303
309
 
304
- 标准模式每次 connect 后都需要用户手动操作 RemNote。Headless 模式通过 setup(一次性)+ headless Chrome 实现自动连接,后续 connect 无需用户介入。
310
+ **仅在以下场景使用 headless**:
311
+ - 用户明确要求在**服务器/无 GUI 环境**中运行
312
+ - 用户明确表示**不想参与操作**,希望全自动化(CI/CD、定时任务、批量处理等)
313
+ - 用户自己不在 RemNote 前面,不需要与 Agent 协作浏览
305
314
 
306
- **⚠️ 模式选择建议**:日常使用推荐**标准模式**。Headless 模式下 Chrome 在后台运行,**无法感知用户正在 RemNote 中浏览和操作的界面**(`read-context` 返回的是 headless Chrome 的上下文,而非用户的浏览器)。只有在全自动化场景(CI/CD、定时任务、批量操作等无需与用户界面交互的场景)才建议使用 Headless 模式。
315
+ **默认始终使用标准模式**,除非用户主动要求 headless
307
316
 
308
317
  #### 首次使用(setup)
309
318
 
@@ -442,6 +451,8 @@ read-tree / read-globe / read-context 输出 Markdown 大纲,edit-tree 基于
442
451
  |:-----|:-----|
443
452
  | `# ` / `## ` / `### ` | H1/H2/H3 标题 |
444
453
  | `- [ ] ` / `- [x] ` | 未完成/已完成待办 |
454
+ | `> ` | 引用块 |
455
+ | `1. ` | 有序列表项 |
445
456
  | `` `...` `` | 代码块 |
446
457
  | `---` | 分隔线 |
447
458
 
@@ -512,6 +523,8 @@ read-tree / read-globe / read-context 输出 Markdown 大纲,edit-tree 基于
512
523
  新闪卡 → 答案
513
524
  问题 ↔ 回答
514
525
  - [ ] 新待办
526
+ > 引用内容
527
+ 1. 列表项
515
528
  `代码块`
516
529
  ```
517
530
 
@@ -53,13 +53,20 @@ remnote-bridge --json connect --instance work
53
53
 
54
54
  ## 两种模式
55
55
 
56
- ### 标准模式(默认)
56
+ ### 标准模式(默认,推荐)
57
57
 
58
- 启动 daemon 后需要用户手动在 RemNote 中加载 Plugin。适用于用户已打开 RemNote 的场景。
58
+ **标准模式是推荐的日常使用方式**。启动 daemon 后用户在自己的浏览器中加载 Plugin。优势:Agent 可以通过 `read-context` 感知用户正在浏览的页面和焦点位置,实现真正的协作。
59
59
 
60
- ### Headless 模式(`--headless`)
60
+ ### Headless 模式(`--headless`,特殊场景)
61
61
 
62
- 自动启动 headless Chrome 加载 Plugin,无需用户操作。适用于无 GUI 环境或全自动连接场景。
62
+ 自动启动 headless Chrome 加载 Plugin,无需用户操作。
63
+
64
+ **⚠️ 不推荐日常使用**。Headless Chrome 是后台独立实例,**会丢失用户上下文**——`read-context` 返回的是 headless Chrome 的上下文,不是用户浏览器的。
65
+
66
+ **仅在以下场景使用 headless**:
67
+ - 用户明确要求在**服务器/无 GUI 环境**中运行
68
+ - 用户明确表示**不想参与操作**,希望全自动化(CI/CD、定时任务、批量处理等)
69
+ - 用户自己不在 RemNote 前面,不需要与 Agent 协作浏览
63
70
 
64
71
  **前置条件**:必须先执行 `setup` 完成 RemNote 登录。
65
72
 
@@ -93,12 +100,17 @@ remnote-bridge --headless disconnect # 结束
93
100
 
94
101
  `connect`(不传 `--headless`)成功只意味着 daemon 和 Plugin 服务已启动,**Plugin 并未自动连接**。用户必须在 RemNote 中完成以下操作:
95
102
 
103
+ > **⚠️ 防幻觉红线**:本插件是**开发者插件**,通过「开发你的插件」功能加载本地 URL。
104
+ > - **禁止**告诉用户"去插件市场/商店搜索安装"——本插件**不在 RemNote 插件市场中**
105
+ > - **禁止**告诉用户"Settings → Plugins"——这个路径不存在
106
+ > - **禁止**编造不存在的安装流程——严格按照下方步骤引导用户
107
+
96
108
  ### 首次使用(RemNote 从未加载过此插件)
97
109
 
98
110
  1. 打开 RemNote 桌面端或网页端
99
- 2. 点击左侧边栏底部的插件图标(拼图形状)
100
- 3. 点击「开发你的插件」(Develop Your Plugin)
101
- 4. 在输入框中填入 connect 输出的 Plugin 服务地址(如 `http://localhost:29101`)
111
+ 2. 点击左侧边栏底部的**插件图标**(拼图形状)
112
+ 3. 点击「**开发你的插件**」(Develop Your Plugin)
113
+ 4. 在输入框中填入 connect 输出的 **Plugin 服务地址**(如 `http://localhost:29101`)
102
114
  5. 等待插件加载完成
103
115
 
104
116
  ### 非首次使用(之前已加载过此插件)
@@ -174,64 +174,56 @@ oldStr 必须在缓存大纲中恰好匹配 1 次
174
174
 
175
175
  ---
176
176
 
177
- ## 行引用模板 `{{remId}}`
177
+ ## 两种写法:模板模式与完整匹配模式
178
178
 
179
- oldStr/newStr 中使用 `{{remId}}` 引用缓存大纲中已有行的完整内容(不含缩进)。系统在 str_replace 前自动展开。不含 `{{}}` 的传统写法完全兼容。
179
+ 已有行(带 `<!--remId-->` 注释的行)在 oldStr/newStr 中支持两种写法:
180
180
 
181
- ### 动机
181
+ ### 模板模式(优先使用)
182
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
- **重排(对比传统写法)**
183
+ `{{remId}}` 引用已有行,系统在 str_replace 前自动展开为完整行内容(不含缩进)。节省 token、减少复制错误。
196
184
 
197
185
  ```
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)
186
+ # 重排
203
187
  oldStr: " {{id1_1}}\n {{id1_2}}"
204
188
  newStr: " {{id1_2}}\n {{id1_1}}"
205
- ```
206
189
 
207
- **移动(改变缩进)**
208
-
209
- ```
190
+ # 移动(改变缩进 = 改变父节点)
210
191
  oldStr: " {{idA}}\n {{idT}}\n {{idB}}"
211
192
  newStr: " {{idA}}\n {{idB}}\n {{idT}}"
212
- ```
213
-
214
- **删除(模板用于上下文定位)**
215
193
 
216
- ```
194
+ # 删除(必须同时删子行)
217
195
  oldStr: " {{idA}}\n {{idA1}}\n {{idB}}"
218
196
  newStr: " {{idB}}"
219
- ```
220
-
221
- **新增 + 模板混用**
222
197
 
223
- ```
198
+ # 新增(新增行手动写,已有行用模板)
224
199
  oldStr: " {{idZ}}"
225
200
  newStr: " 新增行\n {{idZ}}"
226
201
  ```
227
202
 
228
- ### 限制
229
-
230
- - 只匹配纯字母数字(`[a-zA-Z0-9]+`),与 RemNote cloze 语法 `{{text}}` 不冲突(cloze 含中文/空格/标点不会被匹配)
203
+ **模板规则**:
204
+ - `{{remId}}` 展开为**不含缩进**的完整行内容,缩进由你控制
205
+ - 只匹配纯字母数字(`[a-zA-Z0-9]+`),与 RemNote cloze 语法 `{{text}}` 不冲突
231
206
  - 匹配到但不在缓存大纲中的 `{{xxx}}` 原样保留(可能是 cloze),并输出 templateWarnings
232
- - `{{remId}}` 不含缩进,缩进由 AI 控制(move 操作会改变缩进)
233
207
  - 新增行没有 remId,不能用模板表示
234
208
 
209
+ ### 完整匹配模式(回退)
210
+
211
+ 直接从大纲复制已有行的完整内容(含 `<!--remId 元数据-->`)。
212
+
213
+ ```
214
+ # 重排
215
+ oldStr: " 动态数组 <!--id1_1 type:concept-->\n 静态数组 <!--id1_2 type:concept-->"
216
+ newStr: " 静态数组 <!--id1_2 type:concept-->\n 动态数组 <!--id1_1 type:concept-->"
217
+
218
+ # 移动
219
+ oldStr: " 子节点 A <!--idA-->\n 目标行 <!--idT-->\n 子节点 B <!--idB-->"
220
+ newStr: " 子节点 A <!--idA-->\n 子节点 B <!--idB-->\n 目标行 <!--idT-->"
221
+ ```
222
+
223
+ ### ⚠️ 回退策略
224
+
225
+ **优先使用模板模式**。但如果模板模式连续 2+ 次因 ID 错误导致 `old_str not found`,说明当前上下文不足以准确引用 ID——**立即切换到完整匹配模式**(重新 read_tree,从最新大纲复制完整行内容),不要反复重试模板。
226
+
235
227
  ---
236
228
 
237
229
  ## 支持的操作
@@ -241,12 +233,13 @@ newStr: " 新增行\n {{idZ}}"
241
233
  在 newStr 中添加**无 remId 注释**的新行。新行可以使用 Markdown 前缀和箭头分隔符来设置属性。
242
234
 
243
235
  ```
244
- oldStr:
245
- 子节点 A <!--idA-->
236
+ # 模板模式
237
+ oldStr: " {{idA}}"
238
+ newStr: " 新增节点\n {{idA}}"
246
239
 
247
- newStr:
248
- 新增节点
249
- 子节点 A <!--idA-->
240
+ # 完整匹配模式
241
+ oldStr: " 子节点 A <!--idA-->"
242
+ newStr: " 新增节点\n 子节点 A <!--idA-->"
250
243
  ```
251
244
 
252
245
  #### 新增行的 Markdown 前缀
@@ -258,10 +251,20 @@ newStr:
258
251
  | `### text` | 创建 H3 标题 |
259
252
  | `- [ ] text` | 创建未完成待办 |
260
253
  | `- [x] text` | 创建已完成待办 |
254
+ | `1. text` | 创建有序列表项 |
261
255
  | `` `text` `` | 创建代码块 |
262
256
  | `---` | 创建分隔线 |
263
257
 
264
- 前缀可组合叠加,解析顺序为 Header → Todo → Code。例如 `## - [ ] text` 创建 H2 + 未完成待办。
258
+ 前缀可组合叠加,解析顺序为 Header → Todo → Quote → ListItem → Code。例如 `## - [ ] text` 创建 H2 + 未完成待办。
259
+
260
+ > **⚠️ 有序列表必须用 `1. ` 前缀**
261
+ >
262
+ > RemNote 的有序列表采用 Lazy Numbering 风格——所有列表项统一写 `1. `,RemNote 按层级自动编号为 1./2./3./A./B./I./II. 等。
263
+ >
264
+ > - **正确**:`1. 第一项`、`1. 第二项`、`1. 第三项`(全部用 `1. `)
265
+ > - **错误**:`2. 第二项`、`3. 第三项`(手动编号无意义,RemNote 会忽略)
266
+ >
267
+ > 系统会容错处理 `2. `~`9. ` 前缀(自动归一化为 `isListItem=true` 并返回 `templateWarnings` 警告),但 `10. ` 及以上不会被识别为有序列表,而是作为纯文本保留。
265
268
 
266
269
  #### 新增行的箭头分隔符
267
270
 
@@ -326,12 +329,13 @@ newStr:
326
329
  示例:
327
330
 
328
331
  ```
329
- oldStr:
330
- 子节点 A <!--idA-->
332
+ # 模板模式
333
+ oldStr: " {{idA}}"
334
+ newStr: " <!--portal refs:refId1,refId2-->\n {{idA}}"
331
335
 
332
- newStr:
333
- <!--portal refs:refId1,refId2-->
334
- 子节点 A <!--idA-->
336
+ # 完整匹配模式
337
+ oldStr: " 子节点 A <!--idA-->"
338
+ newStr: " <!--portal refs:refId1,refId2-->\n 子节点 A <!--idA-->"
335
339
  ```
336
340
 
337
341
  #### 嵌套新增
@@ -339,11 +343,13 @@ newStr:
339
343
  新增行下面可以再嵌套新增行,通过缩进表示父子关系:
340
344
 
341
345
  ```
342
- newStr:
343
- 父节点 ↓
344
- 答案行 1
345
- 答案行 2
346
- 子节点 A <!--idA-->
346
+ # 模板模式
347
+ oldStr: " {{idA}}"
348
+ newStr: " 父节点 ↓\n 答案行 1\n 答案行 2\n {{idA}}"
349
+
350
+ # 完整匹配模式
351
+ oldStr: " 子节点 A <!--idA-->"
352
+ newStr: " 父节点 ↓\n 答案行 1\n 答案行 2\n 子节点 A <!--idA-->"
347
353
  ```
348
354
 
349
355
  嵌套新增行的父 ID 通过内部占位标记 `__new_N__` 管理,创建顺序保证从浅到深。
@@ -353,14 +359,13 @@ newStr:
353
359
  新行**不能**插在一个有子节点的 Rem 和它的 children 之间,否则 children 会被新行"劫持",触发 `children_captured` 错误。
354
360
 
355
361
  ```
356
- 错误:插在父 Rem 和 children 之间
357
- 水分子 <!--idA-->
358
- 新行 ← children 被解析为新行的子节点!
359
- 化学式 H₂O <!--idB role:card-item-->
362
+ 错误(模板):
363
+ oldStr: " {{idA}}" newStr: " {{idA}}\n 新行" ← idA 有子节点,新行劫持 children!
364
+ 错误(完整匹配):
365
+ oldStr: " 水分子 <!--idA-->" newStr: " 水分子 ↓ <!--idA-->\n 新行" ← 同理
360
366
 
361
- 正确:插在所有兄弟末尾
362
- 极性 ... <!--idZ role:card-item-->
363
- 新行 ← 不影响任何已有节点
367
+ 正确:插在末尾
368
+ oldStr: " {{idZ}}" newStr: " {{idZ}}\n 新行"
364
369
  ```
365
370
 
366
371
  #### 两步操作:创建新节点并移入已有 children
@@ -377,13 +382,13 @@ newStr:
377
382
  从 newStr 中移除带 remId 的行。**必须同时删除该行的所有可见子行**,否则报 orphan_detected 错误。
378
383
 
379
384
  ```
380
- oldStr:
381
- 子节点 A <!--idA-->
382
- 孙节点 A1 <!--idA1-->
383
- 子节点 B <!--idB-->
385
+ # 模板模式
386
+ oldStr: " {{idA}}\n {{idA1}}\n {{idB}}"
387
+ newStr: " {{idB}}"
384
388
 
385
- newStr:
386
- 子节点 B <!--idB-->
389
+ # 完整匹配模式
390
+ oldStr: " 子节点 A <!--idA-->\n 孙节点 A1 <!--idA1-->\n 子节点 B <!--idB-->"
391
+ newStr: " 子节点 B <!--idB-->"
387
392
  ```
388
393
 
389
394
  删除操作按深度**从深到浅**执行(先删子后删父),确保 RemNote SDK 不会拒绝操作。
@@ -393,15 +398,13 @@ newStr:
393
398
  改变行的缩进级别或位置,使其移动到新的父节点下:
394
399
 
395
400
  ```
396
- oldStr:
397
- 子节点 A <!--idA-->
398
- 目标行 <!--idT-->
399
- 子节点 B <!--idB-->
401
+ # 模板模式
402
+ oldStr: " {{idA}}\n {{idT}}\n {{idB}}"
403
+ newStr: " {{idA}}\n {{idB}}\n {{idT}}"
400
404
 
401
- newStr:
402
- 子节点 A <!--idA-->
403
- 子节点 B <!--idB-->
404
- 目标行 <!--idT-->
405
+ # 完整匹配模式
406
+ oldStr: " 子节点 A <!--idA-->\n 目标行 <!--idT-->\n 子节点 B <!--idB-->"
407
+ newStr: " 子节点 A <!--idA-->\n 子节点 B <!--idB-->\n 目标行 <!--idT-->"
405
408
  ```
406
409
 
407
410
  ### 重排行
@@ -409,15 +412,13 @@ newStr:
409
412
  调换同级行的顺序:
410
413
 
411
414
  ```
412
- oldStr:
413
- 子节点 A <!--idA-->
414
- 子节点 B <!--idB-->
415
- 子节点 C <!--idC-->
415
+ # 模板模式
416
+ oldStr: " {{idA}}\n {{idB}}\n {{idC}}"
417
+ newStr: " {{idC}}\n {{idA}}\n {{idB}}"
416
418
 
417
- newStr:
418
- 子节点 C <!--idC-->
419
- 子节点 A <!--idA-->
420
- 子节点 B <!--idB-->
419
+ # 完整匹配模式
420
+ oldStr: " 子节点 A <!--idA-->\n 子节点 B <!--idB-->\n 子节点 C <!--idC-->"
421
+ newStr: " 子节点 C <!--idC-->\n 子节点 A <!--idA-->\n 子节点 B <!--idB-->"
421
422
  ```
422
423
 
423
424
  ---
@@ -543,36 +544,42 @@ RemNote SDK 存在已知 bug:
543
544
 
544
545
  ---
545
546
 
546
- ## 常见使用模式
547
+ ## 常见使用模式(JSON 模式)
548
+
549
+ > 优先使用模板模式;连续 2+ 次 `old_str not found` 则回退到完整匹配模式。
547
550
 
548
551
  ### 在指定位置插入新行
549
552
 
550
553
  ```bash
551
- remnote-bridge edit-tree kLr --old-str ' 子节点 A <!--idA-->' --new-str ' 新增行\n 子节点 A <!--idA-->'
554
+ # 模板模式
555
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{idA}}","newStr":" 新增行\n {{idA}}"}'
556
+
557
+ # 完整匹配模式
558
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" 子节点 A <!--idA-->","newStr":" 新增行\n 子节点 A <!--idA-->"}'
552
559
  ```
553
560
 
554
561
  ### 删除一个叶子节点
555
562
 
556
563
  ```bash
557
- remnote-bridge edit-tree kLr --old-str ' 叶子节点 <!--leaf-->\n' --new-str ''
564
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{leaf}}\n","newStr":""}'
558
565
  ```
559
566
 
560
567
  ### 调换两个兄弟的顺序
561
568
 
562
569
  ```bash
563
- # 传统写法
564
- remnote-bridge edit-tree kLr --old-str ' 节点 A <!--idA-->\n 节点 B <!--idB-->' --new-str ' 节点 B <!--idB-->\n 节点 A <!--idA-->'
565
-
566
- # 模板写法(JSON 模式)
570
+ # 模板模式
567
571
  remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{idA}}\n {{idB}}","newStr":" {{idB}}\n {{idA}}"}'
572
+
573
+ # 完整匹配模式
574
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" 节点 A <!--idA-->\n 节点 B <!--idB-->","newStr":" 节点 B <!--idB-->\n 节点 A <!--idA-->"}'
568
575
  ```
569
576
 
570
577
  ### 将节点移到另一个父节点下
571
578
 
572
579
  ```bash
573
- # 传统写法
574
- remnote-bridge edit-tree kLr --old-str ' 旧父 <!--oldP-->\n 目标 <!--target-->\n 新父 <!--newP-->' --new-str ' 旧父 <!--oldP-->\n 新父 <!--newP-->\n 目标 <!--target-->'
575
-
576
- # 模板写法(JSON 模式)
580
+ # 模板模式
577
581
  remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" {{oldP}}\n {{target}}\n {{newP}}","newStr":" {{oldP}}\n {{newP}}\n {{target}}"}'
582
+
583
+ # 完整匹配模式
584
+ remnote-bridge edit-tree --json '{"remId":"kLr","oldStr":" 旧父 <!--oldP-->\n 目标 <!--target-->\n 新父 <!--newP-->","newStr":" 旧父 <!--oldP-->\n 新父 <!--newP-->\n 目标 <!--target-->"}'
578
585
  ```
@@ -190,6 +190,8 @@ RemNote SDK → 知识库
190
190
 
191
191
  一次**会话(Session)= 守护进程的生命周期**。
192
192
 
193
+ **标准模式(推荐)**——用户在自己的浏览器中操作 RemNote,Agent 可感知用户上下文:
194
+
193
195
  ```
194
196
  connect → daemon 启动
195
197
 
@@ -201,6 +203,10 @@ disconnect → daemon 关闭 → 会话结束,缓存清空
201
203
  ```
202
204
 
203
205
  > **重要**:`connect` 成功只意味着 daemon 已启动,Plugin 并未自动连接。首次使用需用户在 RemNote「开发你的插件」中填入对应的 Plugin 服务地址;非首次只需刷新 RemNote 页面。必须引导用户完成此步后再用 `health` 确认就绪。
206
+ >
207
+ > **⚠️ 防幻觉红线**:本插件是**开发者插件**,通过「开发你的插件」加载本地 URL。**禁止**告诉用户去插件市场/商店搜索安装(插件不在市场中);**禁止**编造"Settings → Plugins"等不存在的路径。
208
+
209
+ **Headless 模式(不推荐日常使用)**——通过后台 Chrome 自动连接,但**会丢失用户上下文**(`read-context` 返回 headless 实例的上下文,不是用户浏览器的)。仅在以下场景使用:用户明确要求在服务器/无 GUI 环境运行、用户明确不想参与操作(全自动化)、用户不在 RemNote 前面。详见 `connect.md`。
204
210
 
205
211
  `connect` 启动三个服务,端口由槽位自动分配:
206
212
 
@@ -636,9 +642,17 @@ read-tree / read-globe / read-context 的输出核心是 Markdown 大纲文本
636
642
  | `### ` | H3 标题 | `fontSize: 'H3'` |
637
643
  | `- [ ] ` | 未完成待办 | `isTodo: true, todoStatus: 'Unfinished'` |
638
644
  | `- [x] ` | 已完成待办 | `isTodo: true, todoStatus: 'Finished'` |
645
+ | `> ` | 引用块 | `isQuote: true` |
646
+ | `1. ` | 有序列表项(⚠️ 见下方说明) | `isListItem: true` |
639
647
  | `` `...` `` | 代码块 | `isCode: true` |
640
648
  | `---` | 分隔线 | Divider Powerup |
641
649
 
650
+ > **⚠️ 有序列表必须用 `1. ` 前缀(Lazy Numbering)**
651
+ >
652
+ > RemNote 有序列表采用 Lazy Numbering——所有列表项统一写 `1. `,RemNote 按层级自动编号(1./2./3./A./B./I./II.)。不要手动编号。
653
+ > - `2. `~`9. ` 会被容错处理(归一化为 `isListItem=true`,返回 `templateWarnings` 警告)
654
+ > - `10. ` 及以上**不会**被识别为有序列表,而是作为纯文本内容保留
655
+
642
656
  ### 8.3 箭头分隔符
643
657
 
644
658
  箭头编码 `practiceDirection`(闪卡练习方向),不编码 type(type 由元数据标记承载)。
@@ -805,6 +819,8 @@ edit-tree 使用 str_replace 对 Markdown 大纲进行结构编辑。详细文
805
819
  # 新标题 H1
806
820
  新闪卡 → 答案
807
821
  - [ ] 新待办
822
+ > 引用内容
823
+ 1. 列表项
808
824
  `代码块内容`
809
825
  ```
810
826
 
@@ -140,6 +140,9 @@ Skill 接口加 `s` 后缀(**s** = Skill)。这样两个 subagent 创建的
140
140
  ### Step 2:构造 subagent 并执行
141
141
 
142
142
  使用 haiku subagent 执行测试。
143
+ 严禁只使用单个 sub-agent 进行测试。
144
+ 除非测试用例有特别说明,否则必须同时启用 MCP 和 Skill 两个 sub-agent 进行测试。根据测试要求,动态决定是否并行。
145
+ 严禁只测一个 sub-agent 的行为。
143
146
 
144
147
  #### 双接口策略
145
148