remnote-bridge 0.1.16 → 0.1.18

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">
@@ -80,10 +80,19 @@ CDF 的核心优势:Concept 自动生成双向闪卡("什么是线性回归
80
80
  通过 RichText 中的 \`cId\` 标记实现(不是分隔符),练习时 \`cId\` 标记的文本被遮盖:
81
81
 
82
82
  \`\`\`json
83
- ["The ", {"cId": "cloze1", "i": "m", "text": "capital"}, " of France is Paris"]
83
+ ["The ", {"cId": "8291740362058173", "i": "m", "text": "capital"}, " of France is Paris"]
84
84
  \`\`\`
85
85
 
86
- 通过 \`edit_rem\` 操作 RichText 来创建/修改完形填空——在 \`changes.text\` 数组中为目标文字片段添加 \`cId\` 字段。每个 cloze 需要唯一的 \`cId\` 值。
86
+ #### 创建填空的两条路径
87
+
88
+ | 路径 | 方式 | cId 生成 | 推荐度 |
89
+ |:-----|:-----|:---------|:-------|
90
+ | **edit_tree(推荐)** | 新增行写 \`{{填空文本}}\` | SDK 自动生成安全 cId | ⭐ 优先 |
91
+ | edit_rem(兜底) | changes.text 中手写含 cId 的 RichText | 必须自行生成随机 cId | 仅修改已有 cloze |
92
+
93
+ **优先用 edit_tree**:新增行写 \`水的化学式是{{H2O}}\`,SDK 通过 \`createSingleRemWithMarkdown\` 自动生成唯一 cId,零碰撞风险。
94
+
95
+ **edit_rem 仅用于修改已有 cloze**:先 \`read_rem\` 获取现有 cId,修改时保留原 cId。如必须新建 cloze,cId **禁止**使用有语义的命名(如 \`"cloze1"\`、\`"c1"\`),必须使用随机数字串(如 \`"7390281645937102"\`)。⚠️ cId 碰撞会导致同一 Rem 内多个填空被识别为同一个,练习行为异常且不可逆。
87
96
 
88
97
  ### 链接机制
89
98
 
@@ -238,7 +247,7 @@ disconnect → 关闭 daemon + headless Chrome,清空所有缓存,清除 hea
238
247
  - 用户的描述与你已知信息对不上
239
248
  - 搜索不到用户提到的内容
240
249
 
241
- \`read_context\`:focus 模式(默认)以用户焦点为中心构建鱼眼视图;page 模式以当前页面为根展开。两者都返回面包屑路径。
250
+ \`read_context\`:默认使用 **page 模式**——只需有打开的页面即可,几乎总能成功。仅当需要知道用户光标具体在哪个 Rem 上时,才显式传 \`mode="focus"\`(focus 模式要求用户光标停在某个 Rem 上,否则报错"当前没有聚焦的 Rem")。两者都返回面包屑路径。
242
251
 
243
252
  ### 场景 D:修改文本或属性
244
253
 
@@ -376,7 +385,7 @@ newStr: " 静态数组 <!--id1_2 type:concept-->\\n 动态数组 <!--id1_1
376
385
  **模板规则**:
377
386
  - \`{{remId}}\` 展开为**不含缩进**的完整行内容,缩进由你控制
378
387
  - 只匹配纯字母数字(\`[a-zA-Z0-9]+\`),与 RemNote cloze 语法 \`{{text}}\` 不冲突
379
- - 新增行不能用 \`{{}}\`(新增行没有 remId
388
+ - 新增行不能用 \`{{remId}}\` 模板引用(新增行没有 remId),但可以用 \`{{文本}}\` 创建填空卡片——SDK 自动生成唯一 cId,零碰撞风险(推荐的 cloze 创建方式)
380
389
  - 可以混用:部分行用 \`{{id}}\`,部分行手动写
381
390
 
382
391
  #### ⚠️ children_captured 详解
@@ -416,6 +425,11 @@ oldStr: " {{idZ}}" newStr: " {{idZ}}\\n 新行"
416
425
  - Descriptor(描述属性):作为某个 Concept 的子 Rem,回答"X 的 Y 是什么?"——如"假设 → 因变量与自变量呈线性关系"
417
426
  - 通常 Descriptor 是 Concept 的直接子节点,二者配合构成完整的 CDF 知识结构
418
427
 
428
+ **完形填空(Cloze)**:
429
+ - 创建(推荐):\`edit_tree\` 新增行写 \`水的化学式是{{H2O}}\`,SDK 自动生成唯一 cId
430
+ - 修改已有 cloze:先 \`read_rem\` 获取现有 cId,再 \`edit_rem\` 传含原 cId 的 RichText 数组
431
+ - ⚠️ **禁止**在 \`edit_rem\` 中手造有语义命名的 cId(如 \`"cloze1"\`)——cId 碰撞会导致多个填空被识别为同一个,练习行为异常且不可逆
432
+
419
433
  **修改现有 Rem 的闪卡行为**(\`read_rem\` → \`edit_rem\`):修改 \`type\`、\`practiceDirection\`、\`backText\` 字段。
420
434
 
421
435
  ### 场景 G:排查连接问题
@@ -448,7 +462,7 @@ oldStr: " {{idZ}}" newStr: " {{idZ}}\\n 新行"
448
462
  ### 两道防线
449
463
 
450
464
  1. **缓存存在**:必须有对应的 read 缓存
451
- 2. **并发检测**:edit 时重新读取最新数据与缓存比较,Rem 被外部修改则拒绝——必须重新 read
465
+ 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
466
 
453
467
  ### edit_tree 禁止事项
454
468
 
@@ -465,7 +479,9 @@ oldStr: " {{idZ}}" newStr: " {{idZ}}\\n 新行"
465
479
  | 场景 | 缓存行为 | 重试策略 |
466
480
  |:-----|:---------|:---------|
467
481
  | edit_rem 写入成功 | 从 Plugin 重新读取 → 更新缓存 | 可继续编辑 |
468
- | edit_rem 防线拒绝/部分失败 | 不更新缓存 | 必须重新 read_rem |
482
+ | edit_rem 仅元数据变化 | 静默刷新缓存并放行 | 可继续编辑(返回警告) |
483
+ | edit_rem 语义字段冲突 | 不更新缓存 | 必须重新 read_rem |
484
+ | edit_rem 部分写入失败 | 不更新缓存 | 必须重新 read_rem |
469
485
  | edit_tree 成功 | 自动 re-read → 更新缓存 | 可连续 edit |
470
486
  | edit_tree 防线 3 拒绝(str_replace 不匹配等) | 缓存保持不变 | 调整 oldStr/newStr 后直接重试 |
471
487
  | edit_tree 执行中异常 | 已执行操作保留(**无回滚**),不更新缓存 | 必须重新 read_tree |
@@ -541,6 +557,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
541
557
  - 纯文本行:\`新内容\`
542
558
  - 带前缀:\`# 新标题\`、\`- [ ] 新待办\`、\`> 引用内容\`、\`1. 列表项\`
543
559
  - 带箭头:\`问题 → 答案\`、\`概念 ↔ 定义\`、\`题目 ↓\`
560
+ - 完形填空:\`水的化学式是{{H2O}}\`(SDK 自动生成安全 cId,推荐的 cloze 创建方式)
544
561
  - 带元数据注释(metadata-only,无 remId):\`新行 <!--type:concept doc-->\`
545
562
  - Portal 行:\`<!--portal refs:id1,id2-->\`
546
563
  - 嵌套新增(父+子一起创建):
@@ -556,6 +573,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
556
573
  # 数据结构 <!--kLr type:concept doc top-->
557
574
  ## 线性结构 <!--ABC type:concept-->
558
575
  数组 → Array <!--DEF-->
576
+ {{二分查找}}的时间复杂度是 O(log n) <!--EFG-->
559
577
  链表 ↓ <!--GHI type:concept-->
560
578
  单向链表 <!--JKL role:card-item-->
561
579
  双向链表 <!--MNO role:card-item-->
@@ -596,8 +614,8 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
596
614
  | \`q\` | \`true\` | 行内代码(红色等宽样式) |
597
615
  | \`code\` | \`true\` | 代码块(带语言标签和复制按钮) |
598
616
  | \`language\` | string | 代码语言(如 \`"python"\`) |
599
- | \`cId\` | string | 完形填空 ID |
600
- | \`hiddenCloze\` | \`true\` | 完形填空隐藏状态 |
617
+ | \`cId\` | string | 完形填空 ID(由 SDK 自动生成,禁止手动编造——创建填空用 edit_tree \`{{文本}}\` 语法) |
618
+ | \`hiddenCloze\` | \`true\` | 完形填空隐藏状态(只读,RemNote 自动管理) |
601
619
  | \`iUrl\` | string | 超链接 URL(**不是 \`url\`!**) |
602
620
  | \`qId\` | string | 行内引用的 Rem ID |
603
621
 
@@ -617,7 +635,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
617
635
  {"i": "m", "q": true, "text": "code"} // 行内代码
618
636
  {"i": "m", "iUrl": "https://...", "text": "链接"} // 超链接(iUrl 不是 url!)
619
637
  {"b": true, "h": 1, "i": "m", "text": "重点"} // 粗体+红色高亮
620
- {"cId": "c1", "i": "m", "text": "答案"} // 完形填空
638
+ {"cId": "4718093625140386", "i": "m", "text": "答案"} // 完形填空(⚠️ 创建填空用 edit_tree {{文本}};cId 禁止用 "cloze1"/"c1" 等有语义命名)
621
639
  {"_id": "remId", "b": true, "i": "q"} // Rem 引用加粗
622
640
  {"i": "x", "text": "E = mc^2"} // LaTeX
623
641
  {"i": "a", "onlyAudio": false, "url": "..."} // 视频(onlyAudio 必填!)
@@ -630,7 +648,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
630
648
  - 超链接必须用 \`iUrl\`,\`url\` 字段已废弃无效
631
649
  - RichText 对象内部按 **key 字母序排列**(\`_id\` < \`b\` < \`cId\` < \`h\` < \`i\` < \`iUrl\` < \`text\`),确保序列化一致性
632
650
  - \`highlightColor\`(RemObject 顶层,字符串 \`"Red"\`)与 \`h\`(RichText 内部,数字 \`1\`)完全独立——前者是整行背景色,后者是文字片段荧光底色
633
- - 防线 2(乐观并发检测)依赖 key 字母序的确定性序列化来比较缓存与最新数据
651
+ - 防线 2(语义并发检测)依赖 key 字母序的确定性序列化来比较语义字段
634
652
 
635
653
  ---
636
654
 
@@ -645,7 +663,7 @@ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
645
663
  ├─ "Plugin 未连接" → RemNote 未打开或插件未加载 → 引导用户操作 RemNote
646
664
  ├─ "SDK 未就绪" → 知识库尚未加载 → 等待并重试 health
647
665
  ├─ "has not been read yet" → 未先 read → 执行对应 read 后重试
648
- ├─ "has been modified since last read" → 被外部修改 → 必须重新 read(不可直接重试)
666
+ ├─ "has been modified since last read" → 语义字段被外部修改 → 必须重新 read(不可直接重试)
649
667
  ├─ "Invalid value" → 枚举字段值不合法 → 检查允许的值范围
650
668
  ├─ "old_str not found" → oldStr 不精确 → 检查缩进、空格、换行
651
669
  ├─ "old_str matches N locations" → oldStr 不够具体 → 扩大范围包含更多上下文
@@ -18,6 +18,7 @@ export function registerEditTools(server) {
18
18
  '\\n\\n操作指南:' +
19
19
  '\\n- changes 对象的键=字段名,值=新值。21 个可写字段:text, backText, type, isDocument, parent, fontSize, highlightColor, isTodo, todoStatus, isCode, isQuote, isListItem, isCardItem, isSlot, isProperty, enablePractice, practiceDirection, tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem' +
20
20
  '\\n- RichText 编辑(text/backText):传完整 RichText 数组,元素为纯字符串或格式化对象如 {"i":"m","text":"...","b":true}。backText 传 null 可清除背面' +
21
+ '\\n- ⚠️ 完形填空(cloze):创建填空优先用 edit_tree 的 {{文本}} 语法(SDK 自动生成安全 cId)。edit_rem 仅用于修改已有 cloze(保留原 cId)。如必须新建 cloze,cId 禁止用有语义的命名(如 "cloze1"),必须用随机数字串(如 "7390281645937102")' +
21
22
  '\\n- tags/sources 使用 diff 机制:传入目标 ID 数组,系统自动计算增删差异' +
22
23
  '\\n- portalDirectlyIncludedRem:传入目标 ID 数组(仅 type=portal 的 Rem 可修改),系统自动 diff' +
23
24
  '\\n- parent + positionAmongstSiblings 联动:通过同一个 SDK 调用写入,可单独或同时修改' +
@@ -28,13 +29,17 @@ export function registerEditTools(server) {
28
29
  '\\n- fontSize: H1 / H2 / H3 / null(恢复普通)' +
29
30
  '\\n- todoStatus: Finished / Unfinished / null(需先 isTodo=true)' +
30
31
  '\\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(只读/未知字段警告数组)。' +
32
+ '\\n\\n输出格式:JSON 对象,包含 changes(已写入的字段名数组)和 warnings(警告数组,可能包含防线2元数据警告和字段校验警告)。' +
32
33
  '\\n\\n两道防线:' +
33
34
  '\\n1. 缓存存在检查——未先 read_rem 则报 "has not been read yet"' +
34
- '\\n2. 并发检测——edit 时重新从 Plugin 读取并对比缓存,不一致则报 "has been modified since last read"。防线拒绝/部分失败时不更新缓存,迫使重新 read_rem' +
35
+ '\\n2. 语义并发检测(三层字段分类)——edit 时重新读取并逐字段比较:' +
36
+ '\\n - 语义字段(text/type/tags 等)变化 → 硬拒绝 "has been modified since last read"' +
37
+ '\\n - parent 变化 → 放行 + warnings 返回 "⚠️ parent has changed (was: X, now: Y)..."' +
38
+ '\\n - 普通元数据(positionAmongstSiblings/updatedAt 等)变化 → 放行 + warnings 返回 "ℹ️ Metadata fields changed since last read: ..."' +
39
+ '\\n 这意味着 edit_tree 移动/重排后可直接 edit_rem,无需重新 read' +
35
40
  '\\n\\n常见错误:' +
36
41
  '\\n- "has not been read yet" → 先 read_rem' +
37
- '\\n- "has been modified since last read" → 重新 read_rem' +
42
+ '\\n- "has been modified since last read" → 语义字段被外部修改,重新 read_rem' +
38
43
  '\\n- "Invalid value for \'field\'" → 检查枚举合法值' +
39
44
  '\\n- "Field \'...\' is read-only/unknown and was ignored" → 警告不阻断' +
40
45
  '\\n\\n关联工具:read_rem(前置读取)、edit_tree(子树结构编辑)',
@@ -75,12 +80,13 @@ export function registerEditTools(server) {
75
80
  '\\n ⚠️ 有序列表必须用 `1. `(Lazy Numbering):RemNote 自动编号,不要写 `2. ` `3. ` 等。`2.`~`9.` 会被容错归一化并返回 templateWarnings 警告,`10.` 及以上不识别为列表。' +
76
81
  '\\n- 箭头分隔符(闪卡):→ ← ↔(单行)、↓ ↑ ↕(多行,带 backText 或子节点为答案)' +
77
82
  '\\n- 元数据注释(可选):<!--type:concept--> <!--doc--> <!--tag:Name(id)--> 可组合' +
83
+ '\\n- 完形填空:{{填空文本}}(SDK 自动生成安全 cId,推荐方式)' +
78
84
  '\\n- Portal 创建:<!--portal refs:id1,id2--> 或 <!--portal-->(空 Portal)' +
79
85
  '\\n\\n行引用模板 {{remId}}:' +
80
86
  '\\n在 oldStr/newStr 中使用 {{remId}} 引用缓存大纲中已有行的完整内容(不含缩进),系统在 str_replace 前自动展开。缩进仍由你控制。' +
81
87
  '\\n优势:避免反复抄写行内容(含 remId 和元数据标记),减少 token 开销和复制错误。' +
82
88
  '\\n示例 reorder:oldStr=" {{id1}}\\n {{id2}}" newStr=" {{id2}}\\n {{id1}}"' +
83
- '\\n规则:只匹配纯字母数字(与 RemNote cloze {{text}} 不冲突);未找到的 {{xxx}} 原样保留并输出 templateWarnings;新增行不能使用 {{}};可混用模板和手写内容。' +
89
+ '\\n规则:只匹配纯字母数字(与 RemNote cloze {{text}} 不冲突);未找到的 {{xxx}} 原样保留并输出 templateWarnings;新增行不能用 {{remId}} 模板引用,但可以用 {{文本}} 创建填空卡片;可混用模板和手写内容。' +
84
90
  '\\n\\n⚠️ 插入位置红线:新行必须插在目标层级所有兄弟的末尾,不能插在父 Rem 和它的 children 之间,否则触发 children_captured 错误。如需"创建新父节点并移入已有 children",必须分两次 edit_tree 完成。' +
85
91
  '\\n\\n六种禁止操作:' +
86
92
  '\\n- content_modified:修改已有行内容 → 改用 edit_rem' +
@@ -66,7 +66,7 @@ export function registerReadTools(server) {
66
66
  '\\n- full=true 时返回全部 51 个字段,Portal 类型自动简化为 8 个关键字段' +
67
67
  '\\n- 关键字段:id, text, backText(null), type(concept/descriptor/default/portal), parent, isDocument(false), tags([]), fontSize(H1/H2/H3/null), highlightColor(Red/.../Pink/null), practiceDirection(forward/backward/both/none), isTodo(false), todoStatus(Finished/Unfinished/null)——括号内为默认值' +
68
68
  '\\n- children 字段属于 R-F 层级,默认不输出,需 full=true 或 fields 指定' +
69
- '\\n- text/backText 是 RichText JSON 数组,元素为纯字符串或带 i 字段的对象(i:"m" 格式化文本, i:"q" Rem 引用, i:"x" LaTeX, i:"i" 图片, i:"a" 音视频)' +
69
+ '\\n- text/backText 是 RichText JSON 数组,元素为纯字符串或带 i 字段的对象(i:"m" 格式化文本, i:"q" Rem 引用, i:"x" LaTeX, i:"i" 图片, i:"a" 音视频)。完形填空元素含 cId 字段(如 {"cId":"8291740362058173","i":"m","text":"答案"})' +
70
70
  '\\n- 可能附加 cacheOverridden(覆盖旧缓存时)和 powerupFiltered(过滤统计)元数据' +
71
71
  '\\n\\n关键约束:' +
72
72
  '\\n- 结果自动写入缓存供 edit_rem 使用。缓存存储完整 RemObject,不受 fields/full 选项影响' +
@@ -124,6 +124,7 @@ export function registerReadTools(server) {
124
124
  '\\n大纲每行格式:{缩进}{Markdown前缀}{内容}{箭头}{backText} <!-- {remId} {元数据标记} -->' +
125
125
  '\\n- 缩进:每级 2 空格' +
126
126
  '\\n- Markdown 前缀:# ## ### (标题), - [ ] - [x] (待办), ` (代码)' +
127
+ '\\n- 完形填空:RichText 中带 cId 的元素渲染为 {{文本}},与 edit_tree 新增行的 {{}} 语法对称' +
127
128
  '\\n- 箭头编码 practiceDirection:→←↔(单行闪卡,text→backText);↓↑↕(多行闪卡,答案在子节点)' +
128
129
  '\\n- 元数据标记:type:concept/descriptor/portal, doc, children:N, tag:Name(id), role:card-item, top, refs:id1,id2(Portal引用)' +
129
130
  '\\n- 省略占位符:<!--...elided N siblings (parent:id range:x-y total:z)-->(精确)或 >=N nodes(预算耗尽)' +
@@ -329,11 +330,15 @@ export function registerReadTools(server) {
329
330
  '\\n\\n重要:用户正在看的页面对 AI 不可见。当用户说"这个"、"当前页面"、"这里",或描述与已知信息对不上时,必须主动调用 read_context 对齐信息。' +
330
331
  '\\n\\n适用场景:了解用户当前焦点位置或打开的页面;用户说"我现在在看什么"、"当前页面是什么"时使用;需要上下文才能理解用户指代时使用。' +
331
332
  '\\n不适用场景:查看特定 Rem(已知 remId 用 read_tree);搜索内容(用 search)。' +
332
- '\\n\\n前置条件:daemon 已连接。focus 模式需用户在 RemNote 中有焦点 Rem(光标在某个 Rem 上)或指定 focusRemId;page 模式需有打开的页面。' +
333
+ '\\n\\n前置条件:daemon 已连接。page 模式只需有打开的页面(几乎总满足);focus 模式需用户光标停在某个 Rem 上或指定 focusRemId(否则报错"当前没有聚焦的 Rem")。' +
334
+ '\\n\\n模式选择指引:' +
335
+ '\\n- 绝大多数场景用 page(默认)——用户通常只是打开页面浏览,不会特意点击某个 Rem,page 更可靠' +
336
+ '\\n- 仅当需要知道用户光标具体在哪个 Rem 上时才用 focus——如用户说"我正在编辑的这个"、"光标所在的 Rem"' +
337
+ '\\n- 不确定时用 page——最坏情况只是展开整个页面;focus 的最坏情况是报错' +
333
338
  '\\n\\n参数说明:' +
334
- '\\n- mode(可选,默认 "focus"):视图模式' +
339
+ '\\n- mode(可选,默认 "page"):视图模式' +
340
+ '\\n - page:以当前打开的页面为根,均匀展开子树(推荐,可靠性高)' +
335
341
  '\\n - focus:以焦点 Rem 为中心的鱼眼视图。焦点完全展开(depth=3),siblings 浅层预览(depth=1,前3个children可见),叔伯不展开。焦点行以 * 前缀标记' +
336
- '\\n - page:以当前打开的页面为根,均匀展开子树' +
337
342
  '\\n- focusRemId(可选,仅 focus 模式):指定任意 Rem 作为鱼眼中心,此时不依赖用户实际焦点。page 模式下传入会报错' +
338
343
  '\\n- ancestorLevels(可选,默认 2,仅 focus 模式生效):从焦点向上追溯几层祖先作为上下文起点' +
339
344
  '\\n- depth(可选,默认 3,仅 page 模式生效):向下展开深度(-1 无限)' +
@@ -352,7 +357,7 @@ export function registerReadTools(server) {
352
357
  mode: z
353
358
  .enum(['focus', 'page'])
354
359
  .optional()
355
- .describe('视图模式:focus(聚焦)或 page(页面),默认 focus'),
360
+ .describe('视图模式:page(页面)或 focus(聚焦),默认 page'),
356
361
  ancestorLevels: z
357
362
  .number()
358
363
  .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.18",
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}) -->`+`