remnote-bridge 0.1.15 → 0.1.17
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 +13 -2
- package/README.zh-CN.md +13 -2
- package/dist/cli/commands/edit-tree.js +12 -1
- package/dist/cli/commands/read-context.js +6 -4
- package/dist/cli/config.js +1 -1
- package/dist/cli/handlers/edit-handler.js +63 -16
- package/dist/cli/handlers/tree-edit-handler.js +3 -1
- package/dist/cli/handlers/tree-parser.js +17 -0
- package/dist/cli/main.js +1 -1
- package/dist/cli/server/config-server.js +1 -1
- package/dist/mcp/instructions.js +12 -8
- package/dist/mcp/tools/edit-tools.js +9 -4
- package/dist/mcp/tools/read-tools.js +8 -4
- 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/src/services/read-context.ts +1 -1
- package/remnote-plugin/src/services/rem-builder.ts +8 -2
- package/remnote-plugin/src/types.ts +59 -51
- package/remnote-plugin/src/utils/tree-serializer.ts +10 -0
- package/skills/remnote-bridge/SKILL.md +19 -8
- package/skills/remnote-bridge/instructions/edit-rem.md +44 -15
- package/skills/remnote-bridge/instructions/edit-tree.md +11 -1
- package/skills/remnote-bridge/instructions/overall.md +43 -19
- package/skills/remnote-bridge/instructions/read-context.md +10 -5
- package/skills/remnote-bridge-test/SKILL.md +3 -0
- package/skills/remnote-bridge-test/references/regression-suite.md +64 -0
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ remnote-bridge health
|
|
|
60
60
|
|
|
61
61
|
# 4. Explore your knowledge base
|
|
62
62
|
remnote-bridge read-globe # Global document overview
|
|
63
|
-
remnote-bridge read-context # Current
|
|
63
|
+
remnote-bridge read-context # Current page context in RemNote
|
|
64
64
|
remnote-bridge search "machine learning" # Full-text search
|
|
65
65
|
remnote-bridge read-tree <remId> # Expand a subtree
|
|
66
66
|
remnote-bridge read-rem <remId> # Read Rem properties
|
|
@@ -108,7 +108,7 @@ remnote-bridge search "machine learning"
|
|
|
108
108
|
| Command | Description | Caches |
|
|
109
109
|
|:--------|:------------|:-------|
|
|
110
110
|
| `read-globe` | Global document-level overview | No |
|
|
111
|
-
| `read-context` | Current focus
|
|
111
|
+
| `read-context` | Current page/focus context view | No |
|
|
112
112
|
| `read-tree <remId>` | Subtree as Markdown outline | Yes |
|
|
113
113
|
| `read-rem <remId>` | Single Rem's full JSON properties | Yes |
|
|
114
114
|
| `read-rem-in-tree <remId>` | Subtree outline + all Rem objects in one call | Yes |
|
|
@@ -292,6 +292,17 @@ remnote-bridge addon uninstall remnote-rag --purge
|
|
|
292
292
|
|
|
293
293
|
## Changelog
|
|
294
294
|
|
|
295
|
+
### 0.1.17 (2026-03-19)
|
|
296
|
+
|
|
297
|
+
- **Defense-2 three-layer field classification** — `edit_rem` concurrency detection now classifies field differences into three tiers: semantic fields (text/type/tags etc.) → hard reject; parent → pass with `⚠️ parent has changed` warning; metadata (position/timestamps etc.) → pass with `ℹ️ Metadata fields changed` warning. This eliminates false positives after `edit_tree` move/reorder operations
|
|
298
|
+
- **Documentation sync** — Updated defense-2 descriptions across all 5 doc surfaces (SKILL.md, edit-rem.md, overall.md, MCP tool description, SERVER_INSTRUCTIONS) with three-layer classification, warning text examples, and revised judgment tree
|
|
299
|
+
|
|
300
|
+
### 0.1.16 (2026-03-19)
|
|
301
|
+
|
|
302
|
+
- **Ordered list prefix tolerance** — `edit-tree` now accepts `2.`~`9.` prefixes for ordered lists, auto-normalizes to `isListItem=true`, and returns `templateWarnings` to remind agents to use `1.` (Lazy Numbering). `10.` and above are not matched (kept as plain text)
|
|
303
|
+
- **templateWarnings passthrough fix** — CLI `edit-tree` command now correctly includes `templateWarnings` in JSON output (was silently dropped)
|
|
304
|
+
- **Lazy Numbering documentation** — Added prominent warnings in MCP tool description, SERVER_INSTRUCTIONS, and Skill docs explaining RemNote's auto-numbering convention
|
|
305
|
+
|
|
295
306
|
### 0.1.15 (2026-03-18)
|
|
296
307
|
|
|
297
308
|
- **Defense-2 false positive fix** — Removed `.sort()` from ID arrays in RemObject serialization that caused spurious concurrency conflicts in edit-rem
|
package/README.zh-CN.md
CHANGED
|
@@ -60,7 +60,7 @@ remnote-bridge health
|
|
|
60
60
|
|
|
61
61
|
# 4. 浏览知识库
|
|
62
62
|
remnote-bridge read-globe # 全局文档概览
|
|
63
|
-
remnote-bridge read-context #
|
|
63
|
+
remnote-bridge read-context # 当前页面/焦点上下文
|
|
64
64
|
remnote-bridge search "machine learning" # 全文搜索
|
|
65
65
|
remnote-bridge read-tree <remId> # 展开子树
|
|
66
66
|
remnote-bridge read-rem <remId> # 读取 Rem 属性
|
|
@@ -108,7 +108,7 @@ remnote-bridge search "machine learning"
|
|
|
108
108
|
| 命令 | 说明 | 缓存 |
|
|
109
109
|
|:-----|:-----|:-----|
|
|
110
110
|
| `read-globe` | 全局文档级概览 | 否 |
|
|
111
|
-
| `read-context` |
|
|
111
|
+
| `read-context` | 当前页面/焦点上下文视图 | 否 |
|
|
112
112
|
| `read-tree <remId>` | 子树序列化为 Markdown 大纲 | 是 |
|
|
113
113
|
| `read-rem <remId>` | 单个 Rem 的完整 JSON 属性 | 是 |
|
|
114
114
|
| `read-rem-in-tree <remId>` | 子树大纲 + 所有 Rem 对象,一次调用 | 是 |
|
|
@@ -292,6 +292,17 @@ remnote-bridge addon uninstall remnote-rag --purge
|
|
|
292
292
|
|
|
293
293
|
## Changelog
|
|
294
294
|
|
|
295
|
+
### 0.1.17 (2026-03-19)
|
|
296
|
+
|
|
297
|
+
- **防线 2 三层字段分类** — `edit_rem` 并发检测现在将字段差异分为三层:语义字段(text/type/tags 等)→ 硬拒绝;parent → 放行并返回 `⚠️ parent has changed` 警告;元数据(位置/时间戳等)→ 放行并返回 `ℹ️ Metadata fields changed` 警告。消除 `edit_tree` 移动/重排后的误报
|
|
298
|
+
- **文档同步** — 在全部 5 个文档面(SKILL.md、edit-rem.md、overall.md、MCP 工具描述、SERVER_INSTRUCTIONS)更新防线 2 描述:三层分类机制、警告文本示例、修订判断树
|
|
299
|
+
|
|
300
|
+
### 0.1.16 (2026-03-19)
|
|
301
|
+
|
|
302
|
+
- **有序列表前缀容错** — `edit-tree` 新增行现在接受 `2.`~`9.` 前缀,自动归一化为 `isListItem=true`,并返回 `templateWarnings` 提醒使用 `1.`(Lazy Numbering 风格)。`10.` 及以上不匹配(保留为纯文本)
|
|
303
|
+
- **templateWarnings 透传修复** — CLI `edit-tree` 命令现在正确包含 `templateWarnings` 到 JSON 输出中(之前被静默丢弃)
|
|
304
|
+
- **Lazy Numbering 文档** — 在 MCP 工具描述、SERVER_INSTRUCTIONS 和 Skill 文档中添加醒目的有序列表使用规范说明
|
|
305
|
+
|
|
295
306
|
### 0.1.15 (2026-03-18)
|
|
296
307
|
|
|
297
308
|
- **防线 2 误判修复** — 移除 RemObject 序列化中 ID 数组的 `.sort()`,消除 edit-rem 并发检测的假阳性
|
|
@@ -33,7 +33,12 @@ export async function editTreeCommand(remId, options) {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
if (json) {
|
|
36
|
-
jsonOutput({
|
|
36
|
+
jsonOutput({
|
|
37
|
+
ok: true,
|
|
38
|
+
command: 'edit-tree',
|
|
39
|
+
operations: data.operations,
|
|
40
|
+
...(data.templateWarnings?.length && { templateWarnings: data.templateWarnings }),
|
|
41
|
+
});
|
|
37
42
|
}
|
|
38
43
|
else {
|
|
39
44
|
if (data.operations.length === 0) {
|
|
@@ -46,5 +51,11 @@ export async function editTreeCommand(remId, options) {
|
|
|
46
51
|
console.log(` - ${o.type}: ${JSON.stringify(o)}`);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
54
|
+
if (data.templateWarnings?.length) {
|
|
55
|
+
console.log('⚠ 模板/前缀警告:');
|
|
56
|
+
for (const w of data.templateWarnings) {
|
|
57
|
+
console.log(` - ${w}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
49
60
|
}
|
|
50
61
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* read-context 命令
|
|
3
3
|
*
|
|
4
4
|
* 读取当前上下文视图。
|
|
5
|
-
* - --mode focus|page(默认
|
|
5
|
+
* - --mode focus|page(默认 page)
|
|
6
6
|
* - --ancestor-levels N 向上追溯几层祖先(默认 2,仅 focus 模式)
|
|
7
7
|
* - --depth N 展开深度(默认 3,仅 page 模式)
|
|
8
8
|
* - --max-nodes N 全局节点上限(默认 200)
|
|
@@ -14,8 +14,8 @@ import { sendDaemonRequest } from '../daemon/send-request.js';
|
|
|
14
14
|
import { jsonOutput, handleCommandError } from '../utils/output.js';
|
|
15
15
|
export async function readContextCommand(options = {}) {
|
|
16
16
|
const { json } = options;
|
|
17
|
-
const mode = options.mode
|
|
18
|
-
if (mode !== 'focus' && mode !== 'page') {
|
|
17
|
+
const mode = options.mode;
|
|
18
|
+
if (mode !== undefined && mode !== 'focus' && mode !== 'page') {
|
|
19
19
|
const errMsg = '--mode must be "focus" or "page"';
|
|
20
20
|
if (json) {
|
|
21
21
|
jsonOutput({ ok: false, command: 'read-context', error: errMsg });
|
|
@@ -43,7 +43,9 @@ export async function readContextCommand(options = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
let result;
|
|
45
45
|
try {
|
|
46
|
-
const reqPayload = {
|
|
46
|
+
const reqPayload = { ancestorLevels, depth, maxNodes, maxSiblings };
|
|
47
|
+
if (mode)
|
|
48
|
+
reqPayload.mode = mode;
|
|
47
49
|
if (options.focusRemId)
|
|
48
50
|
reqPayload.focusRemId = options.focusRemId;
|
|
49
51
|
result = await sendDaemonRequest('read_context', reqPayload);
|
package/dist/cli/config.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Plugin 只负责原子写入(write_rem_fields)。
|
|
6
6
|
*
|
|
7
7
|
* 防线 1:缓存存在性检查(必须先 read 再 edit)
|
|
8
|
-
* 防线 2
|
|
8
|
+
* 防线 2:语义并发检测(三层字段比较:语义字段硬拒绝,元数据放行+警告)
|
|
9
9
|
*/
|
|
10
10
|
/** 只读字段集合 — 变更这些字段只产生警告,不执行写入 */
|
|
11
11
|
const READ_ONLY_FIELDS = new Set([
|
|
@@ -25,6 +25,28 @@ const READ_ONLY_FIELDS = new Set([
|
|
|
25
25
|
'isPowerup', 'isPowerupEnum', 'isPowerupProperty',
|
|
26
26
|
'isPowerupPropertyListItem', 'isPowerupSlot',
|
|
27
27
|
]);
|
|
28
|
+
// ── 防线 2 三层字段分类 ──────────────────────────────────────
|
|
29
|
+
// 不在以下两个集合中的字段 = 第1层语义字段(硬拒绝)
|
|
30
|
+
/** 第2层:敏感元数据字段——放行但输出专门警告。扩展时须同步更新 handleEditRem 中的 warned 警告生成逻辑 */
|
|
31
|
+
const DEFENSE2_WARN_FIELDS = new Set([
|
|
32
|
+
'parent',
|
|
33
|
+
]);
|
|
34
|
+
/** 第3层:普通元数据字段——放行,有变化时输出统一警告 */
|
|
35
|
+
const DEFENSE2_IGNORE_FIELDS = new Set([
|
|
36
|
+
'id',
|
|
37
|
+
// 位置/时间
|
|
38
|
+
'positionAmongstSiblings', 'updatedAt', 'localUpdatedAt', 'createdAt',
|
|
39
|
+
'lastPracticed', 'lastTimeMovedTo', 'timesSelectedInSearch',
|
|
40
|
+
// 级联关联
|
|
41
|
+
'children', 'siblingRem', 'descendants',
|
|
42
|
+
'deepRemsBeingReferenced', 'remsReferencingThis',
|
|
43
|
+
'taggedRem', 'ancestorTagRem', 'descendantTagRem',
|
|
44
|
+
'portalsAndDocumentsIn', 'allRemInDocumentOrPortal', 'allRemInFolderQueue',
|
|
45
|
+
// 系统
|
|
46
|
+
'schemaVersion', 'embeddedQueueViewMode',
|
|
47
|
+
'isPowerup', 'isPowerupEnum', 'isPowerupProperty',
|
|
48
|
+
'isPowerupPropertyListItem', 'isPowerupSlot',
|
|
49
|
+
]);
|
|
28
50
|
/** 可写字段白名单 — 21 个可写字段 */
|
|
29
51
|
const WRITABLE_FIELDS = new Set([
|
|
30
52
|
'text', 'backText', 'type', 'isDocument', 'parent',
|
|
@@ -58,19 +80,34 @@ export class EditHandler {
|
|
|
58
80
|
if (!cachedObj) {
|
|
59
81
|
throw new Error(`Rem ${remId} has not been read yet. Read it first before editing.`);
|
|
60
82
|
}
|
|
61
|
-
// ── 防线 2:
|
|
83
|
+
// ── 防线 2: 乐观并发检测(三层字段比较) ──
|
|
62
84
|
const currentRemObject = await this.forwardToPlugin('read_rem', { remId });
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
const diff = diffFields(cachedObj, currentRemObject);
|
|
68
|
-
console.error(`[defense-2] Rem ${remId} conflict detected. Changed fields: ${diff.join(', ') || '(JSON differs but no top-level field diff — possible nested change)'}`);
|
|
69
|
-
// 不更新缓存 — 迫使 AI re-read
|
|
85
|
+
const { semantic, warned, ignored } = classifyDiff(cachedObj, currentRemObject);
|
|
86
|
+
// 第1层:语义字段冲突 → 硬拒绝
|
|
87
|
+
if (semantic.length > 0) {
|
|
88
|
+
console.error(`[defense-2] Rem ${remId} conflict. Changed semantic fields: ${semantic.join(', ')}`);
|
|
70
89
|
throw new Error(`Rem ${remId} has been modified since last read. Please read it again before editing.`);
|
|
71
90
|
}
|
|
91
|
+
// 第2层+第3层:元数据变化 → 放行,收集警告
|
|
92
|
+
const defense2Warnings = [];
|
|
93
|
+
if (warned.length > 0) {
|
|
94
|
+
const cachedParent = cachedObj.parent;
|
|
95
|
+
const currentParent = currentRemObject.parent;
|
|
96
|
+
defense2Warnings.push(`⚠️ parent has changed (was: ${cachedParent}, now: ${currentParent}). ` +
|
|
97
|
+
`The Rem has been moved to a different parent since last read. Proceeding with edit.`);
|
|
98
|
+
console.error(`[defense-2] Rem ${remId} parent changed: ${cachedParent} → ${currentParent}`);
|
|
99
|
+
}
|
|
100
|
+
if (ignored.length > 0) {
|
|
101
|
+
defense2Warnings.push(`ℹ️ Metadata fields changed since last read: ${ignored.join(', ')}. ` +
|
|
102
|
+
`This is expected after structural operations. Proceeding with edit.`);
|
|
103
|
+
console.error(`[defense-2] Rem ${remId} metadata drift: ${ignored.join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
// 静默刷新缓存(保持新鲜度)
|
|
106
|
+
if (warned.length > 0 || ignored.length > 0) {
|
|
107
|
+
this.cache.set('rem:' + remId, currentRemObject);
|
|
108
|
+
}
|
|
72
109
|
// ── 遍历 changes keys:分类过滤 ──
|
|
73
|
-
const warnings = [];
|
|
110
|
+
const warnings = [...defense2Warnings];
|
|
74
111
|
const writableChanges = {};
|
|
75
112
|
for (const key of Object.keys(changes)) {
|
|
76
113
|
if (READ_ONLY_FIELDS.has(key)) {
|
|
@@ -129,14 +166,24 @@ export class EditHandler {
|
|
|
129
166
|
};
|
|
130
167
|
}
|
|
131
168
|
}
|
|
132
|
-
/**
|
|
133
|
-
function
|
|
169
|
+
/** 三层字段分类比较 — 防线2核心逻辑 */
|
|
170
|
+
function classifyDiff(cached, current) {
|
|
134
171
|
const allKeys = new Set([...Object.keys(cached), ...Object.keys(current)]);
|
|
135
|
-
const
|
|
172
|
+
const semantic = [];
|
|
173
|
+
const warned = [];
|
|
174
|
+
const ignored = [];
|
|
136
175
|
for (const key of allKeys) {
|
|
137
|
-
if (JSON.stringify(cached[key])
|
|
138
|
-
|
|
176
|
+
if (JSON.stringify(cached[key]) === JSON.stringify(current[key]))
|
|
177
|
+
continue;
|
|
178
|
+
if (DEFENSE2_WARN_FIELDS.has(key)) {
|
|
179
|
+
warned.push(key);
|
|
180
|
+
}
|
|
181
|
+
else if (DEFENSE2_IGNORE_FIELDS.has(key)) {
|
|
182
|
+
ignored.push(key);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
semantic.push(key);
|
|
139
186
|
}
|
|
140
187
|
}
|
|
141
|
-
return
|
|
188
|
+
return { semantic, warned, ignored };
|
|
142
189
|
}
|
|
@@ -172,7 +172,9 @@ export class TreeEditHandler {
|
|
|
172
172
|
else {
|
|
173
173
|
// ── 普通 Rem 创建路径 ──
|
|
174
174
|
// 解析 Markdown 前缀 + 箭头分隔符 → 属性
|
|
175
|
-
const { cleanContent, powerups, backText, practiceDirection } = parsePowerupPrefix(op.content);
|
|
175
|
+
const { cleanContent, powerups, backText, practiceDirection, warnings: prefixWarnings } = parsePowerupPrefix(op.content);
|
|
176
|
+
if (prefixWarnings?.length)
|
|
177
|
+
templateWarnings.push(...prefixWarnings);
|
|
176
178
|
const createResult = await this.forwardToPlugin('create_rem', {
|
|
177
179
|
content: cleanContent,
|
|
178
180
|
parentId,
|
|
@@ -141,6 +141,7 @@ export function parseMetadata(metadataStr) {
|
|
|
141
141
|
*/
|
|
142
142
|
export function parsePowerupPrefix(rawContent) {
|
|
143
143
|
const powerups = {};
|
|
144
|
+
const warnings = [];
|
|
144
145
|
if (rawContent === '---') {
|
|
145
146
|
return { cleanContent: '', powerups: { addPowerup: 'dv' } };
|
|
146
147
|
}
|
|
@@ -168,6 +169,20 @@ export function parsePowerupPrefix(rawContent) {
|
|
|
168
169
|
powerups.isTodo = true;
|
|
169
170
|
content = content.slice(6);
|
|
170
171
|
}
|
|
172
|
+
// Quote(引用块)
|
|
173
|
+
if (content.startsWith('> ')) {
|
|
174
|
+
powerups.isQuote = true;
|
|
175
|
+
content = content.slice(2);
|
|
176
|
+
}
|
|
177
|
+
// ListItem(有序列表)— 容错 1-9 开头,归一化为 isListItem
|
|
178
|
+
const listItemMatch = content.match(/^([1-9])\. /);
|
|
179
|
+
if (listItemMatch) {
|
|
180
|
+
powerups.isListItem = true;
|
|
181
|
+
content = content.slice(listItemMatch[0].length);
|
|
182
|
+
if (listItemMatch[1] !== '1') {
|
|
183
|
+
warnings.push(`有序列表前缀 "${listItemMatch[0]}" 已归一化为 isListItem(RemNote 自动编号,请统一使用 "1. ")`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
171
186
|
// Code
|
|
172
187
|
if (content.startsWith('`') && content.endsWith('`') && content.length >= 2) {
|
|
173
188
|
powerups.isCode = true;
|
|
@@ -231,6 +246,8 @@ export function parsePowerupPrefix(rawContent) {
|
|
|
231
246
|
result.practiceDirection = practiceDirection;
|
|
232
247
|
if (isMultiline !== undefined)
|
|
233
248
|
result.isMultiline = isMultiline;
|
|
249
|
+
if (warnings.length > 0)
|
|
250
|
+
result.warnings = warnings;
|
|
234
251
|
return result;
|
|
235
252
|
}
|
|
236
253
|
// ────────────────────────── Multiline 检测 ──────────────────────────
|
package/dist/cli/main.js
CHANGED
|
@@ -249,7 +249,7 @@ program
|
|
|
249
249
|
program
|
|
250
250
|
.command('read-context [jsonStr]')
|
|
251
251
|
.description('读取当前上下文视图(focus 鱼眼 / page 页面)')
|
|
252
|
-
.option('--mode <mode>', '模式:
|
|
252
|
+
.option('--mode <mode>', '模式:page(默认)或 focus')
|
|
253
253
|
.option('--ancestor-levels <levels>', '向上追溯几层祖先(默认 2,仅 focus 模式)')
|
|
254
254
|
.option('--depth <depth>', '展开深度(默认 3,仅 page 模式)')
|
|
255
255
|
.option('--max-nodes <maxNodes>', '全局节点上限(默认 200)')
|
|
@@ -281,8 +281,8 @@ export class ConfigServer {
|
|
|
281
281
|
<div class="field">
|
|
282
282
|
<label>模式 (readContextMode)</label>
|
|
283
283
|
<select id="readContextMode">
|
|
284
|
-
<option value="focus">focus</option>
|
|
285
284
|
<option value="page">page</option>
|
|
285
|
+
<option value="focus">focus</option>
|
|
286
286
|
</select>
|
|
287
287
|
</div>
|
|
288
288
|
<div class="field">
|
package/dist/mcp/instructions.js
CHANGED
|
@@ -238,7 +238,7 @@ disconnect → 关闭 daemon + headless Chrome,清空所有缓存,清除 hea
|
|
|
238
238
|
- 用户的描述与你已知信息对不上
|
|
239
239
|
- 搜索不到用户提到的内容
|
|
240
240
|
|
|
241
|
-
\`read_context
|
|
241
|
+
\`read_context\`:默认使用 **page 模式**——只需有打开的页面即可,几乎总能成功。仅当需要知道用户光标具体在哪个 Rem 上时,才显式传 \`mode="focus"\`(focus 模式要求用户光标停在某个 Rem 上,否则报错"当前没有聚焦的 Rem")。两者都返回面包屑路径。
|
|
242
242
|
|
|
243
243
|
### 场景 D:修改文本或属性
|
|
244
244
|
|
|
@@ -326,7 +326,9 @@ changes: { "type": "concept", "highlightColor": "Yellow", "fontSize": "H1" }
|
|
|
326
326
|
|
|
327
327
|
新增行(无 remId 注释的行)支持以下格式:
|
|
328
328
|
|
|
329
|
-
**Markdown 前缀**:\`# \` \`## \` \`### \` \`- [ ] \` \`- [x] \` \\\`code\\\` \`---\`
|
|
329
|
+
**Markdown 前缀**:\`# \` \`## \` \`### \` \`- [ ] \` \`- [x] \` \`> \`(引用块) \`1. \`(有序列表) \\\`code\\\` \`---\`
|
|
330
|
+
|
|
331
|
+
> **⚠️ 有序列表必须用 \`1. \` 前缀(Lazy Numbering)**:RemNote 有序列表采用 Lazy Numbering——所有列表项统一写 \`1. \`,RemNote 按层级自动编号(1./2./3./A./B./I./II.)。不要手动编号(如 \`2. \` \`3. \`)。\`2. \`~\`9. \` 会被容错处理(归一化为 isListItem 并返回 templateWarnings 警告),\`10. \` 及以上不会被识别为有序列表。
|
|
330
332
|
|
|
331
333
|
**箭头分隔符**:
|
|
332
334
|
- 单行:\`→\`(forward)\`←\`(backward)\`↔\`(both)——格式 \`text → backText\`
|
|
@@ -446,7 +448,7 @@ oldStr: " {{idZ}}" newStr: " {{idZ}}\\n 新行"
|
|
|
446
448
|
### 两道防线
|
|
447
449
|
|
|
448
450
|
1. **缓存存在**:必须有对应的 read 缓存
|
|
449
|
-
2.
|
|
451
|
+
2. **语义并发检测**(三层字段分类):edit 时重新读取最新数据并逐字段比较——语义字段(text/type/tags 等)变化 → 硬拒绝;parent 变化 → 放行 + warnings 返回 \`"⚠️ parent has changed (was: X, now: Y)..."\`;普通元数据(positionAmongstSiblings/updatedAt 等)变化 → 放行 + warnings 返回 \`"ℹ️ Metadata fields changed since last read: ..."\`。这意味着 \`edit_tree\` 移动/重排 Rem 后,可以直接 \`edit_rem\` 修改受影响节点,无需重新 read
|
|
450
452
|
|
|
451
453
|
### edit_tree 禁止事项
|
|
452
454
|
|
|
@@ -463,7 +465,9 @@ oldStr: " {{idZ}}" newStr: " {{idZ}}\\n 新行"
|
|
|
463
465
|
| 场景 | 缓存行为 | 重试策略 |
|
|
464
466
|
|:-----|:---------|:---------|
|
|
465
467
|
| edit_rem 写入成功 | 从 Plugin 重新读取 → 更新缓存 | 可继续编辑 |
|
|
466
|
-
| edit_rem
|
|
468
|
+
| edit_rem 仅元数据变化 | 静默刷新缓存并放行 | 可继续编辑(返回警告) |
|
|
469
|
+
| edit_rem 语义字段冲突 | 不更新缓存 | 必须重新 read_rem |
|
|
470
|
+
| edit_rem 部分写入失败 | 不更新缓存 | 必须重新 read_rem |
|
|
467
471
|
| edit_tree 成功 | 自动 re-read → 更新缓存 | 可连续 edit |
|
|
468
472
|
| edit_tree 防线 3 拒绝(str_replace 不匹配等) | 缓存保持不变 | 调整 oldStr/newStr 后直接重试 |
|
|
469
473
|
| edit_tree 执行中异常 | 已执行操作保留(**无回滚**),不更新缓存 | 必须重新 read_tree |
|
|
@@ -502,7 +506,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
|
|
|
502
506
|
\`\`\`
|
|
503
507
|
|
|
504
508
|
- 缩进:每级 2 空格
|
|
505
|
-
- 前缀:\`# \`(H1)、\`## \`(H2)、\`### \`(H3)、\`- [ ] \`(待办)、\`- [x] \`(已完成)、\\\`...\\\`(代码)、\`---\`(分隔线)
|
|
509
|
+
- 前缀:\`# \`(H1)、\`## \`(H2)、\`### \`(H3)、\`- [ ] \`(待办)、\`- [x] \`(已完成)、\`> \`(引用块)、\`1. \`(有序列表)、\\\`...\\\`(代码)、\`---\`(分隔线)
|
|
506
510
|
|
|
507
511
|
### 元数据标记
|
|
508
512
|
|
|
@@ -537,7 +541,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
|
|
|
537
541
|
新增行(无 remId 注释的行)在 newStr 中出现时,会被创建为新的 Rem。格式选项:
|
|
538
542
|
|
|
539
543
|
- 纯文本行:\`新内容\`
|
|
540
|
-
- 带前缀:\`# 新标题\`、\`- [ ]
|
|
544
|
+
- 带前缀:\`# 新标题\`、\`- [ ] 新待办\`、\`> 引用内容\`、\`1. 列表项\`
|
|
541
545
|
- 带箭头:\`问题 → 答案\`、\`概念 ↔ 定义\`、\`题目 ↓\`
|
|
542
546
|
- 带元数据注释(metadata-only,无 remId):\`新行 <!--type:concept doc-->\`
|
|
543
547
|
- Portal 行:\`<!--portal refs:id1,id2-->\`
|
|
@@ -628,7 +632,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
|
|
|
628
632
|
- 超链接必须用 \`iUrl\`,\`url\` 字段已废弃无效
|
|
629
633
|
- RichText 对象内部按 **key 字母序排列**(\`_id\` < \`b\` < \`cId\` < \`h\` < \`i\` < \`iUrl\` < \`text\`),确保序列化一致性
|
|
630
634
|
- \`highlightColor\`(RemObject 顶层,字符串 \`"Red"\`)与 \`h\`(RichText 内部,数字 \`1\`)完全独立——前者是整行背景色,后者是文字片段荧光底色
|
|
631
|
-
- 防线 2
|
|
635
|
+
- 防线 2(语义并发检测)依赖 key 字母序的确定性序列化来比较语义字段
|
|
632
636
|
|
|
633
637
|
---
|
|
634
638
|
|
|
@@ -643,7 +647,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
|
|
|
643
647
|
├─ "Plugin 未连接" → RemNote 未打开或插件未加载 → 引导用户操作 RemNote
|
|
644
648
|
├─ "SDK 未就绪" → 知识库尚未加载 → 等待并重试 health
|
|
645
649
|
├─ "has not been read yet" → 未先 read → 执行对应 read 后重试
|
|
646
|
-
├─ "has been modified since last read" →
|
|
650
|
+
├─ "has been modified since last read" → 语义字段被外部修改 → 必须重新 read(不可直接重试)
|
|
647
651
|
├─ "Invalid value" → 枚举字段值不合法 → 检查允许的值范围
|
|
648
652
|
├─ "old_str not found" → oldStr 不精确 → 检查缩进、空格、换行
|
|
649
653
|
├─ "old_str matches N locations" → oldStr 不够具体 → 扩大范围包含更多上下文
|
|
@@ -28,13 +28,17 @@ export function registerEditTools(server) {
|
|
|
28
28
|
'\\n- fontSize: H1 / H2 / H3 / null(恢复普通)' +
|
|
29
29
|
'\\n- todoStatus: Finished / Unfinished / null(需先 isTodo=true)' +
|
|
30
30
|
'\\n\\nhighlightColor vs RichText h 字段:两者完全独立。highlightColor 是整行背景色(字符串如 "Red");h 是 RichText 元素内部的行内荧光底色(数字:1=Red, 2=Orange, 3=Yellow, 4=Green, 5=Purple, 6=Blue, 7=Gray, 8=Brown, 9=Pink)。' +
|
|
31
|
-
'\\n\\n输出格式:JSON 对象,包含 changes(已写入的字段名数组)和 warnings
|
|
31
|
+
'\\n\\n输出格式:JSON 对象,包含 changes(已写入的字段名数组)和 warnings(警告数组,可能包含防线2元数据警告和字段校验警告)。' +
|
|
32
32
|
'\\n\\n两道防线:' +
|
|
33
33
|
'\\n1. 缓存存在检查——未先 read_rem 则报 "has not been read yet"' +
|
|
34
|
-
'\\n2.
|
|
34
|
+
'\\n2. 语义并发检测(三层字段分类)——edit 时重新读取并逐字段比较:' +
|
|
35
|
+
'\\n - 语义字段(text/type/tags 等)变化 → 硬拒绝 "has been modified since last read"' +
|
|
36
|
+
'\\n - parent 变化 → 放行 + warnings 返回 "⚠️ parent has changed (was: X, now: Y)..."' +
|
|
37
|
+
'\\n - 普通元数据(positionAmongstSiblings/updatedAt 等)变化 → 放行 + warnings 返回 "ℹ️ Metadata fields changed since last read: ..."' +
|
|
38
|
+
'\\n 这意味着 edit_tree 移动/重排后可直接 edit_rem,无需重新 read' +
|
|
35
39
|
'\\n\\n常见错误:' +
|
|
36
40
|
'\\n- "has not been read yet" → 先 read_rem' +
|
|
37
|
-
'\\n- "has been modified since last read" →
|
|
41
|
+
'\\n- "has been modified since last read" → 语义字段被外部修改,重新 read_rem' +
|
|
38
42
|
'\\n- "Invalid value for \'field\'" → 检查枚举合法值' +
|
|
39
43
|
'\\n- "Field \'...\' is read-only/unknown and was ignored" → 警告不阻断' +
|
|
40
44
|
'\\n\\n关联工具:read_rem(前置读取)、edit_tree(子树结构编辑)',
|
|
@@ -71,7 +75,8 @@ export function registerEditTools(server) {
|
|
|
71
75
|
'\\n4. 重排:调换同级行的顺序' +
|
|
72
76
|
'\\n执行顺序:Create → Move → Reorder → Delete' +
|
|
73
77
|
'\\n\\n新增行格式:' +
|
|
74
|
-
'\\n- Markdown 前缀:# / ## / ### / - [ ] / - [x] / `代码` / ---' +
|
|
78
|
+
'\\n- Markdown 前缀:# / ## / ### / - [ ] / - [x] / > / 1. / `代码` / ---' +
|
|
79
|
+
'\\n ⚠️ 有序列表必须用 `1. `(Lazy Numbering):RemNote 自动编号,不要写 `2. ` `3. ` 等。`2.`~`9.` 会被容错归一化并返回 templateWarnings 警告,`10.` 及以上不识别为列表。' +
|
|
75
80
|
'\\n- 箭头分隔符(闪卡):→ ← ↔(单行)、↓ ↑ ↕(多行,带 backText 或子节点为答案)' +
|
|
76
81
|
'\\n- 元数据注释(可选):<!--type:concept--> <!--doc--> <!--tag:Name(id)--> 可组合' +
|
|
77
82
|
'\\n- Portal 创建:<!--portal refs:id1,id2--> 或 <!--portal-->(空 Portal)' +
|
|
@@ -329,11 +329,15 @@ export function registerReadTools(server) {
|
|
|
329
329
|
'\\n\\n重要:用户正在看的页面对 AI 不可见。当用户说"这个"、"当前页面"、"这里",或描述与已知信息对不上时,必须主动调用 read_context 对齐信息。' +
|
|
330
330
|
'\\n\\n适用场景:了解用户当前焦点位置或打开的页面;用户说"我现在在看什么"、"当前页面是什么"时使用;需要上下文才能理解用户指代时使用。' +
|
|
331
331
|
'\\n不适用场景:查看特定 Rem(已知 remId 用 read_tree);搜索内容(用 search)。' +
|
|
332
|
-
'\\n\\n前置条件:daemon 已连接。focus
|
|
332
|
+
'\\n\\n前置条件:daemon 已连接。page 模式只需有打开的页面(几乎总满足);focus 模式需用户光标停在某个 Rem 上或指定 focusRemId(否则报错"当前没有聚焦的 Rem")。' +
|
|
333
|
+
'\\n\\n模式选择指引:' +
|
|
334
|
+
'\\n- 绝大多数场景用 page(默认)——用户通常只是打开页面浏览,不会特意点击某个 Rem,page 更可靠' +
|
|
335
|
+
'\\n- 仅当需要知道用户光标具体在哪个 Rem 上时才用 focus——如用户说"我正在编辑的这个"、"光标所在的 Rem"' +
|
|
336
|
+
'\\n- 不确定时用 page——最坏情况只是展开整个页面;focus 的最坏情况是报错' +
|
|
333
337
|
'\\n\\n参数说明:' +
|
|
334
|
-
'\\n- mode(可选,默认 "
|
|
338
|
+
'\\n- mode(可选,默认 "page"):视图模式' +
|
|
339
|
+
'\\n - page:以当前打开的页面为根,均匀展开子树(推荐,可靠性高)' +
|
|
335
340
|
'\\n - focus:以焦点 Rem 为中心的鱼眼视图。焦点完全展开(depth=3),siblings 浅层预览(depth=1,前3个children可见),叔伯不展开。焦点行以 * 前缀标记' +
|
|
336
|
-
'\\n - page:以当前打开的页面为根,均匀展开子树' +
|
|
337
341
|
'\\n- focusRemId(可选,仅 focus 模式):指定任意 Rem 作为鱼眼中心,此时不依赖用户实际焦点。page 模式下传入会报错' +
|
|
338
342
|
'\\n- ancestorLevels(可选,默认 2,仅 focus 模式生效):从焦点向上追溯几层祖先作为上下文起点' +
|
|
339
343
|
'\\n- depth(可选,默认 3,仅 page 模式生效):向下展开深度(-1 无限)' +
|
|
@@ -352,7 +356,7 @@ export function registerReadTools(server) {
|
|
|
352
356
|
mode: z
|
|
353
357
|
.enum(['focus', 'page'])
|
|
354
358
|
.optional()
|
|
355
|
-
.describe('视图模式:focus
|
|
359
|
+
.describe('视图模式:page(页面)或 focus(聚焦),默认 page'),
|
|
356
360
|
ancestorLevels: z
|
|
357
361
|
.number()
|
|
358
362
|
.optional()
|