remnote-bridge 0.1.13 → 0.1.15

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.
Files changed (42) hide show
  1. package/README.md +147 -28
  2. package/README.zh-CN.md +374 -0
  3. package/dist/cli/commands/health.js +231 -112
  4. package/dist/cli/commands/read-rem-in-tree.js +84 -0
  5. package/dist/cli/config.js +2 -0
  6. package/dist/cli/daemon/registry.js +8 -0
  7. package/dist/cli/handlers/edit-handler.js +14 -0
  8. package/dist/cli/handlers/patch-engine.js +347 -0
  9. package/dist/cli/handlers/read-handler.js +2 -53
  10. package/dist/cli/handlers/rem-field-filter.js +102 -0
  11. package/dist/cli/handlers/tree-edit-handler.js +67 -7
  12. package/dist/cli/handlers/tree-read-handler.js +4 -1
  13. package/dist/cli/handlers/tree-rem-read-handler.js +73 -0
  14. package/dist/cli/main.js +53 -2
  15. package/dist/cli/server/ws-server.js +9 -1
  16. package/dist/mcp/daemon-client.js +22 -2
  17. package/dist/mcp/instructions.js +99 -58
  18. package/dist/mcp/tools/edit-tools.js +7 -2
  19. package/dist/mcp/tools/infra-tools.js +20 -11
  20. package/dist/mcp/tools/read-tools.js +88 -2
  21. package/package.json +1 -1
  22. package/remnote-plugin/dist/index-sandbox.js +24 -24
  23. package/remnote-plugin/dist/index.js +24 -24
  24. package/remnote-plugin/dist/manifest.json +1 -1
  25. package/remnote-plugin/package.json +1 -1
  26. package/remnote-plugin/public/manifest.json +1 -1
  27. package/remnote-plugin/src/bridge/message-router.ts +3 -0
  28. package/remnote-plugin/src/services/read-rem-in-tree.ts +43 -0
  29. package/remnote-plugin/src/services/read-rem.ts +31 -16
  30. package/remnote-plugin/src/services/read-tree.ts +5 -0
  31. package/remnote-plugin/src/settings.ts +1 -1
  32. package/skills/remnote-bridge/SKILL.md +50 -8
  33. package/skills/remnote-bridge/instructions/connect.md +31 -8
  34. package/skills/remnote-bridge/instructions/disconnect.md +5 -0
  35. package/skills/remnote-bridge/instructions/edit-tree.md +117 -51
  36. package/skills/remnote-bridge/instructions/health.md +81 -53
  37. package/skills/remnote-bridge/instructions/overall.md +39 -8
  38. package/skills/remnote-bridge/instructions/read-rem-in-tree.md +100 -0
  39. package/skills/remnote-bridge/instructions/read-rem.md +30 -11
  40. package/skills/remnote-bridge-test/SKILL.md +847 -0
  41. package/skills/remnote-bridge-test/references/regression-suite.md +960 -0
  42. package/skills/remnote-bridge-test/references/verification-guide.md +161 -0
@@ -2,16 +2,21 @@
2
2
  * health 命令
3
3
  *
4
4
  * 检查守护进程、Plugin 连接、SDK 状态,输出 ✅/❌ 列表。
5
- * - 全部健康 → 退出码 0
6
- * - 部分不健康 → 退出码 1
7
- * - 守护进程不可达 → 退出码 2
5
+ *
6
+ * 两种模式:
7
+ * - 全量模式(默认):遍历注册表所有活跃实例,逐个查询状态
8
+ * - 单实例模式(--instance / --headless):只查询指定实例
9
+ *
10
+ * 退出码:
11
+ * - 0:全部健康
12
+ * - 1:部分不健康
13
+ * - 2:无活跃实例 / 守护进程不可达
8
14
  */
9
15
  import { resolveInstanceId, loadRegistry, cleanStaleSlots, findSlotByInstance, } from '../daemon/registry.js';
10
16
  import { sendDaemonRequest } from '../daemon/send-request.js';
11
17
  import { jsonOutput } from '../utils/output.js';
12
18
  export async function healthCommand(options = {}) {
13
19
  const { json, diagnose, reload } = options;
14
- const instanceId = resolveInstanceId(options.instance);
15
20
  // --diagnose 和 --reload 不能同时使用
16
21
  if (diagnose && reload) {
17
22
  const error = '--diagnose 和 --reload 不能同时使用';
@@ -24,7 +29,44 @@ export async function healthCommand(options = {}) {
24
29
  process.exitCode = 1;
25
30
  return;
26
31
  }
27
- // 通过注册表检查 daemon 是否运行
32
+ // --diagnose / --reload 走单实例模式(不变)
33
+ if (diagnose || reload) {
34
+ const instanceId = resolveInstanceId(options.instance);
35
+ const registry = loadRegistry();
36
+ cleanStaleSlots(registry);
37
+ const entry = findSlotByInstance(registry, instanceId);
38
+ if (!entry) {
39
+ const error = `守护进程未运行(实例: ${instanceId}),请先执行 remnote-bridge connect`;
40
+ if (json) {
41
+ jsonOutput({ ok: false, command: 'health', error });
42
+ }
43
+ else {
44
+ console.error(error);
45
+ }
46
+ process.exitCode = 2;
47
+ return;
48
+ }
49
+ if (diagnose) {
50
+ await handleDiagnose(entry, instanceId, options);
51
+ }
52
+ else {
53
+ await handleReload(entry, instanceId, options);
54
+ }
55
+ return;
56
+ }
57
+ // 判断全量 vs 单实例
58
+ const isExplicitInstance = !!options.instance || process.env.REMNOTE_HEADLESS === '1';
59
+ if (isExplicitInstance) {
60
+ await handleSingleInstance(options);
61
+ }
62
+ else {
63
+ await handleAllInstances(options);
64
+ }
65
+ }
66
+ // ── 单实例模式 ──
67
+ async function handleSingleInstance(options) {
68
+ const { json } = options;
69
+ const instanceId = resolveInstanceId(options.instance);
28
70
  const registry = loadRegistry();
29
71
  cleanStaleSlots(registry);
30
72
  const entry = findSlotByInstance(registry, instanceId);
@@ -36,6 +78,7 @@ export async function healthCommand(options = {}) {
36
78
  daemon: { running: false },
37
79
  plugin: { connected: false },
38
80
  sdk: { ready: false },
81
+ error: `守护进程未运行(实例: ${instanceId}),请先执行 remnote-bridge connect`,
39
82
  });
40
83
  }
41
84
  else {
@@ -47,97 +90,6 @@ export async function healthCommand(options = {}) {
47
90
  process.exitCode = 2;
48
91
  return;
49
92
  }
50
- // --diagnose 模式
51
- if (diagnose) {
52
- try {
53
- const result = await sendDaemonRequest('diagnose', {}, { instance: options.instance });
54
- if (!result) {
55
- const error = '非 headless 模式,不支持 --diagnose';
56
- if (json) {
57
- jsonOutput({ ok: false, command: 'health', error });
58
- }
59
- else {
60
- console.error(error);
61
- }
62
- process.exitCode = 1;
63
- return;
64
- }
65
- if (json) {
66
- jsonOutput({ ok: true, command: 'health', mode: 'diagnose', instance: instanceId, ...result });
67
- }
68
- else {
69
- console.log('=== Headless Chrome 诊断 ===');
70
- console.log(`状态: ${result.headless.status}`);
71
- console.log(`Chrome 连接: ${result.headless.chromeConnected ? '是' : '否'}`);
72
- console.log(`页面 URL: ${result.headless.pageUrl ?? '无'}`);
73
- console.log(`重载次数: ${result.headless.reloadCount}`);
74
- console.log(`Plugin 连接: ${result.pluginConnected ? '是' : '否'}`);
75
- console.log(`SDK 就绪: ${result.sdkReady ? '是' : '否'}`);
76
- if (result.screenshotPath) {
77
- console.log(`截图: ${result.screenshotPath}`);
78
- }
79
- if (result.headless.lastError) {
80
- console.log(`\n最近错误: ${result.headless.lastError}`);
81
- }
82
- if (result.headless.recentConsoleErrors.length > 0) {
83
- console.log('\nConsole 错误:');
84
- for (const err of result.headless.recentConsoleErrors) {
85
- console.log(` ${err}`);
86
- }
87
- }
88
- if (!result.headless.chromeConnected) {
89
- console.log('\n排查建议: Chrome 已断开,尝试 `health --reload` 重载');
90
- }
91
- else if (!result.pluginConnected) {
92
- console.log('\n排查建议: Chrome 运行中但 Plugin 未连接,可能页面加载异常,尝试 `health --reload`');
93
- }
94
- else if (!result.sdkReady) {
95
- console.log('\n排查建议: Plugin 已连接但 SDK 未就绪,等待几秒后重试');
96
- }
97
- }
98
- }
99
- catch (err) {
100
- const errorMsg = err instanceof Error ? err.message : String(err);
101
- if (json) {
102
- jsonOutput({ ok: false, command: 'health', error: errorMsg });
103
- }
104
- else {
105
- console.error(`诊断失败: ${errorMsg}`);
106
- }
107
- process.exitCode = 1;
108
- }
109
- return;
110
- }
111
- // --reload 模式
112
- if (reload) {
113
- try {
114
- const result = await sendDaemonRequest('headless_reload', {}, { instance: options.instance });
115
- if (json) {
116
- jsonOutput({ ok: result.ok, command: 'health', mode: 'reload', instance: instanceId, error: result.error });
117
- }
118
- else {
119
- if (result.ok) {
120
- console.log('Headless Chrome 页面已重载');
121
- }
122
- else {
123
- console.error(`重载失败: ${result.error}`);
124
- }
125
- }
126
- process.exitCode = result.ok ? 0 : 1;
127
- }
128
- catch (err) {
129
- const errorMsg = err instanceof Error ? err.message : String(err);
130
- if (json) {
131
- jsonOutput({ ok: false, command: 'health', mode: 'reload', error: errorMsg });
132
- }
133
- else {
134
- console.error(`重载失败: ${errorMsg}`);
135
- }
136
- process.exitCode = 1;
137
- }
138
- return;
139
- }
140
- // 通过 WS 连接守护进程获取状态
141
93
  let status;
142
94
  try {
143
95
  status = await sendDaemonRequest('get_status', {}, { instance: options.instance });
@@ -155,7 +107,7 @@ export async function healthCommand(options = {}) {
155
107
  });
156
108
  }
157
109
  else {
158
- console.log('❌ 守护进程 不可达');
110
+ console.log(`❌ 守护进程 不可达(实例: ${instanceId})`);
159
111
  console.log('❌ Plugin 未连接');
160
112
  console.log('❌ SDK 不可用');
161
113
  console.log(`\n错误: ${errorMsg}`);
@@ -163,7 +115,6 @@ export async function healthCommand(options = {}) {
163
115
  process.exitCode = 2;
164
116
  return;
165
117
  }
166
- // 退出码
167
118
  const allHealthy = status.pluginConnected && status.sdkReady;
168
119
  const exitCode = allHealthy ? 0 : 1;
169
120
  if (json) {
@@ -171,34 +122,202 @@ export async function healthCommand(options = {}) {
171
122
  ok: allHealthy, command: 'health', exitCode,
172
123
  instance: instanceId, slotIndex: entry.index,
173
124
  daemon: { running: true, pid: entry.pid, reachable: true, uptime: status.uptime },
174
- plugin: { connected: status.pluginConnected },
125
+ plugin: { connected: status.pluginConnected, isTwin: status.pluginIsTwin },
175
126
  sdk: { ready: status.sdkReady },
176
127
  timeoutRemaining: status.timeoutRemaining,
177
128
  ...(status.headless ? { headless: status.headless } : {}),
178
129
  });
179
130
  }
180
131
  else {
181
- console.log(`✅ 守护进程 运行中(PID: ${entry.pid},实例: ${instanceId},槽位: ${entry.index},已运行 ${formatUptime(status.uptime)})`);
182
- if (status.pluginConnected) {
183
- console.log('✅ Plugin 已连接');
132
+ printInstanceStatus(entry, instanceId, status);
133
+ }
134
+ process.exitCode = exitCode;
135
+ }
136
+ // ── 全量模式 ──
137
+ async function handleAllInstances(options) {
138
+ const { json } = options;
139
+ const registry = loadRegistry();
140
+ cleanStaleSlots(registry);
141
+ const activeEntries = registry.slots.filter((e) => e !== null);
142
+ if (activeEntries.length === 0) {
143
+ if (json) {
144
+ jsonOutput({
145
+ ok: false, command: 'health', exitCode: 2,
146
+ instances: [],
147
+ error: '没有活跃的实例,请执行 remnote-bridge connect 启动守护进程',
148
+ });
184
149
  }
185
150
  else {
186
- console.log(' Plugin 未连接');
151
+ console.log('没有活跃的实例。执行 `remnote-bridge connect` 启动守护进程。');
187
152
  }
188
- if (status.sdkReady) {
189
- console.log('✅ SDK 就绪');
153
+ process.exitCode = 2;
154
+ return;
155
+ }
156
+ const instances = [];
157
+ let allHealthy = true;
158
+ let anyUnreachable = false;
159
+ for (const entry of activeEntries) {
160
+ try {
161
+ const status = await sendDaemonRequest('get_status', {}, { instance: entry.instance });
162
+ const healthy = status.pluginConnected && status.sdkReady;
163
+ if (!healthy)
164
+ allHealthy = false;
165
+ instances.push({
166
+ instance: entry.instance,
167
+ slotIndex: entry.index,
168
+ daemon: { running: true, pid: entry.pid, reachable: true, uptime: status.uptime },
169
+ plugin: { connected: status.pluginConnected, isTwin: status.pluginIsTwin },
170
+ sdk: { ready: status.sdkReady },
171
+ timeoutRemaining: status.timeoutRemaining,
172
+ ...(status.headless ? { headless: status.headless } : {}),
173
+ });
174
+ if (!json) {
175
+ if (instances.length > 1)
176
+ console.log('');
177
+ printInstanceStatus(entry, entry.instance, status);
178
+ }
179
+ }
180
+ catch {
181
+ allHealthy = false;
182
+ anyUnreachable = true;
183
+ instances.push({
184
+ instance: entry.instance,
185
+ slotIndex: entry.index,
186
+ daemon: { running: true, pid: entry.pid, reachable: false },
187
+ plugin: { connected: false },
188
+ sdk: { ready: false },
189
+ });
190
+ if (!json) {
191
+ if (instances.length > 1)
192
+ console.log('');
193
+ console.log(`=== 实例: ${entry.instance}(槽位 ${entry.index})===`);
194
+ console.log(`❌ 守护进程 不可达(PID: ${entry.pid})`);
195
+ console.log('❌ Plugin 未连接');
196
+ console.log('❌ SDK 不可用');
197
+ }
198
+ }
199
+ }
200
+ const exitCode = allHealthy ? 0 : anyUnreachable ? 2 : 1;
201
+ if (json) {
202
+ jsonOutput({
203
+ ok: allHealthy, command: 'health', exitCode,
204
+ instances,
205
+ });
206
+ }
207
+ process.exitCode = exitCode;
208
+ }
209
+ // ── diagnose / reload ──
210
+ async function handleDiagnose(entry, instanceId, options) {
211
+ const { json } = options;
212
+ try {
213
+ const result = await sendDaemonRequest('diagnose', {}, { instance: options.instance });
214
+ if (!result) {
215
+ const error = '非 headless 模式,不支持 --diagnose';
216
+ if (json) {
217
+ jsonOutput({ ok: false, command: 'health', error });
218
+ }
219
+ else {
220
+ console.error(error);
221
+ }
222
+ process.exitCode = 1;
223
+ return;
224
+ }
225
+ if (json) {
226
+ jsonOutput({ ok: true, command: 'health', mode: 'diagnose', instance: instanceId, ...result });
227
+ }
228
+ else {
229
+ console.log('=== Headless Chrome 诊断 ===');
230
+ console.log(`状态: ${result.headless.status}`);
231
+ console.log(`Chrome 连接: ${result.headless.chromeConnected ? '是' : '否'}`);
232
+ console.log(`页面 URL: ${result.headless.pageUrl ?? '无'}`);
233
+ console.log(`重载次数: ${result.headless.reloadCount}`);
234
+ console.log(`Plugin 连接: ${result.pluginConnected ? '是' : '否'}`);
235
+ console.log(`SDK 就绪: ${result.sdkReady ? '是' : '否'}`);
236
+ if (result.screenshotPath) {
237
+ console.log(`截图: ${result.screenshotPath}`);
238
+ }
239
+ if (result.headless.lastError) {
240
+ console.log(`\n最近错误: ${result.headless.lastError}`);
241
+ }
242
+ if (result.headless.recentConsoleErrors.length > 0) {
243
+ console.log('\nConsole 错误:');
244
+ for (const err of result.headless.recentConsoleErrors) {
245
+ console.log(` ${err}`);
246
+ }
247
+ }
248
+ if (!result.headless.chromeConnected) {
249
+ console.log('\n排查建议: Chrome 已断开,尝试 `health --reload` 重载');
250
+ }
251
+ else if (!result.pluginConnected) {
252
+ console.log('\n排查建议: Chrome 运行中但 Plugin 未连接,可能页面加载异常,尝试 `health --reload`');
253
+ }
254
+ else if (!result.sdkReady) {
255
+ console.log('\n排查建议: Plugin 已连接但 SDK 未就绪,等待几秒后重试');
256
+ }
257
+ }
258
+ }
259
+ catch (err) {
260
+ const errorMsg = err instanceof Error ? err.message : String(err);
261
+ if (json) {
262
+ jsonOutput({ ok: false, command: 'health', error: errorMsg });
190
263
  }
191
264
  else {
192
- console.log('❌ SDK 未就绪');
265
+ console.error(`诊断失败: ${errorMsg}`);
266
+ }
267
+ process.exitCode = 1;
268
+ }
269
+ }
270
+ async function handleReload(entry, instanceId, options) {
271
+ const { json } = options;
272
+ try {
273
+ const result = await sendDaemonRequest('headless_reload', {}, { instance: options.instance });
274
+ if (json) {
275
+ jsonOutput({ ok: result.ok, command: 'health', mode: 'reload', instance: instanceId, error: result.error });
276
+ }
277
+ else {
278
+ if (result.ok) {
279
+ console.log('Headless Chrome 页面已重载');
280
+ }
281
+ else {
282
+ console.error(`重载失败: ${result.error}`);
283
+ }
193
284
  }
194
- if (status.headless) {
195
- const h = status.headless;
196
- const icon = h.status === 'running' ? '✅' : '❌';
197
- console.log(`${icon} Chrome ${h.status}${h.lastError ? ` (${h.lastError})` : ''}`);
285
+ process.exitCode = result.ok ? 0 : 1;
286
+ }
287
+ catch (err) {
288
+ const errorMsg = err instanceof Error ? err.message : String(err);
289
+ if (json) {
290
+ jsonOutput({ ok: false, command: 'health', mode: 'reload', error: errorMsg });
198
291
  }
199
- console.log(`\n超时: ${formatUptime(status.timeoutRemaining)} 后自动关闭`);
292
+ else {
293
+ console.error(`重载失败: ${errorMsg}`);
294
+ }
295
+ process.exitCode = 1;
200
296
  }
201
- process.exitCode = exitCode;
297
+ }
298
+ // ── 共享输出 ──
299
+ function printInstanceStatus(entry, instanceId, status) {
300
+ console.log(`=== 实例: ${instanceId}(槽位 ${entry.index})===`);
301
+ console.log(`✅ 守护进程 运行中(PID: ${entry.pid},已运行 ${formatUptime(status.uptime)})`);
302
+ if (status.pluginConnected) {
303
+ const twinLabel = status.pluginIsTwin ? '孪生' : '非孪生';
304
+ console.log(`✅ Plugin 已连接(${twinLabel})`);
305
+ }
306
+ else {
307
+ console.log('❌ Plugin 未连接');
308
+ }
309
+ if (status.sdkReady) {
310
+ console.log('✅ SDK 就绪');
311
+ }
312
+ else {
313
+ console.log('❌ SDK 未就绪');
314
+ }
315
+ if (status.headless) {
316
+ const h = status.headless;
317
+ const icon = h.status === 'running' ? '✅' : '❌';
318
+ console.log(`${icon} Chrome ${h.status}${h.lastError ? ` (${h.lastError})` : ''}`);
319
+ }
320
+ console.log(`超时: ${formatUptime(status.timeoutRemaining)} 后自动关闭`);
202
321
  }
203
322
  function formatUptime(seconds) {
204
323
  if (seconds < 60)
@@ -0,0 +1,84 @@
1
+ /**
2
+ * read-rem-in-tree 命令
3
+ *
4
+ * 一次调用同时完成 read-tree + 对树中每个 Rem 执行 read-rem。
5
+ * 返回 outline + remObjects,同时建立 tree 和 rem 双重缓存。
6
+ */
7
+ import { sendDaemonRequest } from '../daemon/send-request.js';
8
+ import { jsonOutput, handleCommandError } from '../utils/output.js';
9
+ export async function readRemInTreeCommand(remId, options = {}) {
10
+ const { json } = options;
11
+ const depth = options.depth !== undefined ? parseInt(options.depth, 10) : undefined;
12
+ const maxNodes = options.maxNodes !== undefined ? parseInt(options.maxNodes, 10) : undefined;
13
+ const maxSiblings = options.maxSiblings !== undefined ? parseInt(options.maxSiblings, 10) : undefined;
14
+ const ancestorLevels = options.ancestorLevels !== undefined ? parseInt(options.ancestorLevels, 10) : undefined;
15
+ if ((depth !== undefined && isNaN(depth)) || (maxNodes !== undefined && isNaN(maxNodes)) || (maxSiblings !== undefined && isNaN(maxSiblings)) || (ancestorLevels !== undefined && isNaN(ancestorLevels))) {
16
+ const errMsg = '--depth, --max-nodes, --max-siblings, --ancestor-levels must be numbers';
17
+ if (json) {
18
+ jsonOutput({ ok: false, command: 'read-rem-in-tree', error: errMsg });
19
+ }
20
+ else {
21
+ console.error(`错误: ${errMsg}`);
22
+ }
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ // 构造 payload
27
+ const payload = { remId, depth, maxNodes, maxSiblings, ancestorLevels };
28
+ if (options.includePowerup)
29
+ payload.includePowerup = true;
30
+ if (options.full)
31
+ payload.full = true;
32
+ if (options.fields) {
33
+ payload.fields = Array.isArray(options.fields)
34
+ ? options.fields
35
+ : options.fields.split(',').map(f => f.trim()).filter(Boolean);
36
+ }
37
+ let result;
38
+ try {
39
+ result = await sendDaemonRequest('read_rem_in_tree', payload);
40
+ }
41
+ catch (err) {
42
+ handleCommandError(err, 'read-rem-in-tree', json);
43
+ return;
44
+ }
45
+ const data = result;
46
+ const cacheOverridden = data._cacheOverridden;
47
+ delete data._cacheOverridden;
48
+ const powerupFiltered = data.powerupFiltered;
49
+ delete data.powerupFiltered;
50
+ const ancestors = data.ancestors;
51
+ delete data.ancestors;
52
+ if (json) {
53
+ jsonOutput({
54
+ ok: true, command: 'read-rem-in-tree', data,
55
+ ...(ancestors ? { ancestors } : {}),
56
+ ...(cacheOverridden ? { cacheOverridden } : {}),
57
+ ...(powerupFiltered ? { powerupFiltered } : {}),
58
+ });
59
+ }
60
+ else {
61
+ if (cacheOverridden) {
62
+ console.warn(`注意: Tree ${cacheOverridden.id} 的缓存(${cacheOverridden.previousCachedAt})已被本次 read 覆盖`);
63
+ }
64
+ if (powerupFiltered) {
65
+ console.warn(`⚠ 已过滤 Powerup 系统数据(${powerupFiltered.tags} 个 Tag、${powerupFiltered.children} 个隐藏子 Rem)。`);
66
+ }
67
+ if (ancestors && ancestors.length > 0) {
68
+ const pathParts = [...ancestors].reverse().map(a => {
69
+ const topMark = a.isTopLevel ? ' [top]' : '';
70
+ return `${a.name} (${a.id}${topMark})`;
71
+ });
72
+ console.log(`<!-- ancestors: ${pathParts.join(' > ')} -->`);
73
+ }
74
+ // 输出 outline
75
+ console.log(data.outline);
76
+ // 输出 remObjects 摘要
77
+ const remObjects = data.remObjects;
78
+ if (remObjects) {
79
+ const count = Object.keys(remObjects).length;
80
+ console.log(`\n--- ${count} RemObjects ---`);
81
+ console.log(JSON.stringify(remObjects, null, 2));
82
+ }
83
+ }
84
+ }
@@ -24,6 +24,7 @@ export const DEFAULT_DEFAULTS = {
24
24
  readTreeDepth: 3,
25
25
  readTreeAncestorLevels: 0,
26
26
  readTreeIncludePowerup: false,
27
+ readRemInTreeMaxNodes: 50,
27
28
  readGlobeDepth: -1,
28
29
  readContextMode: 'focus',
29
30
  readContextAncestorLevels: 2,
@@ -54,6 +55,7 @@ function mergeDefaults(parsed) {
54
55
  ? parsed.readTreeAncestorLevels : DEFAULT_DEFAULTS.readTreeAncestorLevels,
55
56
  readTreeIncludePowerup: typeof parsed.readTreeIncludePowerup === 'boolean'
56
57
  ? parsed.readTreeIncludePowerup : DEFAULT_DEFAULTS.readTreeIncludePowerup,
58
+ readRemInTreeMaxNodes: isPositiveNumber(parsed.readRemInTreeMaxNodes) ? parsed.readRemInTreeMaxNodes : DEFAULT_DEFAULTS.readRemInTreeMaxNodes,
57
59
  readGlobeDepth: typeof parsed.readGlobeDepth === 'number' ? parsed.readGlobeDepth : DEFAULT_DEFAULTS.readGlobeDepth,
58
60
  readContextMode: parsed.readContextMode === 'focus' || parsed.readContextMode === 'page'
59
61
  ? parsed.readContextMode : DEFAULT_DEFAULTS.readContextMode,
@@ -135,11 +135,19 @@ export function instanceLogPath(slotIndex) {
135
135
  * 解析实例标识。
136
136
  *
137
137
  * 优先级:REMNOTE_HEADLESS(最高,覆盖一切)> cliArg > REMNOTE_BRIDGE_INSTANCE > "default"
138
+ *
139
+ * "headless" 是保留实例名,只能通过 --headless 全局选项设置,不允许通过 --instance 或环境变量直接指定。
138
140
  */
139
141
  export function resolveInstanceId(cliArg) {
140
142
  // headless 模式覆盖 --instance,固定实例名
141
143
  if (process.env.REMNOTE_HEADLESS === '1')
142
144
  return 'headless';
145
+ // 禁止通过 --instance 或环境变量使用保留名 "headless"
146
+ const candidate = cliArg || process.env.REMNOTE_BRIDGE_INSTANCE;
147
+ if (candidate === 'headless') {
148
+ throw new Error('--instance headless 不是合法用法。请使用 --headless 全局选项。'
149
+ + '\n用法: remnote-bridge --headless connect → remnote-bridge --headless <命令>');
150
+ }
143
151
  if (cliArg)
144
152
  return cliArg;
145
153
  const fromEnv = process.env.REMNOTE_BRIDGE_INSTANCE;
@@ -63,6 +63,9 @@ export class EditHandler {
63
63
  const currentJson = JSON.stringify(currentRemObject, null, 2);
64
64
  const cachedJson = JSON.stringify(cachedObj, null, 2);
65
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)'}`);
66
69
  // 不更新缓存 — 迫使 AI re-read
67
70
  throw new Error(`Rem ${remId} has been modified since last read. Please read it again before editing.`);
68
71
  }
@@ -126,3 +129,14 @@ export class EditHandler {
126
129
  };
127
130
  }
128
131
  }
132
+ /** 比较两个 RemObject 的顶层字段,返回值不同的 key 列表 */
133
+ function diffFields(cached, current) {
134
+ const allKeys = new Set([...Object.keys(cached), ...Object.keys(current)]);
135
+ const changed = [];
136
+ for (const key of allKeys) {
137
+ if (JSON.stringify(cached[key]) !== JSON.stringify(current[key])) {
138
+ changed.push(key);
139
+ }
140
+ }
141
+ return changed;
142
+ }