remnote-bridge 0.1.16 → 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 并发检测的假阳性
@@ -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
  }
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
 
@@ -448,7 +448,7 @@ oldStr: " {{idZ}}" newStr: " {{idZ}}\\n 新行"
448
448
  ### 两道防线
449
449
 
450
450
  1. **缓存存在**:必须有对应的 read 缓存
451
- 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
452
452
 
453
453
  ### edit_tree 禁止事项
454
454
 
@@ -465,7 +465,9 @@ oldStr: " {{idZ}}" newStr: " {{idZ}}\\n 新行"
465
465
  | 场景 | 缓存行为 | 重试策略 |
466
466
  |:-----|:---------|:---------|
467
467
  | edit_rem 写入成功 | 从 Plugin 重新读取 → 更新缓存 | 可继续编辑 |
468
- | edit_rem 防线拒绝/部分失败 | 不更新缓存 | 必须重新 read_rem |
468
+ | edit_rem 仅元数据变化 | 静默刷新缓存并放行 | 可继续编辑(返回警告) |
469
+ | edit_rem 语义字段冲突 | 不更新缓存 | 必须重新 read_rem |
470
+ | edit_rem 部分写入失败 | 不更新缓存 | 必须重新 read_rem |
469
471
  | edit_tree 成功 | 自动 re-read → 更新缓存 | 可连续 edit |
470
472
  | edit_tree 防线 3 拒绝(str_replace 不匹配等) | 缓存保持不变 | 调整 oldStr/newStr 后直接重试 |
471
473
  | edit_tree 执行中异常 | 已执行操作保留(**无回滚**),不更新缓存 | 必须重新 read_tree |
@@ -630,7 +632,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
630
632
  - 超链接必须用 \`iUrl\`,\`url\` 字段已废弃无效
631
633
  - RichText 对象内部按 **key 字母序排列**(\`_id\` < \`b\` < \`cId\` < \`h\` < \`i\` < \`iUrl\` < \`text\`),确保序列化一致性
632
634
  - \`highlightColor\`(RemObject 顶层,字符串 \`"Red"\`)与 \`h\`(RichText 内部,数字 \`1\`)完全独立——前者是整行背景色,后者是文字片段荧光底色
633
- - 防线 2(乐观并发检测)依赖 key 字母序的确定性序列化来比较缓存与最新数据
635
+ - 防线 2(语义并发检测)依赖 key 字母序的确定性序列化来比较语义字段
634
636
 
635
637
  ---
636
638
 
@@ -645,7 +647,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
645
647
  ├─ "Plugin 未连接" → RemNote 未打开或插件未加载 → 引导用户操作 RemNote
646
648
  ├─ "SDK 未就绪" → 知识库尚未加载 → 等待并重试 health
647
649
  ├─ "has not been read yet" → 未先 read → 执行对应 read 后重试
648
- ├─ "has been modified since last read" → 被外部修改 → 必须重新 read(不可直接重试)
650
+ ├─ "has been modified since last read" → 语义字段被外部修改 → 必须重新 read(不可直接重试)
649
651
  ├─ "Invalid value" → 枚举字段值不合法 → 检查允许的值范围
650
652
  ├─ "old_str not found" → oldStr 不精确 → 检查缩进、空格、换行
651
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(子树结构编辑)',
@@ -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.16",
3
+ "version": "0.1.17",
4
4
  "description": "RemNote 自动化桥接工具集:CLI + MCP Server + Plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,7 +57,7 @@ Add a <Suspense fallback=...> component higher in the tree to provide a loading
57
57
  * LICENSE file in the root directory of this source tree.
58
58
  */var qe,Ot,De,it;if(typeof performance=="object"&&typeof performance.now=="function"){var B=performance;K.unstable_now=function(){return B.now()}}else{var wn=Date,Et=wn.now();K.unstable_now=function(){return wn.now()-Et}}if(typeof window>"u"||typeof MessageChannel!="function"){var Tt=null,b=null,Y=function(){if(Tt!==null)try{var L=K.unstable_now();Tt(!0,L),Tt=null}catch(A){throw setTimeout(Y,0),A}};qe=function(L){Tt!==null?setTimeout(qe,0,L):(Tt=L,setTimeout(Y,0))},Ot=function(L,A){b=setTimeout(L,A)},De=function(){clearTimeout(b)},K.unstable_shouldYield=function(){return!1},it=K.unstable_forceFrameRate=function(){}}else{var c=window.setTimeout,Mt=window.clearTimeout;if(typeof console<"u"){var ht=window.cancelAnimationFrame;typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof ht!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")}var He=!1,at=null,Pt=-1,tt=5,ot=0;K.unstable_shouldYield=function(){return K.unstable_now()>=ot},it=function(){},K.unstable_forceFrameRate=function(L){0>L||125<L?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):tt=0<L?Math.floor(1e3/L):5};var te=new MessageChannel,ae=te.port2;te.port1.onmessage=function(){if(at!==null){var L=K.unstable_now();ot=L+tt;try{at(!0,L)?ae.postMessage(null):(He=!1,at=null)}catch(A){throw ae.postMessage(null),A}}else He=!1},qe=function(L){at=L,He||(He=!0,ae.postMessage(null))},Ot=function(L,A){Pt=c(function(){L(K.unstable_now())},A)},De=function(){Mt(Pt),Pt=-1}}function _e(L,A){var $=L.length;L.push(A);e:for(;;){var H=$-1>>>1,ne=L[H];if(ne!==void 0&&0<st(ne,A))L[H]=A,L[$]=ne,$=H;else break e}}function Oe(L){return L=L[0],L===void 0?null:L}function $e(L){var A=L[0];if(A!==void 0){var $=L.pop();if($!==A){L[0]=$;e:for(var H=0,ne=L.length;H<ne;){var U=2*(H+1)-1,ye=L[U],M=U+1,ln=L[M];if(ye!==void 0&&0>st(ye,$))ln!==void 0&&0>st(ln,ye)?(L[H]=ln,L[M]=$,H=M):(L[H]=ye,L[U]=$,H=U);else if(ln!==void 0&&0>st(ln,$))L[H]=ln,L[M]=$,H=M;else break e}}return A}return null}function st(L,A){var $=L.sortIndex-A.sortIndex;return $!==0?$:L.id-A.id}var Z=[],Ze=[],Ye=1,ue=null,fe=3,ee=!1,ie=!1,me=!1;function q(L){for(var A=Oe(Ze);A!==null;){if(A.callback===null)$e(Ze);else if(A.startTime<=L)$e(Ze),A.sortIndex=A.expirationTime,_e(Z,A);else break;A=Oe(Ze)}}function ge(L){if(me=!1,q(L),!ie)if(Oe(Z)!==null)ie=!0,qe(C);else{var A=Oe(Ze);A!==null&&Ot(ge,A.startTime-L)}}function C(L,A){ie=!1,me&&(me=!1,De()),ee=!0;var $=fe;try{for(q(A),ue=Oe(Z);ue!==null&&(!(ue.expirationTime>A)||L&&!K.unstable_shouldYield());){var H=ue.callback;if(typeof H=="function"){ue.callback=null,fe=ue.priorityLevel;var ne=H(ue.expirationTime<=A);A=K.unstable_now(),typeof ne=="function"?ue.callback=ne:ue===Oe(Z)&&$e(Z),q(A)}else $e(Z);ue=Oe(Z)}if(ue!==null)var U=!0;else{var ye=Oe(Ze);ye!==null&&Ot(ge,ye.startTime-A),U=!1}return U}finally{ue=null,fe=$,ee=!1}}var V=it;K.unstable_IdlePriority=5,K.unstable_ImmediatePriority=1,K.unstable_LowPriority=4,K.unstable_NormalPriority=3,K.unstable_Profiling=null,K.unstable_UserBlockingPriority=2,K.unstable_cancelCallback=function(L){L.callback=null},K.unstable_continueExecution=function(){ie||ee||(ie=!0,qe(C))},K.unstable_getCurrentPriorityLevel=function(){return fe},K.unstable_getFirstCallbackNode=function(){return Oe(Z)},K.unstable_next=function(L){switch(fe){case 1:case 2:case 3:var A=3;break;default:A=fe}var $=fe;fe=A;try{return L()}finally{fe=$}},K.unstable_pauseExecution=function(){},K.unstable_requestPaint=V,K.unstable_runWithPriority=function(L,A){switch(L){case 1:case 2:case 3:case 4:case 5:break;default:L=3}var $=fe;fe=L;try{return A()}finally{fe=$}},K.unstable_scheduleCallback=function(L,A,$){var H=K.unstable_now();switch(typeof $=="object"&&$!==null?($=$.delay,$=typeof $=="number"&&0<$?H+$:H):$=H,L){case 1:var ne=-1;break;case 2:ne=250;break;case 5:ne=1073741823;break;case 4:ne=1e4;break;default:ne=5e3}return ne=$+ne,L={id:Ye++,callback:A,priorityLevel:L,startTime:$,expirationTime:ne,sortIndex:-1},$>H?(L.sortIndex=$,_e(Ze,L),Oe(Z)===null&&L===Oe(Ze)&&(me?De():me=!0,Ot(ge,$-H))):(L.sortIndex=ne,_e(Z,L),ie||ee||(ie=!0,qe(C))),L},K.unstable_wrapCallback=function(L){var A=fe;return function(){var $=fe;fe=A;try{return L.apply(this,arguments)}finally{fe=$}}}},825(tn,K,qe){"use strict";tn.exports=qe(742)}},vd={};function Ac(tn){var K=vd[tn];if(K!==void 0)return K.exports;var qe=vd[tn]={exports:{}};return Pf[tn](qe,qe.exports,Ac),qe.exports}Ac.g=(function(){if(typeof globalThis=="object")return globalThis;try{return this||new Function("return this")()}catch{if(typeof window=="object")return window}})();var wp={};(()=>{"use strict";var tn=Ac(216);const K="0.2.1",qe=[29100,29110,29120,29130],Ot=18e3;var De=Object.defineProperty,it=(w,y,E)=>y in w?De(w,y,{enumerable:!0,configurable:!0,writable:!0,value:E}):w[y]=E,B=(w,y,E)=>it(w,typeof y!="symbol"?y+"":y,E);const wn=4e3,Et=4003;class Tt{constructor(y){B(this,"ws",null),B(this,"reconnectAttempts",0),B(this,"reconnectTimeout",null),B(this,"messageHandler",null),B(this,"status","disconnected"),B(this,"isShuttingDown",!1),B(this,"isPreempted",!1),B(this,"_sdkReady"),B(this,"config"),this._sdkReady=y.sdkReady,this.config={url:y.url,pluginVersion:y.pluginVersion,sdkReady:y.sdkReady,twinSlotIndex:y.twinSlotIndex,isTwinConnection:y.isTwinConnection??!1,maxReconnectAttempts:y.maxReconnectAttempts??10,initialReconnectDelay:y.initialReconnectDelay??1e3,maxReconnectDelay:y.maxReconnectDelay??3e4,onStatusChange:y.onStatusChange,onLog:y.onLog,onPreempted:y.onPreempted,onTwinOccupied:y.onTwinOccupied,onOtherOccupied:y.onOtherOccupied}}log(y,E="info"){this.config.onLog?.(y,E)}setStatus(y){this.status!==y&&(this.status=y,this.config.onStatusChange?.(y))}sendHello(){const y={type:"hello",version:this.config.pluginVersion,sdkReady:this._sdkReady,twinSlotIndex:this.config.twinSlotIndex};try{this.ws?.send(JSON.stringify(y)),this.log(`\u53D1\u9001 hello\uFF08v${this.config.pluginVersion}, sdkReady=${this._sdkReady}, twinSlot=${this.config.twinSlotIndex}\uFF09`)}catch(E){this.log(`\u53D1\u9001 hello \u5931\u8D25: ${E}`,"warn")}}connect(){if(!(this.ws?.readyState===WebSocket.OPEN||this.ws?.readyState===WebSocket.CONNECTING)){this.isShuttingDown=!1,this.isPreempted=!1,this.setStatus("connecting");try{this.ws=new WebSocket(this.config.url),this.ws.onopen=()=>{this.log("\u5DF2\u8FDE\u63A5\u5230\u5B88\u62A4\u8FDB\u7A0B"),this.reconnectAttempts=0,this.setStatus("connected"),this.sendHello()},this.ws.onmessage=async y=>{await this.handleMessage(typeof y.data=="string"?y.data:String(y.data))},this.ws.onclose=y=>{y.code!==1006&&this.log(`\u8FDE\u63A5\u65AD\u5F00: ${y.code} ${y.reason}`,"warn"),this.setStatus("disconnected"),y.code===Et?this.config.onTwinOccupied?.():y.code===wn&&this.config.onOtherOccupied?.(),this.isShuttingDown||this.scheduleReconnect()},this.ws.onerror=()=>{}}catch(y){this.log(`\u8FDE\u63A5\u5931\u8D25: ${y}`,"error"),this.setStatus("disconnected"),this.scheduleReconnect()}}}async handleMessage(y){try{const E=JSON.parse(y);if(E.type==="preempted"){this.isPreempted=!0,this.log(`\u88AB\u5B6A\u751F Plugin \u62A2\u5360: ${E.reason}`,"warn"),this.config.onPreempted?.();return}if(E.type==="ping"){this.ws?.send(JSON.stringify({type:"pong"}));return}if(E.id&&E.action&&this.messageHandler){const R=E;this.log(`\u6536\u5230\u8BF7\u6C42: ${R.action}`);try{const N=await this.messageHandler(R),F={id:R.id,result:N};this.ws?.send(JSON.stringify(F)),this.log(`\u5B8C\u6210: ${R.action}`)}catch(N){const F=N instanceof Error?N.message:String(N),Q={id:R.id,error:F};this.ws?.send(JSON.stringify(Q)),this.log(`\u5931\u8D25: ${R.action} - ${F}`,"error")}}}catch(E){this.log(`\u5904\u7406\u6D88\u606F\u5931\u8D25: ${E}`,"error")}}scheduleReconnect(){if(this.isShuttingDown||this.isPreempted||!this.config.isTwinConnection)return;if(this.reconnectAttempts>=this.config.maxReconnectAttempts){this.log("\u5DF2\u8FBE\u6700\u5927\u91CD\u8FDE\u6B21\u6570","error");return}const y=Math.min(this.config.initialReconnectDelay*Math.pow(2,this.reconnectAttempts),this.config.maxReconnectDelay),E=Math.random()*.3*y,R=y+E;this.reconnectAttempts++,this.log(`${Math.round(R)}ms \u540E\u91CD\u8FDE\uFF08\u7B2C ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} \u6B21\uFF09`),this.reconnectTimeout=setTimeout(()=>{this.connect()},R)}setMessageHandler(y){this.messageHandler=y}disconnect(){this.isShuttingDown=!0,this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null),this.ws&&(this.ws.close(1e3,"Plugin disconnect"),this.ws=null),this.setStatus("disconnected")}getStatus(){return this.status}}var b=Object.defineProperty,Y=(w,y,E)=>y in w?b(w,y,{enumerable:!0,configurable:!0,writable:!0,value:E}):w[y]=E,c=(w,y,E)=>Y(w,typeof y!="symbol"?y+"":y,E);class Mt{constructor(y){c(this,"clients",[]),c(this,"slotStates",[]),c(this,"scanTimer",null),c(this,"config"),this.config=y;for(let E=0;E<qe.length;E++){const R=E===y.twinSlotIndex;this.slotStates.push({slotIndex:E,wsPort:qe[E],status:"disconnected",isTwin:R,disconnectReason:"not_started"}),this.clients.push(new Tt({url:`ws://127.0.0.1:${qe[E]}`,pluginVersion:y.pluginVersion,sdkReady:y.sdkReady,twinSlotIndex:y.twinSlotIndex,isTwinConnection:R,maxReconnectAttempts:R?10:0,initialReconnectDelay:1e3,maxReconnectDelay:3e4,onStatusChange:N=>this.handleStatusChange(E,N),onLog:(N,F)=>y.onLog(E,N,F),onPreempted:()=>this.handleDisconnectReason(E,"preempted"),onTwinOccupied:()=>this.handleDisconnectReason(E,"twin_occupied"),onOtherOccupied:()=>this.handleDisconnectReason(E,"other_occupied")}))}}setMessageHandler(y){for(const E of this.clients)E.setMessageHandler(y)}start(){const y=this.config.twinSlotIndex;this.clients[y].connect(),setTimeout(()=>{for(let E=0;E<this.clients.length;E++)E!==y&&this.clients[E].connect()},2e3),this.scanTimer=setInterval(()=>this.scanAndReconnect(),Ot)}stop(){this.scanTimer&&(clearInterval(this.scanTimer),this.scanTimer=null);for(const y of this.clients)y.disconnect()}getSlots(){return this.slotStates.slice()}handleStatusChange(y,E){const R=this.slotStates[y];R.status=E,E==="connected"&&(R.disconnectReason=null),this.notifySlotsChange()}handleDisconnectReason(y,E){this.slotStates[y].disconnectReason=E,this.notifySlotsChange()}notifySlotsChange(){this.config.onSlotsChange(this.slotStates.slice())}scanAndReconnect(){for(let y=0;y<this.clients.length;y++){const E=this.slotStates[y];E.isTwin||E.status==="connected"||E.status==="connecting"||this.clients[y].connect()}}}async function ht(w){const[y,E,R,N]=await Promise.all([w.isPowerupProperty(),w.isPowerupSlot(),w.isPowerupPropertyListItem(),w.isPowerupEnum()]);return y||E||R||N}async function He(w){const y=await Promise.all(w.map(ht));return w.filter((E,R)=>!y[R])}async function at(w){const y=await Promise.all(w.map(E=>E.isPowerup()));return w.filter((E,R)=>!y[R])}async function Pt(w,y){try{return await w.richText.toMarkdown(y)}catch{return tt(y)}}function tt(w){return w.map(y=>{if(typeof y=="string")return y;if(typeof y!="object"||y===null)return"";const E=y;switch(E.i){case"m":return String(E.text??"");case"q":return`[[${String(E._id??"")}]]`;case"u":return E.title?`[${String(E.title)}](${String(E.url)})`:String(E.url??"");case"x":return`$${String(E.text??"")}$`;case"i":return`![image](${String(E.url??"")})`;case"a":return`[audio](${String(E.url??"")})`;case"p":return String(E.text??"");case"g":return String(E.text??"");case"n":return String(E.text??"");case"o":return String(E.text??"");case"s":case"fi":case"ai":return"";default:return String(E.text??E.url??"")}}).join("")}async function ot(w,y,E,R){const N=R?.includePowerup??!1,[F,Q,G,ve,Ae,Ne,he,mt,Me,Qe,wt,Pe,Be,Xe,nt]=await Promise.all([Pt(w,y.text??[]),y.backText?Pt(w,y.backText):Promise.resolve(null),y.getType(),y.isCardItem(),y.isDocument(),y.getTagRems().then(Je=>N?Je:at(Je).then(Ke=>{const Zt=Je.length-Ke.length;return Zt>0&&R?.onFilteredTags?.(Zt),Ke})),y.getPracticeDirection(),y.getFontSize(),y.isTodo(),y.getTodoStatus(),y.isCode(),y.isQuote(),y.isListItem(),y.hasPowerup("dv"),y.type===6?y.getPortalDirectlyIncludedRem():Promise.resolve([])]);let We=!1;E.length>0&&(We=(await Promise.all(E.map(Ke=>Ke.isCardItem()))).some(Boolean));const xt=Xe&&(y.text??[]).length===0,Fe=await Promise.all(Ne.map(async Je=>({id:Je._id,name:te(await Pt(w,Je.text??[]))})));return{id:y._id,markdownText:te(F),markdownBackText:Q!==null?te(Q):null,type:ae(G),hasMultilineChildren:We,practiceDirection:he??"none",isCardItem:ve,isDocument:Ae,isPortal:y.type===6,portalRefs:nt.map(Je=>Je._id),childrenCount:E.length,tags:Fe,fontSize:mt??null,isTodo:Me,todoStatus:Qe??null,isCode:wt,isQuote:Pe,isListItem:Be,isDivider:xt,isTopLevel:y.parent===null}}function te(w){return w.replace(/\n/g," ")}function ae(w){switch(w){case 1:return"concept";case 2:return"descriptor";case 6:return"portal";default:return"default"}}async function _e(w,y){const{includePowerup:E=!1}=y,R=await w.rem.findOne(y.remId);if(!R)throw new Error(`Rem not found: ${y.remId}`);return Oe(w,R,{includePowerup:E})}async function Oe(w,y,E){const{includePowerup:R=!1}=E,[N,F,Q,G,ve,Ae,Ne,he,mt,Me,Qe,wt,Pe,Be,Xe,nt,We,xt,Fe,Je,Ke,Zt,Ge,Kt,un,pn,Wt,ut,ct,Nt,Bt,hn,Dt,Jn,ur,So,er,Tr,_n,Sn,Dn,cr,xr]=await Promise.all([y.isDocument(),y.getFontSize(),y.getHighlightColor(),y.isTodo(),y.getTodoStatus(),y.isCode(),y.isQuote(),y.isListItem(),y.isCardItem(),y.isTable(),y.isSlot(),y.isProperty(),y.getEnablePractice(),y.getPracticeDirection(),y.getTagRems(),y.getSources(),y.getAliases(),y.positionAmongstSiblings(),y.isPowerup(),y.isPowerupEnum(),y.isPowerupProperty(),y.isPowerupPropertyListItem(),y.isPowerupSlot(),y.getPortalType(),y.getPortalDirectlyIncludedRem(),y.getPropertyType(),y.remsBeingReferenced(),y.deepRemsBeingReferenced(),y.remsReferencingThis(),y.taggedRem(),y.ancestorTagRem(),y.descendantTagRem(),y.getDescendants(),y.siblingRem(),y.portalsAndDocumentsIn(),y.allRemInDocumentOrPortal(),y.allRemInFolderQueue(),y.getChildrenRem(),y.timesSelectedInSearch(),y.getLastTimeMovedTo(),y.getSchemaVersion(),y.embeddedQueueViewMode(),y.getLastPracticed()]);let dr=Xe,Cr=y.children??[],mi=0,Mr=0;if(!R){dr=await at(Xe),mi=Xe.length-dr.length;const ze=await He(Tr);Mr=Tr.length-ze.length,Cr=ze.map(An=>An._id)}const fr={id:y._id,text:$e(y.text??[]),backText:y.backText?$e(y.backText):null,type:ae(y.type),isDocument:N,parent:y.parent,children:Cr,fontSize:F??null,highlightColor:Q??null,isTodo:G,todoStatus:ve??null,isCode:Ae,isQuote:Ne,isListItem:he,isCardItem:mt,isTable:!!Me,isSlot:Qe,isProperty:wt,isPowerup:Fe,isPowerupEnum:Je,isPowerupProperty:Ke,isPowerupPropertyListItem:Zt,isPowerupSlot:Ge,portalType:ae(y.type)==="portal"?st(Kt):null,portalDirectlyIncludedRem:un.map(ze=>ze._id).sort(),propertyType:pn??null,enablePractice:Pe,practiceDirection:Be,tags:dr.map(ze=>ze._id).sort(),sources:nt.map(ze=>ze._id).sort(),aliases:We.map(ze=>ze._id).sort(),remsBeingReferenced:Wt.map(ze=>ze._id).sort(),deepRemsBeingReferenced:ut.map(ze=>ze._id).sort(),remsReferencingThis:ct.map(ze=>ze._id).sort(),taggedRem:Nt.map(ze=>ze._id).sort(),ancestorTagRem:Bt.map(ze=>ze._id).sort(),descendantTagRem:hn.map(ze=>ze._id).sort(),descendants:Dt.map(ze=>ze._id).sort(),siblingRem:Jn.map(ze=>ze._id).sort(),portalsAndDocumentsIn:ur.map(ze=>ze._id).sort(),allRemInDocumentOrPortal:So.map(ze=>ze._id).sort(),allRemInFolderQueue:er.map(ze=>ze._id).sort(),positionAmongstSiblings:xt??null,timesSelectedInSearch:_n,lastTimeMovedTo:Sn,schemaVersion:Dn,embeddedQueueViewMode:cr,createdAt:y.createdAt,updatedAt:y.updatedAt,localUpdatedAt:y.localUpdatedAt,lastPracticed:xr};return!R&&(mi>0||Mr>0)?{...fr,powerupFiltered:{tags:mi,children:Mr}}:fr}function $e(w){return w.map(y=>{if(typeof y=="string")return y;const E={};for(const R of Object.keys(y).sort())E[R]=y[R];return E})}function st(w){switch(w){case 2:return"embedded_queue";case 3:return"scaffold";case 4:return"search_portal";default:return"portal"}}function Z(w){return"type"in w&&w.type==="elided"}function Ze(w){if(w.isDivider)return"---";const{markdownText:y,markdownBackText:E,hasMultilineChildren:R,practiceDirection:N}=w;let F;return E!==null?F=y+(R?N==="backward"?" \u2191 ":N==="both"?" \u2195 ":" \u2193 ":N==="backward"?" \u2190 ":N==="both"?" \u2194 ":" \u2192 ")+E:R?F=y+(N==="backward"?" \u2191":N==="both"?" \u2195":" \u2193"):F=y,w.isCode&&(F="`"+F+"`"),w.isListItem&&(F="1. "+F),w.isQuote&&(F="> "+F),w.isTodo&&(F=(w.todoStatus==="Finished"?"- [x] ":"- [ ] ")+F),w.fontSize&&(F=(w.fontSize==="H1"?"# ":w.fontSize==="H2"?"## ":"### ")+F),F}function Ye(w,y){const E=[];w.type==="concept"?E.push("type:concept"):w.type==="descriptor"?E.push("type:descriptor"):w.type==="portal"&&(E.push("type:portal"),w.portalRefs.length>0&&E.push("refs:"+w.portalRefs.join(","))),w.isDocument&&E.push("doc"),w.isCardItem&&E.push("role:card-item"),y&&w.childrenCount>0&&E.push("children:"+w.childrenCount);for(const R of w.tags)E.push("tag:"+R.name+"("+R.id+")");return w.isTopLevel&&E.push("top"),E}function ue(w){return{markdownBackText:null,type:"default",hasMultilineChildren:!1,practiceDirection:"none",isCardItem:!1,isDocument:!1,isPortal:!1,portalRefs:[],tags:[],fontSize:null,isTodo:!1,todoStatus:null,isCode:!1,isQuote:!1,isListItem:!1,isDivider:!1,...w}}function fe(w,y=!1){const E=Ze(w),R=Ye(w,y),N=R.length>0?" "+R.join(" "):"";return`${E} <!--${w.id}${N}-->`}function ee(w){const y=w.isExact?"siblings":"nodes";return`<!--...elided ${w.isExact?"":">="}${w.count} ${y} (parent:${w.parentId} range:${w.rangeFrom}-${w.rangeTo} total:${w.totalSiblings})-->`}function ie(w){const y=[];function E(R,N){const F=" ".repeat(N);if(Z(R)){y.push(F+ee(R));return}const Q=fe(R.rem,R.folded);if(y.push(F+Q),!R.folded)for(const G of R.children)E(G,N+1)}return E(w,0),y.join(`
59
59
  `)}function me(w,y,E){if(w<=y)return{visibleIndices:null,elided:null};const R=Math.ceil(y*.7),N=Math.floor(y*.3);return{visibleIndices:{head:R,tail:N},elided:{count:w-R-N,parentId:E,rangeFrom:R,rangeTo:w-N-1,totalSiblings:w}}}async function q(w,y){const{remId:E,depth:R=3,maxNodes:N=200,maxSiblings:F=20,ancestorLevels:Q=0,includePowerup:G=!1}=y,ve=await w.rem.findOne(E);if(!ve)throw new Error(`Rem not found: ${E}`);let Ae=0,Ne=0,he=0;const mt={remaining:N},Me=[];async function Qe(We,xt,Fe){Ae++,mt.remaining--,Me.push(We._id);const Je=await We.getChildrenRem(),Ke=G?Je:await He(Je);G||(he+=Je.length-Ke.length);const Ge=Fe!==-1&&xt>=Fe&&Ke.length>0,Kt=await ot(w,We,Ke,{includePowerup:G,onFilteredTags:pn=>{Ne+=pn}});Ge&&(Kt.hasMultilineChildren=!1);const un=[];if(!Ge){const{visibleIndices:pn,elided:Wt}=me(Ke.length,F,We._id);if(pn){const{head:ut,tail:ct}=pn;for(let Bt=0;Bt<ut&&mt.remaining>0;Bt++)un.push(await Qe(Ke[Bt],xt+1,Fe));if(Wt){const Bt=Fe!==-1&&xt+1>=Fe;un.push({type:"elided",count:Wt.count,isExact:Bt,parentId:Wt.parentId,rangeFrom:Wt.rangeFrom,rangeTo:Wt.rangeTo,totalSiblings:Wt.totalSiblings})}const Nt=Ke.length-ct;for(let Bt=Nt;Bt<Ke.length&&mt.remaining>0;Bt++)un.push(await Qe(Ke[Bt],xt+1,Fe))}else for(let ut=0;ut<Ke.length;ut++){if(mt.remaining<=0){const ct=Ke.length-ut;un.push({type:"elided",count:ct,isExact:!1,parentId:We._id,rangeFrom:ut,rangeTo:Ke.length-1,totalSiblings:Ke.length});break}un.push(await Qe(Ke[ut],xt+1,Fe))}}return{rem:Kt,children:un,folded:Ge}}const wt=await Qe(ve,0,R);await ve.getParentRem()||(wt.rem.isTopLevel=!0);const Be=ie(wt),Xe={rootId:E,depth:R,nodeCount:Ae,outline:Be,nodeRemIds:Me},nt=Math.min(Math.max(Q,0),10);if(nt>0){const We=[];let xt=ve;for(let Fe=0;Fe<nt;Fe++){const Je=await xt.getParentRem();if(!Je)break;const[Ke,Zt,Ge]=await Promise.all([Pt(w,Je.text??[]),Je.getChildrenRem(),Je.isDocument()]);We.push({id:Je._id,name:te(Ke),childrenCount:Zt.length,isDocument:Ge}),xt=Je}if(We.length>0){const Fe=We[We.length-1];await xt.getParentRem()||(Fe.isTopLevel=!0),Xe.ancestors=We}}return!G&&(Ne>0||he>0)&&(Xe.powerupFiltered={tags:Ne,children:he}),Xe}async function ge(w,y){const E=await q(w,y),R=y.includePowerup??!1,N={};return await Promise.all(E.nodeRemIds.map(async F=>{const Q=await w.rem.findOne(F);Q&&(N[F]=await Oe(w,Q,{includePowerup:R}))})),{...E,remObjects:N}}async function C(w,y){const{depth:E=-1,maxNodes:R=200,maxSiblings:N=20}=y,Q=(await w.rem.getAll()).filter(Pe=>Pe.parent===null),G=await Promise.all(Q.map(Pe=>Pe.isDocument())),ve=Q.filter((Pe,Be)=>G[Be]);let Ae=0;const Ne={remaining:R};async function he(Pe,Be,Xe){Ae++,Ne.remaining--;const nt=await Pe.getChildrenRem(),We=await He(nt),xt=await Promise.all(We.map(ut=>ut.isDocument())),Fe=We.filter((ut,ct)=>xt[ct]),Je=We.length-Fe.length,Zt=Xe!==-1&&Be>=Xe&&Fe.length>0,Ge=Pe.type===6,[Kt,un]=await Promise.all([Pt(w,Pe.text??[]),Ge?Pe.getPortalDirectlyIncludedRem():Promise.resolve([])]),pn=ue({id:Pe._id,markdownText:Kt.replace(/\n/g," "),childrenCount:We.length,isDocument:!0,isTopLevel:Pe.parent===null,isPortal:Ge,...Ge?{type:"portal",portalRefs:un.map(ut=>ut._id)}:{}}),Wt=[];if(!Zt&&Fe.length>0){const{visibleIndices:ut,elided:ct}=me(Fe.length,N,Pe._id);if(ut){const{head:Nt,tail:Bt}=ut;for(let Dt=0;Dt<Nt&&Ne.remaining>0;Dt++)Wt.push(await he(Fe[Dt],Be+1,Xe));if(ct){const Dt=Xe!==-1&&Be+1>=Xe;Wt.push({type:"elided",count:ct.count,isExact:Dt,parentId:ct.parentId,rangeFrom:ct.rangeFrom,rangeTo:ct.rangeTo,totalSiblings:ct.totalSiblings})}const hn=Fe.length-Bt;for(let Dt=hn;Dt<Fe.length&&Ne.remaining>0;Dt++)Wt.push(await he(Fe[Dt],Be+1,Xe))}else for(let Nt=0;Nt<Fe.length;Nt++){if(Ne.remaining<=0){const Bt=Fe.length-Nt;Wt.push({type:"elided",count:Bt,isExact:!1,parentId:Pe._id,rangeFrom:Nt,rangeTo:Fe.length-1,totalSiblings:Fe.length});break}Wt.push(await he(Fe[Nt],Be+1,Xe))}}return{rem:pn,children:Wt,folded:Zt}}const mt=[],{visibleIndices:Me,elided:Qe}=me(ve.length,N,"root");if(Me){const{head:Pe,tail:Be}=Me;for(let nt=0;nt<Pe&&Ne.remaining>0;nt++)mt.push(await he(ve[nt],0,E));Qe&&mt.push({type:"elided",count:Qe.count,isExact:!1,parentId:"root",rangeFrom:Qe.rangeFrom,rangeTo:Qe.rangeTo,totalSiblings:Qe.totalSiblings});const Xe=ve.length-Be;for(let nt=Xe;nt<ve.length&&Ne.remaining>0;nt++)mt.push(await he(ve[nt],0,E))}else for(let Pe=0;Pe<ve.length;Pe++){if(Ne.remaining<=0){const Be=ve.length-Pe;mt.push({type:"elided",count:Be,isExact:!1,parentId:"root",rangeFrom:Pe,rangeTo:ve.length-1,totalSiblings:ve.length});break}mt.push(await he(ve[Pe],0,E))}const wt=["<!-- globe: \u77E5\u8BC6\u5E93\u6982\u89C8 -->"];for(const Pe of mt)Z(Pe)?wt.push(ee(Pe)):wt.push(ie(Pe));return{nodeCount:Ae,outline:wt.join(`
60
- `)}}async function V(w,y){const E=[];let R=y;for(;R;){const N=await Pt(w,R.text??[]);E.unshift(N.replace(/\n/g," ").trim()||R._id),R=await R.getParentRem()}return E}async function L(w,y){const{mode:E="focus",ancestorLevels:R=2,maxNodes:N=200,maxSiblings:F=20,depth:Q=3,focusRemId:G}=y;if(E==="page"){if(G)throw new Error("focusRemId \u4EC5\u5728 focus \u6A21\u5F0F\u4E0B\u6709\u6548\uFF0Cpage \u6A21\u5F0F\u4E0B\u8BF7\u52FF\u6307\u5B9A");return A(w,{maxNodes:N,maxSiblings:F,depth:Q})}return $(w,{ancestorLevels:R,maxNodes:N,maxSiblings:F,focusRemId:G})}async function A(w,y){const E=await w.window.getFocusedPaneId();if(!E)throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u9762\u677F ID\uFF0C\u8BF7\u786E\u4FDD\u6709\u6253\u5F00\u7684\u9875\u9762");const R=await w.window.getOpenPaneRemId(E);if(!R)throw new Error("\u5F53\u524D\u9762\u677F\u6CA1\u6709\u6253\u5F00\u4EFB\u4F55 Rem");const N=await w.rem.findOne(R);if(!N)throw new Error(`Page Rem not found: ${R}`);const F=await V(w,N);let Q=0;const G={remaining:y.maxNodes},ve=await U(w,N,0,y.depth,y.maxSiblings,G);Q=y.maxNodes-G.remaining;const Ne=`<!-- page: ${F[F.length-1]||R} -->
60
+ `)}}async function V(w,y){const E=[];let R=y;for(;R;){const N=await Pt(w,R.text??[]);E.unshift(N.replace(/\n/g," ").trim()||R._id),R=await R.getParentRem()}return E}async function L(w,y){const{mode:E="page",ancestorLevels:R=2,maxNodes:N=200,maxSiblings:F=20,depth:Q=3,focusRemId:G}=y;if(E==="page"){if(G)throw new Error("focusRemId \u4EC5\u5728 focus \u6A21\u5F0F\u4E0B\u6709\u6548\uFF0Cpage \u6A21\u5F0F\u4E0B\u8BF7\u52FF\u6307\u5B9A");return A(w,{maxNodes:N,maxSiblings:F,depth:Q})}return $(w,{ancestorLevels:R,maxNodes:N,maxSiblings:F,focusRemId:G})}async function A(w,y){const E=await w.window.getFocusedPaneId();if(!E)throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u9762\u677F ID\uFF0C\u8BF7\u786E\u4FDD\u6709\u6253\u5F00\u7684\u9875\u9762");const R=await w.window.getOpenPaneRemId(E);if(!R)throw new Error("\u5F53\u524D\u9762\u677F\u6CA1\u6709\u6253\u5F00\u4EFB\u4F55 Rem");const N=await w.rem.findOne(R);if(!N)throw new Error(`Page Rem not found: ${R}`);const F=await V(w,N);let Q=0;const G={remaining:y.maxNodes},ve=await U(w,N,0,y.depth,y.maxSiblings,G);Q=y.maxNodes-G.remaining;const Ne=`<!-- page: ${F[F.length-1]||R} -->
61
61
  <!-- path: ${F.join(" > ")} -->`+`
62
62
  `+ie(ve);return{nodeCount:Q,outline:Ne,breadcrumb:F,mode:"page"}}async function $(w,y){let E;if(y.focusRemId){if(E=await w.rem.findOne(y.focusRemId),!E)throw new Error(`\u6307\u5B9A\u7684 Rem \u4E0D\u5B58\u5728: ${y.focusRemId}`)}else if(E=await w.focus.getFocusedRem(),!E)throw new Error("\u5F53\u524D\u6CA1\u6709\u805A\u7126\u7684 Rem\uFF0C\u8BF7\u5148\u5728 RemNote \u4E2D\u70B9\u51FB\u4E00\u4E2A Rem");const R=await V(w,E),N=[E];let F=E;for(let Me=0;Me<y.ancestorLevels;Me++){const Qe=await F.getParentRem();if(!Qe)break;N.unshift(Qe),F=Qe}const Q={remaining:y.maxNodes};let G=0;const ve=N[0],Ae=await H(w,ve,N,0,E._id,y.maxSiblings,Q);G=y.maxNodes-Q.remaining;const Ne=R[R.length-1]||E._id,mt=`<!-- path: ${R.join(" > ")} -->
63
63
  <!-- focus: ${Ne} (${E._id}) -->`+`
@@ -57,7 +57,7 @@ Add a <Suspense fallback=...> component higher in the tree to provide a loading
57
57
  * LICENSE file in the root directory of this source tree.
58
58
  */var qe,Ot,De,it;if(typeof performance=="object"&&typeof performance.now=="function"){var B=performance;K.unstable_now=function(){return B.now()}}else{var wn=Date,Et=wn.now();K.unstable_now=function(){return wn.now()-Et}}if(typeof window>"u"||typeof MessageChannel!="function"){var Tt=null,b=null,Y=function(){if(Tt!==null)try{var L=K.unstable_now();Tt(!0,L),Tt=null}catch(A){throw setTimeout(Y,0),A}};qe=function(L){Tt!==null?setTimeout(qe,0,L):(Tt=L,setTimeout(Y,0))},Ot=function(L,A){b=setTimeout(L,A)},De=function(){clearTimeout(b)},K.unstable_shouldYield=function(){return!1},it=K.unstable_forceFrameRate=function(){}}else{var c=window.setTimeout,Mt=window.clearTimeout;if(typeof console<"u"){var ht=window.cancelAnimationFrame;typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof ht!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")}var He=!1,at=null,Pt=-1,tt=5,ot=0;K.unstable_shouldYield=function(){return K.unstable_now()>=ot},it=function(){},K.unstable_forceFrameRate=function(L){0>L||125<L?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):tt=0<L?Math.floor(1e3/L):5};var te=new MessageChannel,ae=te.port2;te.port1.onmessage=function(){if(at!==null){var L=K.unstable_now();ot=L+tt;try{at(!0,L)?ae.postMessage(null):(He=!1,at=null)}catch(A){throw ae.postMessage(null),A}}else He=!1},qe=function(L){at=L,He||(He=!0,ae.postMessage(null))},Ot=function(L,A){Pt=c(function(){L(K.unstable_now())},A)},De=function(){Mt(Pt),Pt=-1}}function _e(L,A){var $=L.length;L.push(A);e:for(;;){var H=$-1>>>1,ne=L[H];if(ne!==void 0&&0<st(ne,A))L[H]=A,L[$]=ne,$=H;else break e}}function Oe(L){return L=L[0],L===void 0?null:L}function $e(L){var A=L[0];if(A!==void 0){var $=L.pop();if($!==A){L[0]=$;e:for(var H=0,ne=L.length;H<ne;){var U=2*(H+1)-1,ye=L[U],M=U+1,ln=L[M];if(ye!==void 0&&0>st(ye,$))ln!==void 0&&0>st(ln,ye)?(L[H]=ln,L[M]=$,H=M):(L[H]=ye,L[U]=$,H=U);else if(ln!==void 0&&0>st(ln,$))L[H]=ln,L[M]=$,H=M;else break e}}return A}return null}function st(L,A){var $=L.sortIndex-A.sortIndex;return $!==0?$:L.id-A.id}var Z=[],Ze=[],Ye=1,ue=null,fe=3,ee=!1,ie=!1,me=!1;function q(L){for(var A=Oe(Ze);A!==null;){if(A.callback===null)$e(Ze);else if(A.startTime<=L)$e(Ze),A.sortIndex=A.expirationTime,_e(Z,A);else break;A=Oe(Ze)}}function ge(L){if(me=!1,q(L),!ie)if(Oe(Z)!==null)ie=!0,qe(C);else{var A=Oe(Ze);A!==null&&Ot(ge,A.startTime-L)}}function C(L,A){ie=!1,me&&(me=!1,De()),ee=!0;var $=fe;try{for(q(A),ue=Oe(Z);ue!==null&&(!(ue.expirationTime>A)||L&&!K.unstable_shouldYield());){var H=ue.callback;if(typeof H=="function"){ue.callback=null,fe=ue.priorityLevel;var ne=H(ue.expirationTime<=A);A=K.unstable_now(),typeof ne=="function"?ue.callback=ne:ue===Oe(Z)&&$e(Z),q(A)}else $e(Z);ue=Oe(Z)}if(ue!==null)var U=!0;else{var ye=Oe(Ze);ye!==null&&Ot(ge,ye.startTime-A),U=!1}return U}finally{ue=null,fe=$,ee=!1}}var V=it;K.unstable_IdlePriority=5,K.unstable_ImmediatePriority=1,K.unstable_LowPriority=4,K.unstable_NormalPriority=3,K.unstable_Profiling=null,K.unstable_UserBlockingPriority=2,K.unstable_cancelCallback=function(L){L.callback=null},K.unstable_continueExecution=function(){ie||ee||(ie=!0,qe(C))},K.unstable_getCurrentPriorityLevel=function(){return fe},K.unstable_getFirstCallbackNode=function(){return Oe(Z)},K.unstable_next=function(L){switch(fe){case 1:case 2:case 3:var A=3;break;default:A=fe}var $=fe;fe=A;try{return L()}finally{fe=$}},K.unstable_pauseExecution=function(){},K.unstable_requestPaint=V,K.unstable_runWithPriority=function(L,A){switch(L){case 1:case 2:case 3:case 4:case 5:break;default:L=3}var $=fe;fe=L;try{return A()}finally{fe=$}},K.unstable_scheduleCallback=function(L,A,$){var H=K.unstable_now();switch(typeof $=="object"&&$!==null?($=$.delay,$=typeof $=="number"&&0<$?H+$:H):$=H,L){case 1:var ne=-1;break;case 2:ne=250;break;case 5:ne=1073741823;break;case 4:ne=1e4;break;default:ne=5e3}return ne=$+ne,L={id:Ye++,callback:A,priorityLevel:L,startTime:$,expirationTime:ne,sortIndex:-1},$>H?(L.sortIndex=$,_e(Ze,L),Oe(Z)===null&&L===Oe(Ze)&&(me?De():me=!0,Ot(ge,$-H))):(L.sortIndex=ne,_e(Z,L),ie||ee||(ie=!0,qe(C))),L},K.unstable_wrapCallback=function(L){var A=fe;return function(){var $=fe;fe=A;try{return L.apply(this,arguments)}finally{fe=$}}}},825(tn,K,qe){"use strict";tn.exports=qe(742)}},vd={};function Ac(tn){var K=vd[tn];if(K!==void 0)return K.exports;var qe=vd[tn]={exports:{}};return Pf[tn](qe,qe.exports,Ac),qe.exports}Ac.g=(function(){if(typeof globalThis=="object")return globalThis;try{return this||new Function("return this")()}catch{if(typeof window=="object")return window}})();var wp={};(()=>{"use strict";var tn=Ac(216);const K="0.2.1",qe=[29100,29110,29120,29130],Ot=18e3;var De=Object.defineProperty,it=(w,y,E)=>y in w?De(w,y,{enumerable:!0,configurable:!0,writable:!0,value:E}):w[y]=E,B=(w,y,E)=>it(w,typeof y!="symbol"?y+"":y,E);const wn=4e3,Et=4003;class Tt{constructor(y){B(this,"ws",null),B(this,"reconnectAttempts",0),B(this,"reconnectTimeout",null),B(this,"messageHandler",null),B(this,"status","disconnected"),B(this,"isShuttingDown",!1),B(this,"isPreempted",!1),B(this,"_sdkReady"),B(this,"config"),this._sdkReady=y.sdkReady,this.config={url:y.url,pluginVersion:y.pluginVersion,sdkReady:y.sdkReady,twinSlotIndex:y.twinSlotIndex,isTwinConnection:y.isTwinConnection??!1,maxReconnectAttempts:y.maxReconnectAttempts??10,initialReconnectDelay:y.initialReconnectDelay??1e3,maxReconnectDelay:y.maxReconnectDelay??3e4,onStatusChange:y.onStatusChange,onLog:y.onLog,onPreempted:y.onPreempted,onTwinOccupied:y.onTwinOccupied,onOtherOccupied:y.onOtherOccupied}}log(y,E="info"){this.config.onLog?.(y,E)}setStatus(y){this.status!==y&&(this.status=y,this.config.onStatusChange?.(y))}sendHello(){const y={type:"hello",version:this.config.pluginVersion,sdkReady:this._sdkReady,twinSlotIndex:this.config.twinSlotIndex};try{this.ws?.send(JSON.stringify(y)),this.log(`\u53D1\u9001 hello\uFF08v${this.config.pluginVersion}, sdkReady=${this._sdkReady}, twinSlot=${this.config.twinSlotIndex}\uFF09`)}catch(E){this.log(`\u53D1\u9001 hello \u5931\u8D25: ${E}`,"warn")}}connect(){if(!(this.ws?.readyState===WebSocket.OPEN||this.ws?.readyState===WebSocket.CONNECTING)){this.isShuttingDown=!1,this.isPreempted=!1,this.setStatus("connecting");try{this.ws=new WebSocket(this.config.url),this.ws.onopen=()=>{this.log("\u5DF2\u8FDE\u63A5\u5230\u5B88\u62A4\u8FDB\u7A0B"),this.reconnectAttempts=0,this.setStatus("connected"),this.sendHello()},this.ws.onmessage=async y=>{await this.handleMessage(typeof y.data=="string"?y.data:String(y.data))},this.ws.onclose=y=>{y.code!==1006&&this.log(`\u8FDE\u63A5\u65AD\u5F00: ${y.code} ${y.reason}`,"warn"),this.setStatus("disconnected"),y.code===Et?this.config.onTwinOccupied?.():y.code===wn&&this.config.onOtherOccupied?.(),this.isShuttingDown||this.scheduleReconnect()},this.ws.onerror=()=>{}}catch(y){this.log(`\u8FDE\u63A5\u5931\u8D25: ${y}`,"error"),this.setStatus("disconnected"),this.scheduleReconnect()}}}async handleMessage(y){try{const E=JSON.parse(y);if(E.type==="preempted"){this.isPreempted=!0,this.log(`\u88AB\u5B6A\u751F Plugin \u62A2\u5360: ${E.reason}`,"warn"),this.config.onPreempted?.();return}if(E.type==="ping"){this.ws?.send(JSON.stringify({type:"pong"}));return}if(E.id&&E.action&&this.messageHandler){const R=E;this.log(`\u6536\u5230\u8BF7\u6C42: ${R.action}`);try{const N=await this.messageHandler(R),F={id:R.id,result:N};this.ws?.send(JSON.stringify(F)),this.log(`\u5B8C\u6210: ${R.action}`)}catch(N){const F=N instanceof Error?N.message:String(N),Q={id:R.id,error:F};this.ws?.send(JSON.stringify(Q)),this.log(`\u5931\u8D25: ${R.action} - ${F}`,"error")}}}catch(E){this.log(`\u5904\u7406\u6D88\u606F\u5931\u8D25: ${E}`,"error")}}scheduleReconnect(){if(this.isShuttingDown||this.isPreempted||!this.config.isTwinConnection)return;if(this.reconnectAttempts>=this.config.maxReconnectAttempts){this.log("\u5DF2\u8FBE\u6700\u5927\u91CD\u8FDE\u6B21\u6570","error");return}const y=Math.min(this.config.initialReconnectDelay*Math.pow(2,this.reconnectAttempts),this.config.maxReconnectDelay),E=Math.random()*.3*y,R=y+E;this.reconnectAttempts++,this.log(`${Math.round(R)}ms \u540E\u91CD\u8FDE\uFF08\u7B2C ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} \u6B21\uFF09`),this.reconnectTimeout=setTimeout(()=>{this.connect()},R)}setMessageHandler(y){this.messageHandler=y}disconnect(){this.isShuttingDown=!0,this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null),this.ws&&(this.ws.close(1e3,"Plugin disconnect"),this.ws=null),this.setStatus("disconnected")}getStatus(){return this.status}}var b=Object.defineProperty,Y=(w,y,E)=>y in w?b(w,y,{enumerable:!0,configurable:!0,writable:!0,value:E}):w[y]=E,c=(w,y,E)=>Y(w,typeof y!="symbol"?y+"":y,E);class Mt{constructor(y){c(this,"clients",[]),c(this,"slotStates",[]),c(this,"scanTimer",null),c(this,"config"),this.config=y;for(let E=0;E<qe.length;E++){const R=E===y.twinSlotIndex;this.slotStates.push({slotIndex:E,wsPort:qe[E],status:"disconnected",isTwin:R,disconnectReason:"not_started"}),this.clients.push(new Tt({url:`ws://127.0.0.1:${qe[E]}`,pluginVersion:y.pluginVersion,sdkReady:y.sdkReady,twinSlotIndex:y.twinSlotIndex,isTwinConnection:R,maxReconnectAttempts:R?10:0,initialReconnectDelay:1e3,maxReconnectDelay:3e4,onStatusChange:N=>this.handleStatusChange(E,N),onLog:(N,F)=>y.onLog(E,N,F),onPreempted:()=>this.handleDisconnectReason(E,"preempted"),onTwinOccupied:()=>this.handleDisconnectReason(E,"twin_occupied"),onOtherOccupied:()=>this.handleDisconnectReason(E,"other_occupied")}))}}setMessageHandler(y){for(const E of this.clients)E.setMessageHandler(y)}start(){const y=this.config.twinSlotIndex;this.clients[y].connect(),setTimeout(()=>{for(let E=0;E<this.clients.length;E++)E!==y&&this.clients[E].connect()},2e3),this.scanTimer=setInterval(()=>this.scanAndReconnect(),Ot)}stop(){this.scanTimer&&(clearInterval(this.scanTimer),this.scanTimer=null);for(const y of this.clients)y.disconnect()}getSlots(){return this.slotStates.slice()}handleStatusChange(y,E){const R=this.slotStates[y];R.status=E,E==="connected"&&(R.disconnectReason=null),this.notifySlotsChange()}handleDisconnectReason(y,E){this.slotStates[y].disconnectReason=E,this.notifySlotsChange()}notifySlotsChange(){this.config.onSlotsChange(this.slotStates.slice())}scanAndReconnect(){for(let y=0;y<this.clients.length;y++){const E=this.slotStates[y];E.isTwin||E.status==="connected"||E.status==="connecting"||this.clients[y].connect()}}}async function ht(w){const[y,E,R,N]=await Promise.all([w.isPowerupProperty(),w.isPowerupSlot(),w.isPowerupPropertyListItem(),w.isPowerupEnum()]);return y||E||R||N}async function He(w){const y=await Promise.all(w.map(ht));return w.filter((E,R)=>!y[R])}async function at(w){const y=await Promise.all(w.map(E=>E.isPowerup()));return w.filter((E,R)=>!y[R])}async function Pt(w,y){try{return await w.richText.toMarkdown(y)}catch{return tt(y)}}function tt(w){return w.map(y=>{if(typeof y=="string")return y;if(typeof y!="object"||y===null)return"";const E=y;switch(E.i){case"m":return String(E.text??"");case"q":return`[[${String(E._id??"")}]]`;case"u":return E.title?`[${String(E.title)}](${String(E.url)})`:String(E.url??"");case"x":return`$${String(E.text??"")}$`;case"i":return`![image](${String(E.url??"")})`;case"a":return`[audio](${String(E.url??"")})`;case"p":return String(E.text??"");case"g":return String(E.text??"");case"n":return String(E.text??"");case"o":return String(E.text??"");case"s":case"fi":case"ai":return"";default:return String(E.text??E.url??"")}}).join("")}async function ot(w,y,E,R){const N=R?.includePowerup??!1,[F,Q,G,ve,Ae,Ne,he,mt,Me,Qe,wt,Pe,Be,Xe,nt]=await Promise.all([Pt(w,y.text??[]),y.backText?Pt(w,y.backText):Promise.resolve(null),y.getType(),y.isCardItem(),y.isDocument(),y.getTagRems().then(Je=>N?Je:at(Je).then(Ke=>{const Zt=Je.length-Ke.length;return Zt>0&&R?.onFilteredTags?.(Zt),Ke})),y.getPracticeDirection(),y.getFontSize(),y.isTodo(),y.getTodoStatus(),y.isCode(),y.isQuote(),y.isListItem(),y.hasPowerup("dv"),y.type===6?y.getPortalDirectlyIncludedRem():Promise.resolve([])]);let We=!1;E.length>0&&(We=(await Promise.all(E.map(Ke=>Ke.isCardItem()))).some(Boolean));const xt=Xe&&(y.text??[]).length===0,Fe=await Promise.all(Ne.map(async Je=>({id:Je._id,name:te(await Pt(w,Je.text??[]))})));return{id:y._id,markdownText:te(F),markdownBackText:Q!==null?te(Q):null,type:ae(G),hasMultilineChildren:We,practiceDirection:he??"none",isCardItem:ve,isDocument:Ae,isPortal:y.type===6,portalRefs:nt.map(Je=>Je._id),childrenCount:E.length,tags:Fe,fontSize:mt??null,isTodo:Me,todoStatus:Qe??null,isCode:wt,isQuote:Pe,isListItem:Be,isDivider:xt,isTopLevel:y.parent===null}}function te(w){return w.replace(/\n/g," ")}function ae(w){switch(w){case 1:return"concept";case 2:return"descriptor";case 6:return"portal";default:return"default"}}async function _e(w,y){const{includePowerup:E=!1}=y,R=await w.rem.findOne(y.remId);if(!R)throw new Error(`Rem not found: ${y.remId}`);return Oe(w,R,{includePowerup:E})}async function Oe(w,y,E){const{includePowerup:R=!1}=E,[N,F,Q,G,ve,Ae,Ne,he,mt,Me,Qe,wt,Pe,Be,Xe,nt,We,xt,Fe,Je,Ke,Zt,Ge,Kt,un,pn,Wt,ut,ct,Nt,Bt,hn,Dt,Jn,ur,So,er,Tr,_n,Sn,Dn,cr,xr]=await Promise.all([y.isDocument(),y.getFontSize(),y.getHighlightColor(),y.isTodo(),y.getTodoStatus(),y.isCode(),y.isQuote(),y.isListItem(),y.isCardItem(),y.isTable(),y.isSlot(),y.isProperty(),y.getEnablePractice(),y.getPracticeDirection(),y.getTagRems(),y.getSources(),y.getAliases(),y.positionAmongstSiblings(),y.isPowerup(),y.isPowerupEnum(),y.isPowerupProperty(),y.isPowerupPropertyListItem(),y.isPowerupSlot(),y.getPortalType(),y.getPortalDirectlyIncludedRem(),y.getPropertyType(),y.remsBeingReferenced(),y.deepRemsBeingReferenced(),y.remsReferencingThis(),y.taggedRem(),y.ancestorTagRem(),y.descendantTagRem(),y.getDescendants(),y.siblingRem(),y.portalsAndDocumentsIn(),y.allRemInDocumentOrPortal(),y.allRemInFolderQueue(),y.getChildrenRem(),y.timesSelectedInSearch(),y.getLastTimeMovedTo(),y.getSchemaVersion(),y.embeddedQueueViewMode(),y.getLastPracticed()]);let dr=Xe,Cr=y.children??[],mi=0,Mr=0;if(!R){dr=await at(Xe),mi=Xe.length-dr.length;const ze=await He(Tr);Mr=Tr.length-ze.length,Cr=ze.map(An=>An._id)}const fr={id:y._id,text:$e(y.text??[]),backText:y.backText?$e(y.backText):null,type:ae(y.type),isDocument:N,parent:y.parent,children:Cr,fontSize:F??null,highlightColor:Q??null,isTodo:G,todoStatus:ve??null,isCode:Ae,isQuote:Ne,isListItem:he,isCardItem:mt,isTable:!!Me,isSlot:Qe,isProperty:wt,isPowerup:Fe,isPowerupEnum:Je,isPowerupProperty:Ke,isPowerupPropertyListItem:Zt,isPowerupSlot:Ge,portalType:ae(y.type)==="portal"?st(Kt):null,portalDirectlyIncludedRem:un.map(ze=>ze._id).sort(),propertyType:pn??null,enablePractice:Pe,practiceDirection:Be,tags:dr.map(ze=>ze._id).sort(),sources:nt.map(ze=>ze._id).sort(),aliases:We.map(ze=>ze._id).sort(),remsBeingReferenced:Wt.map(ze=>ze._id).sort(),deepRemsBeingReferenced:ut.map(ze=>ze._id).sort(),remsReferencingThis:ct.map(ze=>ze._id).sort(),taggedRem:Nt.map(ze=>ze._id).sort(),ancestorTagRem:Bt.map(ze=>ze._id).sort(),descendantTagRem:hn.map(ze=>ze._id).sort(),descendants:Dt.map(ze=>ze._id).sort(),siblingRem:Jn.map(ze=>ze._id).sort(),portalsAndDocumentsIn:ur.map(ze=>ze._id).sort(),allRemInDocumentOrPortal:So.map(ze=>ze._id).sort(),allRemInFolderQueue:er.map(ze=>ze._id).sort(),positionAmongstSiblings:xt??null,timesSelectedInSearch:_n,lastTimeMovedTo:Sn,schemaVersion:Dn,embeddedQueueViewMode:cr,createdAt:y.createdAt,updatedAt:y.updatedAt,localUpdatedAt:y.localUpdatedAt,lastPracticed:xr};return!R&&(mi>0||Mr>0)?{...fr,powerupFiltered:{tags:mi,children:Mr}}:fr}function $e(w){return w.map(y=>{if(typeof y=="string")return y;const E={};for(const R of Object.keys(y).sort())E[R]=y[R];return E})}function st(w){switch(w){case 2:return"embedded_queue";case 3:return"scaffold";case 4:return"search_portal";default:return"portal"}}function Z(w){return"type"in w&&w.type==="elided"}function Ze(w){if(w.isDivider)return"---";const{markdownText:y,markdownBackText:E,hasMultilineChildren:R,practiceDirection:N}=w;let F;return E!==null?F=y+(R?N==="backward"?" \u2191 ":N==="both"?" \u2195 ":" \u2193 ":N==="backward"?" \u2190 ":N==="both"?" \u2194 ":" \u2192 ")+E:R?F=y+(N==="backward"?" \u2191":N==="both"?" \u2195":" \u2193"):F=y,w.isCode&&(F="`"+F+"`"),w.isListItem&&(F="1. "+F),w.isQuote&&(F="> "+F),w.isTodo&&(F=(w.todoStatus==="Finished"?"- [x] ":"- [ ] ")+F),w.fontSize&&(F=(w.fontSize==="H1"?"# ":w.fontSize==="H2"?"## ":"### ")+F),F}function Ye(w,y){const E=[];w.type==="concept"?E.push("type:concept"):w.type==="descriptor"?E.push("type:descriptor"):w.type==="portal"&&(E.push("type:portal"),w.portalRefs.length>0&&E.push("refs:"+w.portalRefs.join(","))),w.isDocument&&E.push("doc"),w.isCardItem&&E.push("role:card-item"),y&&w.childrenCount>0&&E.push("children:"+w.childrenCount);for(const R of w.tags)E.push("tag:"+R.name+"("+R.id+")");return w.isTopLevel&&E.push("top"),E}function ue(w){return{markdownBackText:null,type:"default",hasMultilineChildren:!1,practiceDirection:"none",isCardItem:!1,isDocument:!1,isPortal:!1,portalRefs:[],tags:[],fontSize:null,isTodo:!1,todoStatus:null,isCode:!1,isQuote:!1,isListItem:!1,isDivider:!1,...w}}function fe(w,y=!1){const E=Ze(w),R=Ye(w,y),N=R.length>0?" "+R.join(" "):"";return`${E} <!--${w.id}${N}-->`}function ee(w){const y=w.isExact?"siblings":"nodes";return`<!--...elided ${w.isExact?"":">="}${w.count} ${y} (parent:${w.parentId} range:${w.rangeFrom}-${w.rangeTo} total:${w.totalSiblings})-->`}function ie(w){const y=[];function E(R,N){const F=" ".repeat(N);if(Z(R)){y.push(F+ee(R));return}const Q=fe(R.rem,R.folded);if(y.push(F+Q),!R.folded)for(const G of R.children)E(G,N+1)}return E(w,0),y.join(`
59
59
  `)}function me(w,y,E){if(w<=y)return{visibleIndices:null,elided:null};const R=Math.ceil(y*.7),N=Math.floor(y*.3);return{visibleIndices:{head:R,tail:N},elided:{count:w-R-N,parentId:E,rangeFrom:R,rangeTo:w-N-1,totalSiblings:w}}}async function q(w,y){const{remId:E,depth:R=3,maxNodes:N=200,maxSiblings:F=20,ancestorLevels:Q=0,includePowerup:G=!1}=y,ve=await w.rem.findOne(E);if(!ve)throw new Error(`Rem not found: ${E}`);let Ae=0,Ne=0,he=0;const mt={remaining:N},Me=[];async function Qe(We,xt,Fe){Ae++,mt.remaining--,Me.push(We._id);const Je=await We.getChildrenRem(),Ke=G?Je:await He(Je);G||(he+=Je.length-Ke.length);const Ge=Fe!==-1&&xt>=Fe&&Ke.length>0,Kt=await ot(w,We,Ke,{includePowerup:G,onFilteredTags:pn=>{Ne+=pn}});Ge&&(Kt.hasMultilineChildren=!1);const un=[];if(!Ge){const{visibleIndices:pn,elided:Wt}=me(Ke.length,F,We._id);if(pn){const{head:ut,tail:ct}=pn;for(let Bt=0;Bt<ut&&mt.remaining>0;Bt++)un.push(await Qe(Ke[Bt],xt+1,Fe));if(Wt){const Bt=Fe!==-1&&xt+1>=Fe;un.push({type:"elided",count:Wt.count,isExact:Bt,parentId:Wt.parentId,rangeFrom:Wt.rangeFrom,rangeTo:Wt.rangeTo,totalSiblings:Wt.totalSiblings})}const Nt=Ke.length-ct;for(let Bt=Nt;Bt<Ke.length&&mt.remaining>0;Bt++)un.push(await Qe(Ke[Bt],xt+1,Fe))}else for(let ut=0;ut<Ke.length;ut++){if(mt.remaining<=0){const ct=Ke.length-ut;un.push({type:"elided",count:ct,isExact:!1,parentId:We._id,rangeFrom:ut,rangeTo:Ke.length-1,totalSiblings:Ke.length});break}un.push(await Qe(Ke[ut],xt+1,Fe))}}return{rem:Kt,children:un,folded:Ge}}const wt=await Qe(ve,0,R);await ve.getParentRem()||(wt.rem.isTopLevel=!0);const Be=ie(wt),Xe={rootId:E,depth:R,nodeCount:Ae,outline:Be,nodeRemIds:Me},nt=Math.min(Math.max(Q,0),10);if(nt>0){const We=[];let xt=ve;for(let Fe=0;Fe<nt;Fe++){const Je=await xt.getParentRem();if(!Je)break;const[Ke,Zt,Ge]=await Promise.all([Pt(w,Je.text??[]),Je.getChildrenRem(),Je.isDocument()]);We.push({id:Je._id,name:te(Ke),childrenCount:Zt.length,isDocument:Ge}),xt=Je}if(We.length>0){const Fe=We[We.length-1];await xt.getParentRem()||(Fe.isTopLevel=!0),Xe.ancestors=We}}return!G&&(Ne>0||he>0)&&(Xe.powerupFiltered={tags:Ne,children:he}),Xe}async function ge(w,y){const E=await q(w,y),R=y.includePowerup??!1,N={};return await Promise.all(E.nodeRemIds.map(async F=>{const Q=await w.rem.findOne(F);Q&&(N[F]=await Oe(w,Q,{includePowerup:R}))})),{...E,remObjects:N}}async function C(w,y){const{depth:E=-1,maxNodes:R=200,maxSiblings:N=20}=y,Q=(await w.rem.getAll()).filter(Pe=>Pe.parent===null),G=await Promise.all(Q.map(Pe=>Pe.isDocument())),ve=Q.filter((Pe,Be)=>G[Be]);let Ae=0;const Ne={remaining:R};async function he(Pe,Be,Xe){Ae++,Ne.remaining--;const nt=await Pe.getChildrenRem(),We=await He(nt),xt=await Promise.all(We.map(ut=>ut.isDocument())),Fe=We.filter((ut,ct)=>xt[ct]),Je=We.length-Fe.length,Zt=Xe!==-1&&Be>=Xe&&Fe.length>0,Ge=Pe.type===6,[Kt,un]=await Promise.all([Pt(w,Pe.text??[]),Ge?Pe.getPortalDirectlyIncludedRem():Promise.resolve([])]),pn=ue({id:Pe._id,markdownText:Kt.replace(/\n/g," "),childrenCount:We.length,isDocument:!0,isTopLevel:Pe.parent===null,isPortal:Ge,...Ge?{type:"portal",portalRefs:un.map(ut=>ut._id)}:{}}),Wt=[];if(!Zt&&Fe.length>0){const{visibleIndices:ut,elided:ct}=me(Fe.length,N,Pe._id);if(ut){const{head:Nt,tail:Bt}=ut;for(let Dt=0;Dt<Nt&&Ne.remaining>0;Dt++)Wt.push(await he(Fe[Dt],Be+1,Xe));if(ct){const Dt=Xe!==-1&&Be+1>=Xe;Wt.push({type:"elided",count:ct.count,isExact:Dt,parentId:ct.parentId,rangeFrom:ct.rangeFrom,rangeTo:ct.rangeTo,totalSiblings:ct.totalSiblings})}const hn=Fe.length-Bt;for(let Dt=hn;Dt<Fe.length&&Ne.remaining>0;Dt++)Wt.push(await he(Fe[Dt],Be+1,Xe))}else for(let Nt=0;Nt<Fe.length;Nt++){if(Ne.remaining<=0){const Bt=Fe.length-Nt;Wt.push({type:"elided",count:Bt,isExact:!1,parentId:Pe._id,rangeFrom:Nt,rangeTo:Fe.length-1,totalSiblings:Fe.length});break}Wt.push(await he(Fe[Nt],Be+1,Xe))}}return{rem:pn,children:Wt,folded:Zt}}const mt=[],{visibleIndices:Me,elided:Qe}=me(ve.length,N,"root");if(Me){const{head:Pe,tail:Be}=Me;for(let nt=0;nt<Pe&&Ne.remaining>0;nt++)mt.push(await he(ve[nt],0,E));Qe&&mt.push({type:"elided",count:Qe.count,isExact:!1,parentId:"root",rangeFrom:Qe.rangeFrom,rangeTo:Qe.rangeTo,totalSiblings:Qe.totalSiblings});const Xe=ve.length-Be;for(let nt=Xe;nt<ve.length&&Ne.remaining>0;nt++)mt.push(await he(ve[nt],0,E))}else for(let Pe=0;Pe<ve.length;Pe++){if(Ne.remaining<=0){const Be=ve.length-Pe;mt.push({type:"elided",count:Be,isExact:!1,parentId:"root",rangeFrom:Pe,rangeTo:ve.length-1,totalSiblings:ve.length});break}mt.push(await he(ve[Pe],0,E))}const wt=["<!-- globe: \u77E5\u8BC6\u5E93\u6982\u89C8 -->"];for(const Pe of mt)Z(Pe)?wt.push(ee(Pe)):wt.push(ie(Pe));return{nodeCount:Ae,outline:wt.join(`
60
- `)}}async function V(w,y){const E=[];let R=y;for(;R;){const N=await Pt(w,R.text??[]);E.unshift(N.replace(/\n/g," ").trim()||R._id),R=await R.getParentRem()}return E}async function L(w,y){const{mode:E="focus",ancestorLevels:R=2,maxNodes:N=200,maxSiblings:F=20,depth:Q=3,focusRemId:G}=y;if(E==="page"){if(G)throw new Error("focusRemId \u4EC5\u5728 focus \u6A21\u5F0F\u4E0B\u6709\u6548\uFF0Cpage \u6A21\u5F0F\u4E0B\u8BF7\u52FF\u6307\u5B9A");return A(w,{maxNodes:N,maxSiblings:F,depth:Q})}return $(w,{ancestorLevels:R,maxNodes:N,maxSiblings:F,focusRemId:G})}async function A(w,y){const E=await w.window.getFocusedPaneId();if(!E)throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u9762\u677F ID\uFF0C\u8BF7\u786E\u4FDD\u6709\u6253\u5F00\u7684\u9875\u9762");const R=await w.window.getOpenPaneRemId(E);if(!R)throw new Error("\u5F53\u524D\u9762\u677F\u6CA1\u6709\u6253\u5F00\u4EFB\u4F55 Rem");const N=await w.rem.findOne(R);if(!N)throw new Error(`Page Rem not found: ${R}`);const F=await V(w,N);let Q=0;const G={remaining:y.maxNodes},ve=await U(w,N,0,y.depth,y.maxSiblings,G);Q=y.maxNodes-G.remaining;const Ne=`<!-- page: ${F[F.length-1]||R} -->
60
+ `)}}async function V(w,y){const E=[];let R=y;for(;R;){const N=await Pt(w,R.text??[]);E.unshift(N.replace(/\n/g," ").trim()||R._id),R=await R.getParentRem()}return E}async function L(w,y){const{mode:E="page",ancestorLevels:R=2,maxNodes:N=200,maxSiblings:F=20,depth:Q=3,focusRemId:G}=y;if(E==="page"){if(G)throw new Error("focusRemId \u4EC5\u5728 focus \u6A21\u5F0F\u4E0B\u6709\u6548\uFF0Cpage \u6A21\u5F0F\u4E0B\u8BF7\u52FF\u6307\u5B9A");return A(w,{maxNodes:N,maxSiblings:F,depth:Q})}return $(w,{ancestorLevels:R,maxNodes:N,maxSiblings:F,focusRemId:G})}async function A(w,y){const E=await w.window.getFocusedPaneId();if(!E)throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u9762\u677F ID\uFF0C\u8BF7\u786E\u4FDD\u6709\u6253\u5F00\u7684\u9875\u9762");const R=await w.window.getOpenPaneRemId(E);if(!R)throw new Error("\u5F53\u524D\u9762\u677F\u6CA1\u6709\u6253\u5F00\u4EFB\u4F55 Rem");const N=await w.rem.findOne(R);if(!N)throw new Error(`Page Rem not found: ${R}`);const F=await V(w,N);let Q=0;const G={remaining:y.maxNodes},ve=await U(w,N,0,y.depth,y.maxSiblings,G);Q=y.maxNodes-G.remaining;const Ne=`<!-- page: ${F[F.length-1]||R} -->
61
61
  <!-- path: ${F.join(" > ")} -->`+`
62
62
  `+ie(ve);return{nodeCount:Q,outline:Ne,breadcrumb:F,mode:"page"}}async function $(w,y){let E;if(y.focusRemId){if(E=await w.rem.findOne(y.focusRemId),!E)throw new Error(`\u6307\u5B9A\u7684 Rem \u4E0D\u5B58\u5728: ${y.focusRemId}`)}else if(E=await w.focus.getFocusedRem(),!E)throw new Error("\u5F53\u524D\u6CA1\u6709\u805A\u7126\u7684 Rem\uFF0C\u8BF7\u5148\u5728 RemNote \u4E2D\u70B9\u51FB\u4E00\u4E2A Rem");const R=await V(w,E),N=[E];let F=E;for(let Me=0;Me<y.ancestorLevels;Me++){const Qe=await F.getParentRem();if(!Qe)break;N.unshift(Qe),F=Qe}const Q={remaining:y.maxNodes};let G=0;const ve=N[0],Ae=await H(w,ve,N,0,E._id,y.maxSiblings,Q);G=y.maxNodes-Q.remaining;const Ne=R[R.length-1]||E._id,mt=`<!-- path: ${R.join(" > ")} -->
63
63
  <!-- focus: ${Ne} (${E._id}) -->`+`
@@ -42,7 +42,7 @@ export async function readContext(
42
42
  payload: ReadContextPayload,
43
43
  ): Promise<ReadContextResult> {
44
44
  const {
45
- mode = 'focus',
45
+ mode = 'page',
46
46
  ancestorLevels = 2,
47
47
  maxNodes = 200,
48
48
  maxSiblings = 20,
@@ -97,13 +97,20 @@ export type PropertyTypeValue =
97
97
 
98
98
  // ─── RemObject 主体 ─────────────────────────────────────────
99
99
 
100
+ /**
101
+ * RemObject 字段注释标记说明:
102
+ * - [R] 只读 / [RW] 可读写 / [R-F] 只读+默认过滤
103
+ * - 🛡️D2 = 参与防线2语义比较(变化 → 硬拒绝)
104
+ * - ⚠️D2 = 敏感元数据(变化 → 放行 + 专门警告)
105
+ * - ⊘D2 = 普通元数据(变化 → 放行 + 统一警告)
106
+ */
100
107
  export interface RemObject {
101
108
 
102
109
  // ══════════════════════════════════════════════════════════
103
110
  // 核心标识
104
111
  // ══════════════════════════════════════════════════════════
105
112
 
106
- /** [R] Rem 唯一 ID。SDK 直接属性 _id */
113
+ /** [R] ⊘D2 Rem 唯一 ID。SDK 直接属性 _id */
107
114
  id: string;
108
115
 
109
116
  // ══════════════════════════════════════════════════════════
@@ -111,12 +118,12 @@ export interface RemObject {
111
118
  // ══════════════════════════════════════════════════════════
112
119
 
113
120
  /**
114
- * [RW] ✅ 正面文本(RichText 数组)。SDK: text / setText()
121
+ * [RW] 🛡️D2 ✅ 正面文本(RichText 数组)。SDK: text / setText()
115
122
  * UI 行为:文本内容立即更新显示,无格式副作用
116
123
  */
117
124
  text: RichText;
118
125
  /**
119
- * [RW] ✅ 背面文本。SDK: backText / setBackText()
126
+ * [RW] 🛡️D2 ✅ 背面文本。SDK: backText / setBackText()
120
127
  * UI 行为:设值后 Rem 显示为 "正面文本 → 背面文本" 格式(箭头分隔符)
121
128
  * 默认 null(无背面);设值即产生闪卡正反面结构
122
129
  * 写入语义:null → setBackText([])(SDK 接受 undefined | RichTextInterface,空数组清除背面)
@@ -129,14 +136,14 @@ export interface RemObject {
129
136
  // ══════════════════════════════════════════════════════════
130
137
 
131
138
  /**
132
- * [RW] ✅ Rem 类型。SDK: type, getType() / setType(SetRemType)
139
+ * [RW] 🛡️D2 ✅ Rem 类型。SDK: type, getType() / setType(SetRemType)
133
140
  * UI 行为:CONCEPT → 文字变粗体;DESCRIPTOR → 保持正常字重(与默认无视觉差异)
134
141
  * SetRemType 不含 PORTAL(6),Portal 只能通过 createPortal() 创建
135
142
  * 底层机制:纯字段修改,不涉及 Powerup Tag 注入或隐藏子 Rem
136
143
  */
137
144
  type: RemTypeValue;
138
145
  /**
139
- * [RW] ✅ 是否作为独立文档页面打开。SDK: isDocument() / setIsDocument()
146
+ * [RW] 🛡️D2 ✅ 是否作为独立文档页面打开。SDK: isDocument() / setIsDocument()
140
147
  * UI 行为:bullet (•) 变为文档页面图标(小方块),Rem 可作为独立页面打开
141
148
  * 独立于 type,CONCEPT Rem 可以同时是 Document
142
149
  * 底层机制:Powerup — 注入"文档" Tag + 自动创建 [Status];;[Draft] descriptor 子 Rem
@@ -148,11 +155,12 @@ export interface RemObject {
148
155
  // ══════════════════════════════════════════════════════════
149
156
 
150
157
  /**
151
- * [RW] ✅ 父 Rem ID。null 表示顶级。SDK: parent / setParent(parent, position?)
158
+ * [RW] ⚠️D2 ✅ 父 Rem ID。null 表示顶级。SDK: parent / setParent(parent, position?)
152
159
  * UI 行为:Rem 从原位置消失,出现在新父级的子列表中
160
+ * 防线2:结构操作后常变化,放行但输出专门警告
153
161
  */
154
162
  parent: string | null;
155
- /** [R] 子 Rem ID 有序数组。SDK 直接属性 children(修改子的 parent 间接改变) */
163
+ /** [R] ⊘D2 子 Rem ID 有序数组。SDK 直接属性 children(修改子的 parent 间接改变) */
156
164
  children: string[];
157
165
 
158
166
  // ══════════════════════════════════════════════════════════
@@ -160,14 +168,14 @@ export interface RemObject {
160
168
  // ══════════════════════════════════════════════════════════
161
169
 
162
170
  /**
163
- * [RW] ✅ 标题大小。SDK: getFontSize() / setFontSize()
171
+ * [RW] 🛡️D2 ✅ 标题大小。SDK: getFontSize() / setFontSize()
164
172
  * UI 行为:H1 → 超大粗体;H2 → 大粗体(略小于 H1);H3 → 中粗体
165
173
  * 默认 null(普通大小);setFontSize(undefined) 恢复
166
174
  * 底层机制:Powerup — 注入"标题" Tag + 创建 [Size];;[H1/H2/H3] descriptor 子 Rem
167
175
  */
168
176
  fontSize: FontSize | null;
169
177
  /**
170
- * [RW] ✅ 高亮颜色。SDK: getHighlightColor() / setHighlightColor()
178
+ * [RW] 🛡️D2 ✅ 高亮颜色。SDK: getHighlightColor() / setHighlightColor()
171
179
  * UI 行为:整行背景变为对应颜色(Red→粉红、Blue→浅蓝),bullet 也着色
172
180
  * 默认 null(无高亮)
173
181
  * SDK 注意:setHighlightColor() 只能设置颜色,不能清除(null/undefined 均被拒绝)
@@ -181,47 +189,47 @@ export interface RemObject {
181
189
  // ══════════════════════════════════════════════════════════
182
190
 
183
191
  /**
184
- * [RW] ✅ 是否待办。SDK: isTodo() / setIsTodo()
192
+ * [RW] 🛡️D2 ✅ 是否待办。SDK: isTodo() / setIsTodo()
185
193
  * UI 行为:文本前出现空心 checkbox(☐);副作用:todoStatus 自动初始化为 "Unfinished"
186
194
  * 底层机制:Powerup — 注入"待办" Tag + 自动创建 [Status];;[Unfinished] descriptor 子 Rem
187
195
  */
188
196
  isTodo: boolean;
189
197
  /**
190
- * [RW] ✅ 待办完成状态。SDK: getTodoStatus() / setTodoStatus()
198
+ * [RW] 🛡️D2 ✅ 待办完成状态。SDK: getTodoStatus() / setTodoStatus()
191
199
  * UI 行为:Finished → checkbox 变蓝色已勾选(☑)+ 文本加删除线
192
200
  * 前提:需先 setIsTodo(true),否则无意义
193
201
  * 写入语义:null → 跳过(清除 todo 状态应通过 isTodo=false 实现,SDK 不接受 null)
194
202
  */
195
203
  todoStatus: TodoStatus | null;
196
204
  /**
197
- * [RW] ✅ 是否代码块。SDK: isCode() / setIsCode()
205
+ * [RW] 🛡️D2 ✅ 是否代码块。SDK: isCode() / setIsCode()
198
206
  * UI 行为:Rem 变为代码块容器——等宽字体、灰色背景、块级缩进
199
207
  * 底层机制:Powerup — 注入"代码" Tag,无参数子 Rem
200
208
  */
201
209
  isCode: boolean;
202
210
  /**
203
- * [RW] ✅ 是否引用块。SDK: isQuote() / setIsQuote()
211
+ * [RW] 🛡️D2 ✅ 是否引用块。SDK: isQuote() / setIsQuote()
204
212
  * UI 行为:左侧出现灰色竖线 + 行背景变浅灰(经典 blockquote 样式)
205
213
  * 底层机制:Powerup — 注入"引用" Tag,无参数子 Rem
206
214
  */
207
215
  isQuote: boolean;
208
216
  /**
209
- * [RW] ✅ 是否列表项。SDK: isListItem() / setIsListItem()
217
+ * [RW] 🛡️D2 ✅ 是否列表项。SDK: isListItem() / setIsListItem()
210
218
  * UI 行为:bullet (•) 变为数字编号 "1."(有序列表样式)
211
219
  * 底层机制:Powerup — 注入"列表项" Tag,无参数子 Rem
212
220
  */
213
221
  isListItem: boolean;
214
222
  /**
215
- * [RW] ✅ 是否卡片项。SDK: isCardItem() / setIsCardItem()
223
+ * [RW] 🛡️D2 ✅ 是否卡片项。SDK: isCardItem() / setIsCardItem()
216
224
  * UI:无明显变化。功能:标记 Rem 以卡片样式显示(类似看板布局),
217
225
  * 而非默认项目符号列表,在 RemNote 的 Card View 中生效
218
226
  * 底层机制:Powerup — 注入"卡片条目" Tag (MultiLineCard),无参数子 Rem
219
227
  */
220
228
  isCardItem: boolean;
221
- /** [R] 是否表格。SDK: isTable()(无 setIsTable,只有 setTableFilter) */
229
+ /** [R] 🛡️D2 是否表格。SDK: isTable()(无 setIsTable,只有 setTableFilter) */
222
230
  isTable: boolean;
223
231
  /**
224
- * [RW] ✅ 是否 Powerup 插槽。SDK: isSlot() / setIsSlot()
232
+ * [RW] 🛡️D2 ✅ 是否 Powerup 插槽。SDK: isSlot() / setIsSlot()
225
233
  * UI:bullet 变为方形图标(☐)。功能:标记 Rem 为 Powerup 的数据插槽(slot),
226
234
  * Powerup 注册时通过 slots 配置定义,用于存储键值对数据(值为 RichText)。
227
235
  * 通过 getPowerupProperty(code, slot) / setPowerupProperty() 读写
@@ -232,7 +240,7 @@ export interface RemObject {
232
240
  */
233
241
  isSlot: boolean;
234
242
  /**
235
- * [RW] ✅ 是否 Tag 属性(表格列)。SDK: isProperty() / setIsProperty()
243
+ * [RW] 🛡️D2 ✅ 是否 Tag 属性(表格列)。SDK: isProperty() / setIsProperty()
236
244
  * UI:bullet 变为方形图标(☐,与 isSlot 相同)。功能:标记 Rem 为父级 Tag 的
237
245
  * 结构化属性列,可通过 getPropertyType() 指定数据类型(text/number/date/checkbox/
238
246
  * single_select/multi_select/url/image 等),通过 getTagPropertyValue(propertyId) /
@@ -240,31 +248,31 @@ export interface RemObject {
240
248
  * 底层机制:与 isSlot 完全相同 — 注入同一个"模板插槽" Tag (vD8KGEg5dkj9bzkRn)
241
249
  */
242
250
  isProperty: boolean;
243
- /** [R-F] 是否 Powerup。SDK: isPowerup()(写入用 addPowerup/removePowerup,参数化)。Powerup 系统标识 */
251
+ /** [R-F] ⊘D2 是否 Powerup。SDK: isPowerup()(写入用 addPowerup/removePowerup,参数化)。Powerup 系统标识 */
244
252
  isPowerup: boolean;
245
- /** [R-F] 是否 Powerup 枚举。SDK: isPowerupEnum()。Powerup 细分类型 */
253
+ /** [R-F] ⊘D2 是否 Powerup 枚举。SDK: isPowerupEnum()。Powerup 细分类型 */
246
254
  isPowerupEnum: boolean;
247
- /** [R-F] 是否 Powerup 属性。SDK: isPowerupProperty()。Powerup 细分类型 */
255
+ /** [R-F] ⊘D2 是否 Powerup 属性。SDK: isPowerupProperty()。Powerup 细分类型 */
248
256
  isPowerupProperty: boolean;
249
- /** [R-F] 是否 Powerup 属性列表项。SDK: isPowerupPropertyListItem()。Powerup 细分类型 */
257
+ /** [R-F] ⊘D2 是否 Powerup 属性列表项。SDK: isPowerupPropertyListItem()。Powerup 细分类型 */
250
258
  isPowerupPropertyListItem: boolean;
251
- /** [R-F] 是否 Powerup 插槽。SDK: isPowerupSlot()。Powerup 细分类型 */
259
+ /** [R-F] ⊘D2 是否 Powerup 插槽。SDK: isPowerupSlot()。Powerup 细分类型 */
252
260
  isPowerupSlot: boolean;
253
261
 
254
262
  // ══════════════════════════════════════════════════════════
255
263
  // Portal 专用
256
264
  // ══════════════════════════════════════════════════════════
257
265
 
258
- /** [R] Portal 子类型。仅 type === 'portal' 时有值。SDK: getPortalType() */
266
+ /** [R] 🛡️D2 Portal 子类型。仅 type === 'portal' 时有值。SDK: getPortalType() */
259
267
  portalType: PortalType | null;
260
- /** [Portal-W] Portal 直接包含的 Rem ID 数组。SDK: getPortalDirectlyIncludedRem() / addToPortal() / removeFromPortal()。写入时使用 diff 机制。仅 type === 'portal' 时可修改 */
268
+ /** [Portal-W] 🛡️D2 Portal 直接包含的 Rem ID 数组。SDK: getPortalDirectlyIncludedRem() / addToPortal() / removeFromPortal()。写入时使用 diff 机制。仅 type === 'portal' 时可修改 */
261
269
  portalDirectlyIncludedRem: string[];
262
270
 
263
271
  // ══════════════════════════════════════════════════════════
264
272
  // 属性类型(当此 Rem 是 tag/powerup 的属性时)
265
273
  // ══════════════════════════════════════════════════════════
266
274
 
267
- /** [R] 属性数据类型。SDK: getPropertyType() */
275
+ /** [R] 🛡️D2 属性数据类型。SDK: getPropertyType() */
268
276
  propertyType: PropertyTypeValue | null;
269
277
 
270
278
  // ══════════════════════════════════════════════════════════
@@ -272,14 +280,14 @@ export interface RemObject {
272
280
  // ══════════════════════════════════════════════════════════
273
281
 
274
282
  /**
275
- * [RW] ✅ 是否启用间隔重复练习。SDK: getEnablePractice() / setEnablePractice()
283
+ * [RW] 🛡️D2 ✅ 是否启用间隔重复练习。SDK: getEnablePractice() / setEnablePractice()
276
284
  * UI:无明显变化。功能:为 true 时,RemNote 根据 Rem 的 text/backText 结构
277
285
  * 自动生成闪卡并纳入间隔重复调度。setType(CONCEPT) 可能自动置为 true
278
286
  * 底层机制:纯字段修改,不涉及 Powerup Tag 注入或隐藏子 Rem
279
287
  */
280
288
  enablePractice: boolean;
281
289
  /**
282
- * [RW] ✅ 闪卡练习方向。SDK: getPracticeDirection() / setPracticeDirection()
290
+ * [RW] 🛡️D2 ✅ 闪卡练习方向。SDK: getPracticeDirection() / setPracticeDirection()
283
291
  * UI:无明显变化。功能:控制闪卡生成方向——forward=正面→背面,
284
292
  * backward=背面→正面,both=双向生成,none=不生成闪卡
285
293
  * 底层机制:纯字段修改,不涉及 Powerup Tag 注入或隐藏子 Rem
@@ -291,7 +299,7 @@ export interface RemObject {
291
299
  // ══════════════════════════════════════════════════════════
292
300
 
293
301
  /**
294
- * [RW] ✅ 标签 Rem ID 数组。SDK: getTagRems() / addTag() / removeTag()
302
+ * [RW] 🛡️D2 ✅ 标签 Rem ID 数组。SDK: getTagRems() / addTag() / removeTag()
295
303
  * UI 行为:行右侧出现标签徽章(圆角矩形,显示标签名 + × 删除按钮)
296
304
  * setType(CONCEPT) 等操作可能自动添加系统标签
297
305
  * 注意:系统 Powerup Tag 会混入此数组(如 setIsCode 注入的"代码" Tag),
@@ -299,12 +307,12 @@ export interface RemObject {
299
307
  */
300
308
  tags: string[];
301
309
  /**
302
- * [RW] ✅ 来源 Rem ID 数组。SDK: getSources() / addSource() / removeSource()
310
+ * [RW] 🛡️D2 ✅ 来源 Rem ID 数组。SDK: getSources() / addSource() / removeSource()
303
311
  * UI 行为:Rem 下方出现来源引用子元素(灰色圆角框,显示来源 Rem 名 + ↗ 图标)
304
312
  */
305
313
  sources: string[];
306
314
  /**
307
- * [R] 别名 Rem ID 数组。SDK: getAliases()
315
+ * [R] 🛡️D2 别名 Rem ID 数组。SDK: getAliases()
308
316
  * 写入接口 getOrCreateAliasWithText(text) 需要文本参数(非 ID),与 RemObject 的 ID 数组形式不匹配。
309
317
  * v1 标记为只读,后续可提供独立命令 `add-alias <remId> <text>`
310
318
  */
@@ -314,37 +322,37 @@ export interface RemObject {
314
322
  // 关联 — 引用关系(ID 数组)
315
323
  // ══════════════════════════════════════════════════════════
316
324
 
317
- /** [R] 本 Rem 引用的其他 Rem ID 数组。SDK: remsBeingReferenced() */
325
+ /** [R] 🛡️D2 本 Rem 引用的其他 Rem ID 数组。SDK: remsBeingReferenced() */
318
326
  remsBeingReferenced: string[];
319
- /** [R-F] 本 Rem 深层引用的 Rem ID 数组。SDK: deepRemsBeingReferenced()。可由 remsBeingReferenced 递归获取 */
327
+ /** [R-F] ⊘D2 本 Rem 深层引用的 Rem ID 数组。SDK: deepRemsBeingReferenced()。可由 remsBeingReferenced 递归获取 */
320
328
  deepRemsBeingReferenced: string[];
321
- /** [R] 引用本 Rem 的 Rem ID 数组(反向链接)。SDK: remsReferencingThis() */
329
+ /** [R] ⊘D2 引用本 Rem 的 Rem ID 数组(反向链接)。SDK: remsReferencingThis() */
322
330
  remsReferencingThis: string[];
323
331
 
324
332
  // ══════════════════════════════════════════════════════════
325
333
  // 关联 — 标签体系(ID 数组)
326
334
  // ══════════════════════════════════════════════════════════
327
335
 
328
- /** [R] 被本 Rem 标记的 Rem ID 数组(本 Rem 作为 tag 时)。SDK: taggedRem() */
336
+ /** [R] ⊘D2 被本 Rem 标记的 Rem ID 数组(本 Rem 作为 tag 时)。SDK: taggedRem() */
329
337
  taggedRem: string[];
330
- /** [R-F] 祖先标签 Rem ID 数组。SDK: ancestorTagRem()。标签继承链,低频 */
338
+ /** [R-F] ⊘D2 祖先标签 Rem ID 数组。SDK: ancestorTagRem()。标签继承链,低频 */
331
339
  ancestorTagRem: string[];
332
- /** [R-F] 后代标签 Rem ID 数组。SDK: descendantTagRem()。标签继承链,低频 */
340
+ /** [R-F] ⊘D2 后代标签 Rem ID 数组。SDK: descendantTagRem()。标签继承链,低频 */
333
341
  descendantTagRem: string[];
334
342
 
335
343
  // ══════════════════════════════════════════════════════════
336
344
  // 关联 — 层级遍历(ID 数组)
337
345
  // ══════════════════════════════════════════════════════════
338
346
 
339
- /** [R] 所有后代 Rem ID 数组。SDK: getDescendants() */
347
+ /** [R] ⊘D2 所有后代 Rem ID 数组。SDK: getDescendants() */
340
348
  descendants: string[];
341
- /** [R] 兄弟 Rem ID 数组。SDK: siblingRem() */
349
+ /** [R] ⊘D2 兄弟 Rem ID 数组。SDK: siblingRem() */
342
350
  siblingRem: string[];
343
- /** [R-F] 包含的 Portal 和文档 Rem ID 数组。SDK: portalsAndDocumentsIn()。使用场景有限 */
351
+ /** [R-F] ⊘D2 包含的 Portal 和文档 Rem ID 数组。SDK: portalsAndDocumentsIn()。使用场景有限 */
344
352
  portalsAndDocumentsIn: string[];
345
- /** [R-F] 文档/Portal 中所有 Rem ID 数组。SDK: allRemInDocumentOrPortal()。可能数据量大 */
353
+ /** [R-F] ⊘D2 文档/Portal 中所有 Rem ID 数组。SDK: allRemInDocumentOrPortal()。可能数据量大 */
346
354
  allRemInDocumentOrPortal: string[];
347
- /** [R-F] 文件夹队列中的 Rem ID 数组。SDK: allRemInFolderQueue()。场景有限 */
355
+ /** [R-F] ⊘D2 文件夹队列中的 Rem ID 数组。SDK: allRemInFolderQueue()。场景有限 */
348
356
  allRemInFolderQueue: string[];
349
357
 
350
358
  // ══════════════════════════════════════════════════════════
@@ -352,36 +360,36 @@ export interface RemObject {
352
360
  // ══════════════════════════════════════════════════════════
353
361
 
354
362
  /**
355
- * [RW] ✅ 在兄弟间的位置(0 起始)。SDK: positionAmongstSiblings() / setParent(parent, position)
363
+ * [RW] ⊘D2 ✅ 在兄弟间的位置(0 起始)。SDK: positionAmongstSiblings() / setParent(parent, position)
356
364
  * UI 行为:Rem 在父级子列表中的显示位置改变(测试:A→B→C 变为 B→C→A)
357
365
  * position 超过实际数量会被钳位到末尾;位置相对于父 Rem 的全部 children
358
366
  */
359
367
  positionAmongstSiblings: number | null;
360
- /** [R-F] 搜索中被选次数。SDK: timesSelectedInSearch()。统计数据,低频 */
368
+ /** [R-F] ⊘D2 搜索中被选次数。SDK: timesSelectedInSearch()。统计数据,低频 */
361
369
  timesSelectedInSearch: number;
362
- /** [R-F] 上次移动时间(毫秒时间戳)。SDK: getLastTimeMovedTo()。过于细粒度 */
370
+ /** [R-F] ⊘D2 上次移动时间(毫秒时间戳)。SDK: getLastTimeMovedTo()。过于细粒度 */
363
371
  lastTimeMovedTo: number;
364
- /** [R-F] Schema 版本号。SDK: getSchemaVersion()。内部版本号 */
372
+ /** [R-F] ⊘D2 Schema 版本号。SDK: getSchemaVersion()。内部版本号 */
365
373
  schemaVersion: number;
366
374
 
367
375
  // ══════════════════════════════════════════════════════════
368
376
  // 队列视图
369
377
  // ══════════════════════════════════════════════════════════
370
378
 
371
- /** [R-F] 嵌入式队列视图模式。SDK: embeddedQueueViewMode()。场景有限 */
379
+ /** [R-F] ⊘D2 嵌入式队列视图模式。SDK: embeddedQueueViewMode()。场景有限 */
372
380
  embeddedQueueViewMode: boolean;
373
381
 
374
382
  // ══════════════════════════════════════════════════════════
375
383
  // 元数据 / 时间戳
376
384
  // ══════════════════════════════════════════════════════════
377
385
 
378
- /** [R] 创建时间(毫秒时间戳)。SDK 直接属性 createdAt */
386
+ /** [R] ⊘D2 创建时间(毫秒时间戳)。SDK 直接属性 createdAt */
379
387
  createdAt: number;
380
- /** [R] 最后更新时间(毫秒时间戳)。SDK 直接属性 updatedAt */
388
+ /** [R] ⊘D2 最后更新时间(毫秒时间戳)。SDK 直接属性 updatedAt */
381
389
  updatedAt: number;
382
- /** [R-F] 本地最后更新时间(毫秒时间戳)。SDK 直接属性 localUpdatedAt。与 updatedAt 重叠 */
390
+ /** [R-F] ⊘D2 本地最后更新时间(毫秒时间戳)。SDK 直接属性 localUpdatedAt。与 updatedAt 重叠 */
383
391
  localUpdatedAt: number;
384
- /** [R-F] 上次练习时间(毫秒时间戳)。SDK: getLastPracticed()。间隔重复内部数据 */
392
+ /** [R-F] ⊘D2 上次练习时间(毫秒时间戳)。SDK: getLastPracticed()。间隔重复内部数据 */
385
393
  lastPracticed: number;
386
394
  }
387
395
 
@@ -196,7 +196,7 @@ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的
196
196
 
197
197
  #### 序列化确定性
198
198
 
199
- RichText 对象内部按 **key 字母序排列**(`sortRichTextKeys()`),确保 JSON 始终一致。`_id` 中的 `_`(U+005F)排在所有小写字母(`a`=U+0061)之前。防线 2(乐观并发检测)依赖此确定性序列化来比较缓存与最新数据。
199
+ RichText 对象内部按 **key 字母序排列**(`sortRichTextKeys()`),确保 JSON 始终一致。`_id` 中的 `_`(U+005F)排在所有小写字母(`a`=U+0061)之前。防线 2(语义并发检测)依赖此确定性序列化来逐字段比较缓存与最新数据。
200
200
 
201
201
  ### Powerup 机制与噪音过滤
202
202
 
@@ -232,12 +232,12 @@ RemNote 格式设置(fontSize、highlightColor 等)底层通过 Powerup 机
232
232
  | 场景 | 命令 | 特点 |
233
233
  |:-----|:-----|:-----|
234
234
  | 知识库有什么 | `read-globe` | 仅 Document 层级,**无缓存**,最小序列化(无 backText/箭头) |
235
- | 我在编辑什么 | `read-context --mode focus` | 鱼眼视图(焦点 depth=3,siblings depth=1,叔伯 depth=0),**无缓存** |
236
- | 当前页面内容 | `read-context --mode page` | 均匀展开,**无缓存** |
235
+ | 当前页面内容 | `read-context` | 均匀展开(默认,推荐——只需有打开页面即可),**无缓存** |
236
+ | 我光标在哪 / 正在编辑的 Rem | `read-context --mode focus` | 鱼眼视图(需光标在某 Rem 上,否则报错),**无缓存** |
237
237
  | 展开某主题细节 | `read-tree <id>` | 完整子树,**有缓存**供 edit-tree |
238
238
  | 展开子树 + 每个节点属性 | `read-rem-in-tree <id>` | 大纲 + RemObject,**双重缓存**供 edit-tree 和 edit-rem |
239
239
 
240
- **重要**:read-globe、read-context、search 都**不写入缓存**,不能替代 read-tree/read-rem/read-rem-in-tree 作为 edit 的前置条件。read-context 需要用户在 RemNote 中有焦点(focus 模式)或打开页面(page 模式)。
240
+ **重要**:read-globe、read-context、search 都**不写入缓存**,不能替代 read-tree/read-rem/read-rem-in-tree 作为 edit 的前置条件。read-context 需要用户在 RemNote 中有打开页面(page 模式,默认)或有焦点(focus 模式)。
241
241
 
242
242
  ### 修改:用户想改什么?
243
243
 
@@ -409,9 +409,14 @@ search 调用 RemNote SDK 官方搜索方法,其分词基于空格分割。**
409
409
 
410
410
  必须先 read 再 edit。未缓存的 Rem 不允许编辑。
411
411
 
412
- ### 防线 2:乐观并发检测
412
+ ### 防线 2:语义并发检测(三层字段分类)
413
413
 
414
- edit 时从 SDK 重新读取最新数据,与缓存严格比较。被外部修改则拒绝编辑且**不更新缓存**——迫使 Agent 重新 read。
414
+ edit 时从 SDK 重新读取最新数据,逐字段与缓存比较并按三层分类处理:
415
+ - **语义字段**(text/type/tags/highlightColor 等)变化 → **硬拒绝**,不更新缓存,迫使 Agent 重新 read
416
+ - **敏感元数据**(parent)变化 → **放行** + warnings 返回 `"⚠️ parent has changed (was: X, now: Y)..."`,静默刷新缓存
417
+ - **普通元数据**(positionAmongstSiblings/updatedAt/siblingRem 等)变化 → **放行** + warnings 返回 `"ℹ️ Metadata fields changed since last read: ..."`,静默刷新缓存
418
+
419
+ 这意味着 edit-tree 移动/重排 Rem 后,可直接 edit-rem 修改受影响节点,无需重新 read。
415
420
 
416
421
  ### 防线 3:str_replace 精确匹配(仅 edit-tree)
417
422
 
@@ -424,7 +429,9 @@ edit-rem 使用字段直接修改(changes 对象),不经过 str_replace,
424
429
  | 场景 | 缓存行为 | Agent 操作 |
425
430
  |:-----|:---------|:-----------|
426
431
  | 写入成功 | 从 SDK 重新读取 → 更新缓存 | 可继续编辑 |
427
- | 防线 2 拒绝 / 部分写入失败 | **不更新缓存** | 必须重新 read |
432
+ | 仅元数据变化(位置/时间戳等) | 静默刷新缓存并放行 | 可继续编辑(返回 warnings) |
433
+ | 语义字段冲突(text/type 等) | **不更新缓存** | 必须重新 read |
434
+ | 部分写入失败 | **不更新缓存** | 必须重新 read |
428
435
  | 枚举值非法(edit-rem) | 缓存保持不变 | 检查允许的值范围后重试 |
429
436
  | 防线 3 拒绝 / JSON 语法错误(edit-tree) | 缓存保持不变 | 调整 oldStr/newStr 后**直接重试** |
430
437
  | 操作执行中异常(edit-tree) | 已执行的操作保留(**无回滚**),不更新缓存 | 必须重新 read-tree |
@@ -654,7 +661,7 @@ aliases, descendants, siblingRem, isTable, portalType, propertyType
654
661
  | 守护进程未运行 | 未 connect 或已超时 | `connect` |
655
662
  | Plugin 未连接 | RemNote 未打开 | 打开 RemNote(health 三层:daemon→Plugin→SDK 链式依赖) |
656
663
  | has not been read yet | 未先 read | 执行对应 read 命令(search 结果不算 read!) |
657
- | has been modified since last read | 被外部修改 | 重新 read(必须,不可直接重试) |
664
+ | has been modified since last read | 语义字段被外部修改(元数据变化不触发此错误,只返回 warnings) | 重新 read(必须,不可直接重试) |
658
665
  | Invalid value for 'field' | 枚举字段值不合法(edit-rem) | 检查该字段允许的值范围 |
659
666
  | old_str not found | oldStr 不精确(edit-tree) | 检查引号、空格、换行 |
660
667
  | old_str matches N locations | oldStr 不够具体(edit-tree) | 扩大 oldStr 范围,包含更多上下文 |
@@ -225,28 +225,55 @@ if cache.get('rem:' + remId) === null:
225
225
 
226
226
  **恢复方式**:执行 `read-rem <remId>` 后重试。
227
227
 
228
- ### 防线 2:乐观并发检测
228
+ ### 防线 2:语义并发检测(三层字段分类)
229
229
 
230
230
  **目的**:检测自上次 read 以来,Rem 是否被外部修改(如用户在 RemNote UI 中编辑、其他 Agent 修改等)。
231
231
 
232
+ **核心机制**:将 RemObject 所有字段逐个比较,按差异字段所属层级决定处理方式:
233
+
232
234
  ```
233
235
  currentRemObject = forwardToPlugin('read_rem', { remId })
234
- currentJson = JSON.stringify(currentRemObject, null, 2)
235
- cachedJson = JSON.stringify(cachedObj, null, 2)
236
+ { semantic, warned, ignored } = classifyDiff(cachedObj, currentRemObject)
237
+
238
+ if semantic.length > 0:
239
+ // 第1层:语义字段冲突 → 硬拒绝,不更新缓存
240
+ throw "Rem {remId} has been modified since last read."
241
+
242
+ if warned.length > 0:
243
+ // 第2层:parent 变化 → 放行 + 专门警告
244
+ warnings.push("⚠️ parent has changed (was: {old}, now: {new}). ...")
236
245
 
237
- if currentJson !== cachedJson:
238
- // 不更新缓存 迫使 AI 重新 read 获取最新状态
239
- throw "Rem {remId} has been modified since last read. Please read it again before editing."
246
+ if ignored.length > 0:
247
+ // 第3层:普通元数据变化 放行 + 统一警告
248
+ warnings.push("ℹ️ Metadata fields changed since last read: {fields}. ...")
249
+
250
+ if warned.length > 0 || ignored.length > 0:
251
+ // 静默刷新缓存(保持新鲜度)
252
+ cache.set('rem:' + remId, currentRemObject)
240
253
  ```
241
254
 
242
- **关键设计**:
243
- - 比较方式:**将当前 RemObject 和缓存 RemObject 分别 JSON.stringify 后做文本比较**
244
- - 失败时**不更新缓存**:防止 AI 跳过 re-read 直接重试
245
- - RichText key 排序保证序列化确定性(`sortRichTextKeys()`)
255
+ **三层字段分类**:
256
+
257
+ | 层级 | 包含字段 | 变化时行为 |
258
+ |:-----|:---------|:----------|
259
+ | 第1层:语义字段 | text, backText, type, tags, highlightColor, fontSize 等(不在下面两层的所有字段) | **硬拒绝**,不更新缓存 |
260
+ | 第2层:敏感元数据 | parent | **放行** + `⚠️ parent has changed (was: X, now: Y)...` 警告 + 刷新缓存 |
261
+ | 第3层:普通元数据 | positionAmongstSiblings, updatedAt, siblingRem, children, descendants 等 23 个字段 | **放行** + `ℹ️ Metadata fields changed since last read: ...` 警告 + 刷新缓存 |
246
262
 
247
- **恢复方式**:执行 `read-rem <remId>` 获取最新状态后重试。
263
+ **设计意义**:`edit_tree` 移动/重排 Rem 后,可以直接 `edit_rem` 修改受影响节点的文本格式,无需逐个重新 `read_rem`。只有真正的内容/属性并发冲突才会被拦截。
248
264
 
249
- ### 两道防线判断树
265
+ **warnings 输出示例**:
266
+
267
+ | 场景 | warnings |
268
+ |:-----|:---------|
269
+ | 无外部变化 | `[]` |
270
+ | edit_tree 重排后 | `["ℹ️ Metadata fields changed since last read: positionAmongstSiblings. This is expected after structural operations. Proceeding with edit."]` |
271
+ | edit_tree 移动后 | `["⚠️ parent has changed (was: oldId, now: newId). The Rem has been moved to a different parent since last read. Proceeding with edit.", "ℹ️ Metadata fields changed since last read: siblingRem, positionAmongstSiblings, updatedAt. ..."]` |
272
+ | 语义字段被外部修改 | 不返回(抛异常 `"has been modified since last read"`) |
273
+
274
+ **恢复方式**(仅语义冲突需要):执行 `read-rem <remId>` 获取最新状态后重试。
275
+
276
+ ### 防线判断树
250
277
 
251
278
  ```
252
279
  edit-rem(remId, changes)
@@ -255,9 +282,11 @@ edit-rem(remId, changes)
255
282
  │ ├─ 否 → ERROR: "has not been read yet"
256
283
  │ └─ 是 → 继续
257
284
 
258
- ├─ 防线 2: 当前值 === 缓存值?
259
- │ ├─ → ERROR: "has been modified since last read"
260
- └─ 继续
285
+ ├─ 防线 2: 三层字段分类比较
286
+ │ ├─ 语义字段变化 → ERROR: "has been modified since last read"
287
+ ├─ parent 变化 放行(warnings += ⚠️ parent 警告,刷新缓存)
288
+ │ ├─ 仅元数据变化 → 放行(warnings += ℹ️ 元数据警告,刷新缓存)
289
+ │ └─ 无变化 → 继续
261
290
 
262
291
  ├─ 字段分类
263
292
  │ ├─ 只读字段 → 警告
@@ -310,9 +310,10 @@ Agent 需要根据用户意图选择正确的读取命令:
310
310
  │ 适合:首次探索、了解知识库组织
311
311
 
312
312
  ├─ 用户当前在看什么 → read-context
313
- │ ├─ focus 模式(默认):以用户聚焦的 Rem 为中心的鱼眼视图
314
- │ │ 焦点本身展开 depth=3,siblings 浅层展开,祖先路径可见
315
- │ └─ page 模式:以当前打开页面为根展开子树
313
+ │ ├─ page 模式(默认,推荐):以当前打开页面为根展开子树
314
+ │ │ 只需有打开的页面即可,几乎总能成功
315
+ │ └─ focus 模式(仅特定场景):以用户光标所在 Rem 为中心的鱼眼视图
316
+ │ 需用户光标停在某个 Rem 上,否则报错;仅当需要定位光标位置时使用
316
317
 
317
318
  ├─ 某个具体 Rem 的子树 → read-tree <remId>
318
319
  │ 完整展开子树(支持深度/节点预算控制)
@@ -339,8 +340,8 @@ Agent 需要根据用户意图选择正确的读取命令:
339
340
  | 场景 | 命令 | 输出特点 |
340
341
  |:-----|:-----|:---------|
341
342
  | "看看知识库有什么" | `read-globe` | 仅 Document 层级,无内容 Rem |
342
- | "我现在在编辑什么" | `read-context --mode focus` | 鱼眼视图,焦点处详细 |
343
- | "当前页面的内容" | `read-context --mode page` | 以页面为根展开 |
343
+ | "当前页面的内容" | `read-context` | 以页面为根展开(默认,推荐) |
344
+ | "我光标在哪" / "我正在编辑的 Rem" | `read-context --mode focus` | 鱼眼视图(需光标在某 Rem 上,否则报错) |
344
345
  | "展开某个主题的细节" | `read-tree <id>` | 完整子树,可缓存供编辑 |
345
346
  | "展开子树并查看每个节点属性" | `read-rem-in-tree <id>` | 大纲 + RemObject,双重缓存 |
346
347
 
@@ -354,18 +355,25 @@ Agent 需要根据用户意图选择正确的读取命令:
354
355
 
355
356
  #### read-context 特性
356
357
 
357
- **focus 模式**(默认):
358
- - 获取用户当前聚焦的 Rem(需要用户在 RemNote 中点击某个 Rem)
359
- - 鱼眼视图展开策略:焦点 depth=3,焦点的 siblings depth=1,叔伯节点 depth=0
360
- - 向上追溯 `ancestorLevels` 层(默认 2)
361
- - 焦点 Rem 前标记 `* ` 前缀
362
- - 输出头部含 path 和 focus 信息
358
+ **模式选择指引**:
359
+ - **绝大多数场景用 page(默认)**——用户通常只是打开页面浏览,不会特意点击某个 Rem 让光标停在上面。page 只需有打开的页面就能工作,几乎总能成功
360
+ - **仅当需要定位用户光标时用 focus**——如用户说"我正在编辑的这个"、"光标所在的 Rem"。focus 要求光标停在某个 Rem 上,否则报错"当前没有聚焦的 Rem"
361
+ - **不确定时用 page**——最坏情况只是展开整个页面;focus 的最坏情况是报错
363
362
 
364
- **page 模式**:
363
+ **page 模式**(默认,推荐):
365
364
  - 获取当前面板打开的页面 Rem
366
365
  - 以该页面为根展开子树
367
366
  - 参数:`depth`(默认 3)、`maxNodes`、`maxSiblings`
368
367
  - 输出头部含 page 名和 breadcrumb path
368
+ - 前置条件宽松:只需有打开的页面
369
+
370
+ **focus 模式**(仅特定场景):
371
+ - 获取用户当前聚焦的 Rem(需要用户在 RemNote 中点击某个 Rem)
372
+ - 鱼眼视图展开策略:焦点 depth=3,焦点的 siblings depth=1,叔伯节点 depth=0
373
+ - 向上追溯 `ancestorLevels` 层(默认 2)
374
+ - 焦点 Rem 前标记 `* ` 前缀
375
+ - 输出头部含 path 和 focus 信息
376
+ - ⚠️ 前置条件严格:用户光标必须停在某个 Rem 上,否则报错
369
377
 
370
378
  两者共同点:不缓存、Portal 感知、Powerup 噪音过滤、返回 breadcrumb。
371
379
 
@@ -613,7 +621,7 @@ Portal:portalType [R], portalDirectlyIncludedRem [Portal-W]
613
621
 
614
622
  `h` 颜色值:0=无, 1=Red, 2=Orange, 3=Yellow, 4=Green, 5=Purple, 6=Blue, 7=Gray, 8=Brown, 9=Pink。
615
623
 
616
- **序列化确定性**:RichText 对象内部按 key 字母序排列(`sortRichTextKeys()`)。`_id` 的 `_`(U+005F)排在所有小写字母之前。这对乐观并发检测和 edit-tree 的 str_replace 至关重要。
624
+ **序列化确定性**:RichText 对象内部按 key 字母序排列(`sortRichTextKeys()`)。`_id` 的 `_`(U+005F)排在所有小写字母之前。这对语义并发检测和 edit-tree 的 str_replace 至关重要。
617
625
 
618
626
  ---
619
627
 
@@ -749,13 +757,18 @@ read-tree / read-globe / read-context 的输出核心是 Markdown 大纲文本
749
757
 
750
758
  未缓存的 Rem 不允许编辑。确保 Agent 看到的数据和即将编辑的数据是同一份。
751
759
 
752
- #### 防线 2:乐观并发检测
760
+ #### 防线 2:语义并发检测
753
761
 
754
762
  ```
755
- edit 时重新从 SDK 读取最新数据 → 与缓存严格比较
763
+ edit 时重新从 SDK 读取最新数据 → 只比较语义字段(内容、类型、格式、标签等)
756
764
  ```
757
765
 
758
- 如果 Rem 在 read 之后被外部修改(用户在 RemNote UI 中编辑、其他 Agent 修改等),数据不一致时拒绝编辑,**且不更新缓存**——迫使 Agent 重新 read。
766
+ RemObject 字段分为三层:
767
+ - **语义字段**(text, backText, type, tags, highlightColor, fontSize 等):变化 → 硬拒绝,迫使 Agent 重新 read
768
+ - **敏感元数据**(parent):变化 → 放行 + `"⚠️ parent has changed (was: oldId, now: newId). The Rem has been moved to a different parent since last read. Proceeding with edit."`
769
+ - **普通元数据**(positionAmongstSiblings, updatedAt, siblingRem, children, descendants 等):变化 → 放行 + `"ℹ️ Metadata fields changed since last read: {fields}. This is expected after structural operations. Proceeding with edit."`
770
+
771
+ 这意味着 `edit-tree` 移动/重排 Rem 后,可以直接 `edit-rem` 修改受影响节点的文本格式,无需逐个重新 `read-rem`。只有真正的内容/属性并发冲突才会被拦截。
759
772
 
760
773
  #### edit-rem 防线 3:字段白名单校验
761
774
 
@@ -781,7 +794,8 @@ oldStr 必须在目标文本中恰好匹配 1 次
781
794
  | 场景 | 缓存行为 |
782
795
  |:-----|:---------|
783
796
  | 写入全部成功 | 从 SDK 重新读取最新状态 → **更新缓存** |
784
- | 防线拒绝(缓存缺失 / 并发冲突) | **不更新缓存**(迫使 Agent 重新 read) |
797
+ | 仅元数据变化(位置/时间戳等) | **静默刷新缓存并放行**(返回警告) |
798
+ | 语义字段冲突(内容/类型/格式等) | **不更新缓存**(迫使 Agent 重新 read) |
785
799
  | 枚举值非法 | **报错拒绝**,不更新缓存 |
786
800
  | 部分写入失败 | **不更新缓存** |
787
801
 
@@ -859,7 +873,7 @@ RemNote 的格式设置通过 Powerup 机制实现,会向 Rem 注入隐藏的
859
873
  "readTreeAncestorLevels": 0,
860
874
  "readTreeIncludePowerup": false,
861
875
  "readGlobeDepth": -1,
862
- "readContextMode": "focus",
876
+ "readContextMode": "page",
863
877
  "readContextAncestorLevels": 2,
864
878
  "readContextDepth": 3,
865
879
  "searchNumResults": 20
@@ -903,7 +917,7 @@ Agent 遇到错误时的诊断和恢复指南:
903
917
  | 错误 | 原因 | 恢复 |
904
918
  |:-----|:-----|:-----|
905
919
  | has not been read yet | 未先执行 read-rem / read-tree | 执行对应 read 命令后重试 |
906
- | has been modified since last read | Rem read 和 edit 之间被外部修改 | 重新执行 read 获取最新状态后重试 |
920
+ | has been modified since last read | Rem 的语义字段在 read 和 edit 之间被外部修改 | 重新执行 read 获取最新状态后重试 |
907
921
 
908
922
  ### edit-tree str_replace 错误
909
923
 
@@ -8,8 +8,13 @@
8
8
 
9
9
  `read-context` 根据用户在 RemNote 中的当前位置,生成上下文感知的 Markdown 大纲。两种模式适用于不同场景:
10
10
 
11
- - **focus 模式**(默认):以当前焦点 Rem 为中心,向上追溯祖先,构建鱼眼视图——焦点完全展开,周围递减
12
- - **page 模式**:以当前打开的页面 Rem 为根,均匀展开子树
11
+ - **page 模式**(默认,推荐):以当前打开的页面 Rem 为根,均匀展开子树。只需有打开的页面即可,几乎总能成功
12
+ - **focus 模式**(仅特定场景):以当前焦点 Rem 为中心,向上追溯祖先,构建鱼眼视图——焦点完全展开,周围递减。需用户光标停在某个 Rem 上,否则报错
13
+
14
+ **何时用哪个模式**:
15
+ - 绝大多数场景用 page(默认)——用户通常只是打开页面浏览,不会特意让光标停在某个 Rem 上
16
+ - 仅当需要知道用户光标具体在哪个 Rem 上时才用 focus——如用户说"我正在编辑的这个"、"光标所在位置"
17
+ - 不确定时用 page——最坏情况只是展开整个页面;focus 的最坏情况是报错"当前没有聚焦的 Rem"
13
18
 
14
19
  核心能力:
15
20
  - 鱼眼深度梯度(focus 模式):焦点 depth=3、siblings depth=1、叔伯 depth=0
@@ -32,7 +37,7 @@ remnote-bridge read-context [--mode <mode>] [--ancestor-levels <N>] [--depth <N>
32
37
 
33
38
  | 参数/选项 | 类型 | 必需 | 说明 |
34
39
  |-----------|------|:----:|------|
35
- | `--mode <mode>` | string | 否 | 模式:`focus`(默认)或 `page` |
40
+ | `--mode <mode>` | string | 否 | 模式:`page`(默认)或 `focus` |
36
41
  | `--ancestor-levels <N>` | integer | 否 | 向上追溯几层祖先(默认 2,仅 focus 模式) |
37
42
  | `--depth <N>` | integer | 否 | 展开深度(默认 3,仅 page 模式) |
38
43
  | `--max-nodes <N>` | integer | 否 | 全局节点上限(默认 200) |
@@ -84,7 +89,7 @@ remnote-bridge read-context --json '{"mode":"page","depth":5,"maxSiblings":10}'
84
89
 
85
90
  | 字段 | 类型 | 必需 | 说明 |
86
91
  |------|------|:----:|------|
87
- | `mode` | string | 否 | 模式:`"focus"` 或 `"page"`(默认 `"focus"`) |
92
+ | `mode` | string | 否 | 模式:`"page"` 或 `"focus"`(默认 `"page"`) |
88
93
  | `ancestorLevels` | number | 否 | 向上追溯几层祖先(默认 2,仅 focus 模式) |
89
94
  | `depth` | number | 否 | 展开深度(默认 3,仅 page 模式) |
90
95
  | `maxNodes` | number | 否 | 全局节点上限(默认 200) |
@@ -359,7 +364,7 @@ focus 模式的核心特点是**渐进式展开**——离焦点越近,展开
359
364
 
360
365
  | 配置项 | 默认值 | 说明 |
361
366
  |--------|--------|------|
362
- | `defaults.readContextMode` | `"focus"` | 默认模式 |
367
+ | `defaults.readContextMode` | `"page"` | 默认模式 |
363
368
  | `defaults.readContextAncestorLevels` | 2 | focus 模式默认祖先层数 |
364
369
  | `defaults.readContextDepth` | 3 | page 模式默认展开深度 |
365
370
  | `defaults.maxNodes` | 200 | 默认全局节点上限 |