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.
- package/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/dist/cli/commands/edit-tree.js +12 -1
- package/dist/cli/handlers/edit-handler.js +14 -0
- package/dist/cli/handlers/tree-edit-handler.js +3 -1
- package/dist/cli/handlers/tree-parser.js +17 -0
- package/dist/mcp/instructions.js +67 -71
- package/dist/mcp/tools/edit-tools.js +2 -1
- package/dist/mcp/tools/infra-tools.js +1 -1
- package/package.json +1 -1
- package/remnote-plugin/dist/index-sandbox.js +19 -19
- package/remnote-plugin/dist/index.js +19 -19
- package/remnote-plugin/dist/manifest.json +1 -1
- package/remnote-plugin/package.json +1 -1
- package/remnote-plugin/public/manifest.json +1 -1
- package/remnote-plugin/src/services/read-rem.ts +16 -16
- package/remnote-plugin/src/services/rem-builder.ts +8 -2
- package/remnote-plugin/src/settings.ts +1 -1
- package/remnote-plugin/src/utils/tree-serializer.ts +10 -0
- package/skills/remnote-bridge/SKILL.md +17 -4
- package/skills/remnote-bridge/instructions/connect.md +19 -7
- package/skills/remnote-bridge/instructions/edit-tree.md +100 -93
- package/skills/remnote-bridge/instructions/overall.md +16 -0
- package/skills/remnote-bridge-test/SKILL.md +3 -0
- package/skills/remnote-bridge-test/references/regression-suite.md +64 -0
|
@@ -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.
|
|
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
|
-
### ⚠️
|
|
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
|
-
|
|
310
|
+
**仅在以下场景使用 headless**:
|
|
311
|
+
- 用户明确要求在**服务器/无 GUI 环境**中运行
|
|
312
|
+
- 用户明确表示**不想参与操作**,希望全自动化(CI/CD、定时任务、批量处理等)
|
|
313
|
+
- 用户自己不在 RemNote 前面,不需要与 Agent 协作浏览
|
|
305
314
|
|
|
306
|
-
|
|
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
|
-
|
|
58
|
+
**标准模式是推荐的日常使用方式**。启动 daemon 后用户在自己的浏览器中加载 Plugin。优势:Agent 可以通过 `read-context` 感知用户正在浏览的页面和焦点位置,实现真正的协作。
|
|
59
59
|
|
|
60
|
-
### Headless 模式(`--headless
|
|
60
|
+
### Headless 模式(`--headless`,特殊场景)
|
|
61
61
|
|
|
62
|
-
自动启动 headless Chrome 加载 Plugin
|
|
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.
|
|
101
|
-
4. 在输入框中填入 connect 输出的 Plugin
|
|
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
|
-
##
|
|
177
|
+
## 两种写法:模板模式与完整匹配模式
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
已有行(带 `<!--remId-->` 注释的行)在 oldStr/newStr 中支持两种写法:
|
|
180
180
|
|
|
181
|
-
###
|
|
181
|
+
### 模板模式(优先使用)
|
|
182
182
|
|
|
183
|
-
|
|
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
|
-
#
|
|
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}}`
|
|
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
|
-
|
|
245
|
-
|
|
236
|
+
# 模板模式
|
|
237
|
+
oldStr: " {{idA}}"
|
|
238
|
+
newStr: " 新增节点\n {{idA}}"
|
|
246
239
|
|
|
247
|
-
|
|
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
|
-
|
|
330
|
-
|
|
332
|
+
# 模板模式
|
|
333
|
+
oldStr: " {{idA}}"
|
|
334
|
+
newStr: " <!--portal refs:refId1,refId2-->\n {{idA}}"
|
|
331
335
|
|
|
332
|
-
|
|
333
|
-
<!--
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
答案行 1
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
❌
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
❌ 错误(模板):
|
|
363
|
+
oldStr: " {{idA}}" newStr: " {{idA}}\n 新行" ← idA 有子节点,新行劫持 children!
|
|
364
|
+
❌ 错误(完整匹配):
|
|
365
|
+
oldStr: " 水分子 ↓ <!--idA-->" newStr: " 水分子 ↓ <!--idA-->\n 新行" ← 同理
|
|
360
366
|
|
|
361
|
-
✅
|
|
362
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
子节点 B <!--idB-->
|
|
385
|
+
# 模板模式
|
|
386
|
+
oldStr: " {{idA}}\n {{idA1}}\n {{idB}}"
|
|
387
|
+
newStr: " {{idB}}"
|
|
384
388
|
|
|
385
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
子节点 B <!--idB-->
|
|
401
|
+
# 模板模式
|
|
402
|
+
oldStr: " {{idA}}\n {{idT}}\n {{idB}}"
|
|
403
|
+
newStr: " {{idA}}\n {{idB}}\n {{idT}}"
|
|
400
404
|
|
|
401
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
子节点 C <!--idC-->
|
|
415
|
+
# 模板模式
|
|
416
|
+
oldStr: " {{idA}}\n {{idB}}\n {{idC}}"
|
|
417
|
+
newStr: " {{idC}}\n {{idA}}\n {{idB}}"
|
|
416
418
|
|
|
417
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|