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 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 focus in RemNote
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/page context view | No |
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({ ok: true, command: 'edit-tree', operations: data.operations });
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(默认 focus
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 || 'focus';
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 = { mode, ancestorLevels, depth, maxNodes, maxSiblings };
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);
@@ -26,7 +26,7 @@ export const DEFAULT_DEFAULTS = {
26
26
  readTreeIncludePowerup: false,
27
27
  readRemInTreeMaxNodes: 50,
28
28
  readGlobeDepth: -1,
29
- readContextMode: 'focus',
29
+ readContextMode: 'page',
30
30
  readContextAncestorLevels: 2,
31
31
  readContextDepth: 3,
32
32
  searchNumResults: 20,
@@ -5,7 +5,7 @@
5
5
  * Plugin 只负责原子写入(write_rem_fields)。
6
6
  *
7
7
  * 防线 1:缓存存在性检查(必须先 read 再 edit)
8
- * 防线 2:乐观并发检测(当前 JSON 与缓存 JSON 比较)
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 currentJson = JSON.stringify(currentRemObject, null, 2);
64
- const cachedJson = JSON.stringify(cachedObj, null, 2);
65
- if (currentJson !== cachedJson) {
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
- /** 比较两个 RemObject 的顶层字段,返回值不同的 key 列表 */
133
- function diffFields(cached, current) {
169
+ /** 三层字段分类比较 防线2核心逻辑 */
170
+ function classifyDiff(cached, current) {
134
171
  const allKeys = new Set([...Object.keys(cached), ...Object.keys(current)]);
135
- const changed = [];
172
+ const semantic = [];
173
+ const warned = [];
174
+ const ignored = [];
136
175
  for (const key of allKeys) {
137
- if (JSON.stringify(cached[key]) !== JSON.stringify(current[key])) {
138
- changed.push(key);
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 changed;
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>', '模式:focus(默认)或 page')
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">
@@ -238,7 +238,7 @@ disconnect → 关闭 daemon + headless Chrome,清空所有缓存,清除 hea
238
238
  - 用户的描述与你已知信息对不上
239
239
  - 搜索不到用户提到的内容
240
240
 
241
- \`read_context\`:focus 模式(默认)以用户焦点为中心构建鱼眼视图;page 模式以当前页面为根展开。两者都返回面包屑路径。
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. **并发检测**:edit 时重新读取最新数据与缓存比较,Rem 被外部修改则拒绝——必须重新 read
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 防线拒绝/部分失败 | 不更新缓存 | 必须重新 read_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(乐观并发检测)依赖 key 字母序的确定性序列化来比较缓存与最新数据
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" → 被外部修改 → 必须重新 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. 并发检测——edit 时重新从 Plugin 读取并对比缓存,不一致则报 "has been modified since last read"。防线拒绝/部分失败时不更新缓存,迫使重新 read_rem' +
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" → 重新 read_rem' +
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 模式需用户在 RemNote 中有焦点 Rem(光标在某个 Rem 上)或指定 focusRemId;page 模式需有打开的页面。' +
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(可选,默认 "focus"):视图模式' +
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(聚焦)或 page(页面),默认 focus'),
359
+ .describe('视图模式:page(页面)或 focus(聚焦),默认 page'),
356
360
  ancestorLevels: z
357
361
  .number()
358
362
  .optional()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remnote-bridge",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "RemNote 自动化桥接工具集:CLI + MCP Server + Plugin",
5
5
  "type": "module",
6
6
  "bin": {