remnote-bridge 0.1.11 → 0.1.12

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 (64) hide show
  1. package/dist/cli/addon/addon-manager.js +163 -0
  2. package/dist/cli/addon/registry.js +24 -0
  3. package/dist/cli/commands/addon.js +149 -0
  4. package/dist/cli/commands/clean.js +121 -52
  5. package/dist/cli/commands/connect.js +72 -33
  6. package/dist/cli/commands/disconnect.js +19 -19
  7. package/dist/cli/commands/edit-rem.js +3 -31
  8. package/dist/cli/commands/edit-tree.js +3 -20
  9. package/dist/cli/commands/health.js +19 -18
  10. package/dist/cli/commands/read-context.js +3 -20
  11. package/dist/cli/commands/read-globe.js +3 -20
  12. package/dist/cli/commands/read-rem.js +3 -31
  13. package/dist/cli/commands/read-tree.js +3 -20
  14. package/dist/cli/commands/search.js +97 -21
  15. package/dist/cli/config.js +148 -72
  16. package/dist/cli/daemon/daemon.js +104 -24
  17. package/dist/cli/daemon/dev-server.js +9 -1
  18. package/dist/cli/daemon/pid.js +36 -22
  19. package/dist/cli/daemon/registry.js +160 -0
  20. package/dist/cli/daemon/send-request.js +11 -11
  21. package/dist/cli/daemon/static-server.js +97 -34
  22. package/dist/cli/handlers/read-handler.js +4 -3
  23. package/dist/cli/handlers/tree-parser.js +16 -9
  24. package/dist/cli/main.js +49 -9
  25. package/dist/cli/protocol.js +18 -4
  26. package/dist/cli/server/config-server.js +280 -14
  27. package/dist/cli/server/ws-server.js +93 -44
  28. package/dist/cli/utils/output.js +29 -0
  29. package/dist/mcp/instructions.js +101 -9
  30. package/dist/mcp/resources/edit-rem-guide.js +3 -4
  31. package/dist/mcp/resources/error-reference.js +2 -2
  32. package/dist/mcp/resources/rem-object-fields.js +3 -3
  33. package/dist/mcp/tools/infra-tools.js +54 -6
  34. package/dist/mcp/tools/read-tools.js +9 -2
  35. package/package.json +2 -2
  36. package/remnote-plugin/dist/bridge_widget-sandbox.js +17 -17
  37. package/remnote-plugin/dist/bridge_widget.js +17 -17
  38. package/remnote-plugin/dist/index-sandbox.js +31 -31
  39. package/remnote-plugin/dist/index.js +31 -31
  40. package/remnote-plugin/dist/manifest.json +1 -1
  41. package/remnote-plugin/package.json +1 -1
  42. package/remnote-plugin/public/manifest.json +1 -1
  43. package/remnote-plugin/src/bridge/multi-connection-manager.ts +151 -0
  44. package/remnote-plugin/src/bridge/websocket-client.ts +62 -16
  45. package/remnote-plugin/src/services/index.ts +0 -8
  46. package/remnote-plugin/src/services/read-rem.ts +1 -9
  47. package/remnote-plugin/src/services/search.ts +13 -10
  48. package/remnote-plugin/src/settings.ts +9 -7
  49. package/remnote-plugin/src/utils/index.ts +0 -5
  50. package/remnote-plugin/src/widgets/bridge_widget.tsx +105 -20
  51. package/remnote-plugin/src/widgets/index.tsx +41 -44
  52. package/remnote-plugin/webpack.config.js +35 -0
  53. package/skills/remnote-bridge/SKILL.md +14 -9
  54. package/skills/remnote-bridge/instructions/addon.md +134 -0
  55. package/skills/remnote-bridge/instructions/clean.md +110 -0
  56. package/skills/remnote-bridge/instructions/connect.md +80 -37
  57. package/skills/remnote-bridge/instructions/disconnect.md +22 -9
  58. package/skills/remnote-bridge/instructions/edit-rem.md +37 -9
  59. package/skills/remnote-bridge/instructions/health.md +23 -13
  60. package/skills/remnote-bridge/instructions/install-skill.md +58 -0
  61. package/skills/remnote-bridge/instructions/overall.md +76 -21
  62. package/skills/remnote-bridge/instructions/read-rem.md +10 -10
  63. package/skills/remnote-bridge/instructions/search.md +73 -14
  64. package/skills/remnote-bridge/instructions/setup.md +1 -1
@@ -5,6 +5,7 @@
5
5
  * - 已在运行 → 打印提示,退出码 0
6
6
  * - stale PID → 清理后正常启动
7
7
  * - 启动失败 → 退出码 1
8
+ * - 槽位满载 → 报错,退出码 1
8
9
  *
9
10
  * 默认使用静态文件服务器 serve 预构建 plugin。
10
11
  * --dev 模式使用 webpack-dev-server(支持 HMR)。
@@ -12,8 +13,8 @@
12
13
  import path from 'path';
13
14
  import fs from 'fs';
14
15
  import { fork } from 'child_process';
15
- import { loadConfig, pidFilePath, findProjectRoot } from '../config.js';
16
- import { checkDaemon } from '../daemon/pid.js';
16
+ import { loadConfig, ensureGlobalDir } from '../config.js';
17
+ import { resolveInstanceId, loadRegistry, saveRegistry, cleanStaleSlots, findSlotByInstance, allocateSlot, releaseSlot, formatSlotsFullError, } from '../daemon/registry.js';
17
18
  import { getSetupDonePath, cleanupOrphanChrome } from '../daemon/headless-browser.js';
18
19
  import { jsonOutput } from '../utils/output.js';
19
20
  function isDaemonMessage(msg) {
@@ -23,12 +24,13 @@ function isDaemonMessage(msg) {
23
24
  }
24
25
  export async function connectCommand(options = {}) {
25
26
  const { json } = options;
26
- const projectRoot = findProjectRoot();
27
- const config = loadConfig(projectRoot);
28
- const pidPath = pidFilePath(projectRoot);
27
+ const instanceId = resolveInstanceId(options.instance);
28
+ const config = loadConfig();
29
+ ensureGlobalDir();
30
+ // headless 从全局参数 / 环境变量读取(preAction 已同步到 env)
31
+ const headless = process.env.REMNOTE_HEADLESS === '1';
29
32
  // headless 前置检查
30
- if (options.headless) {
31
- // 清理上次可能残留的孤儿 Chrome 进程
33
+ if (headless) {
32
34
  cleanupOrphanChrome(json ? undefined : (msg) => console.log(msg));
33
35
  const setupDonePath = getSetupDonePath();
34
36
  if (!fs.existsSync(setupDonePath)) {
@@ -43,52 +45,74 @@ export async function connectCommand(options = {}) {
43
45
  return;
44
46
  }
45
47
  }
46
- // 检查是否已在运行
47
- const status = checkDaemon(pidPath);
48
- if (status.running) {
48
+ // 加载注册表并清理 stale 槽位
49
+ const registry = loadRegistry();
50
+ cleanStaleSlots(registry);
51
+ // 检查当前 instance 是否已在运行
52
+ const existing = findSlotByInstance(registry, instanceId);
53
+ if (existing) {
49
54
  if (json) {
50
55
  jsonOutput({
51
56
  ok: true, command: 'connect', alreadyRunning: true,
52
- pid: status.pid, wsPort: config.wsPort, devServerPort: config.devServerPort,
57
+ instance: instanceId,
58
+ pid: existing.pid, wsPort: existing.wsPort, devServerPort: existing.devServerPort,
59
+ configPort: existing.configPort, slotIndex: existing.index,
53
60
  });
54
61
  }
55
62
  else {
56
- console.log(`守护进程已在运行(PID: ${status.pid})`);
63
+ console.log(`守护进程已在运行(PID: ${existing.pid},实例: ${instanceId},槽位: ${existing.index})`);
57
64
  }
58
65
  process.exitCode = 0;
59
66
  return;
60
67
  }
68
+ // 分配空闲槽位(先用 pid=0 占位,daemon ready 后更新)
69
+ const slot = allocateSlot(registry, instanceId, 0);
70
+ if (!slot) {
71
+ const error = formatSlotsFullError(registry);
72
+ if (json) {
73
+ jsonOutput({ ok: false, command: 'connect', error: '已达最大实例数上限(4),无可用槽位' });
74
+ }
75
+ else {
76
+ console.error(error);
77
+ }
78
+ process.exitCode = 1;
79
+ return;
80
+ }
61
81
  // fork 守护进程
62
82
  const daemonScriptJs = path.resolve(import.meta.dirname, '..', 'daemon', 'daemon.js');
63
83
  const daemonScriptTs = path.resolve(import.meta.dirname, '..', 'daemon', 'daemon.ts');
64
84
  let scriptPath;
65
85
  let execArgv = [];
66
- // 判断是 ts 开发模式还是 js 构建后模式
67
86
  if (fs.existsSync(daemonScriptJs)) {
68
87
  scriptPath = daemonScriptJs;
69
88
  }
70
89
  else {
71
90
  scriptPath = daemonScriptTs;
72
- // 继承父进程的 tsx loader 参数(排除 --eval 相关项)
73
91
  execArgv = process.execArgv.filter((arg) => !arg.startsWith('--eval') && !arg.includes('const '));
74
92
  }
93
+ // 保存预分配端口(用于后续比较是否发生回退)
94
+ const originalWsPort = slot.wsPort;
95
+ const originalDevPort = slot.devServerPort;
75
96
  if (!json) {
76
- console.log('正在启动守护进程...');
97
+ console.log(`正在启动守护进程(实例: ${instanceId},槽位: ${slot.index})...`);
77
98
  }
78
99
  const child = fork(scriptPath, [], {
79
100
  detached: true,
80
101
  stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
81
- cwd: projectRoot,
82
102
  execArgv,
83
103
  env: {
84
104
  ...process.env,
85
105
  REMNOTE_BRIDGE_DEV: options.dev ? '1' : '0',
86
- ...(options.headless ? { REMNOTE_HEADLESS: '1' } : {}),
106
+ ...(headless ? { REMNOTE_HEADLESS: '1' } : {}),
87
107
  ...(options.remoteDebuggingPort ? { REMNOTE_HEADLESS_REMOTE_PORT: String(options.remoteDebuggingPort) } : {}),
108
+ SLOT_INDEX: String(slot.index),
109
+ SLOT_WS_PORT: String(slot.wsPort),
110
+ SLOT_DEV_PORT: String(slot.devServerPort),
111
+ SLOT_CONFIG_PORT: String(slot.configPort),
112
+ REMNOTE_BRIDGE_INSTANCE: instanceId,
88
113
  },
89
114
  });
90
115
  // 等待就绪信号,超时 60 秒
91
- // 首次启动可能需要安装 remnote-plugin 依赖(npm install),在 Windows 上可能需要较长时间
92
116
  const ready = await new Promise((resolve) => {
93
117
  const timeout = setTimeout(() => {
94
118
  resolve(null);
@@ -113,37 +137,41 @@ export async function connectCommand(options = {}) {
113
137
  // 断开与子进程的连接(让 CLI 进程可以退出)
114
138
  child.unref();
115
139
  child.disconnect?.();
116
- if (!ready) {
117
- if (json) {
118
- jsonOutput({ ok: false, command: 'connect', error: '守护进程启动超时(60 秒)' });
119
- }
120
- else {
121
- console.error('守护进程启动超时(60 秒)');
122
- }
123
- process.exitCode = 1;
124
- return;
125
- }
126
- if (ready.type === 'error') {
140
+ if (!ready || ready.type === 'error') {
141
+ // 启动失败,释放槽位
142
+ releaseSlot(registry, instanceId);
143
+ const errorMsg = !ready
144
+ ? '守护进程启动超时(60 秒)'
145
+ : ready.message;
127
146
  if (json) {
128
- jsonOutput({ ok: false, command: 'connect', error: ready.message });
147
+ jsonOutput({ ok: false, command: 'connect', error: errorMsg });
129
148
  }
130
149
  else {
131
- console.error(`守护进程启动失败: ${ready.message}`);
150
+ console.error(`守护进程启动失败: ${errorMsg}`);
132
151
  }
133
152
  process.exitCode = 1;
134
153
  return;
135
154
  }
155
+ // 更新注册表中的 PID 和实际端口(可能与预分配不同,若原端口被占用则 OS 自动分配)
156
+ slot.pid = ready.pid;
157
+ slot.wsPort = ready.wsPort;
158
+ slot.devServerPort = ready.devServerPort;
159
+ slot.configPort = ready.configPort;
160
+ saveRegistry(registry);
161
+ const portChanged = ready.wsPort !== originalWsPort || ready.devServerPort !== originalDevPort;
136
162
  if (json) {
137
163
  jsonOutput({
138
164
  ok: true, command: 'connect', alreadyRunning: false,
165
+ instance: instanceId,
139
166
  pid: ready.pid, wsPort: ready.wsPort, devServerPort: ready.devServerPort,
140
- configPort: ready.configPort,
167
+ configPort: ready.configPort, slotIndex: slot.index,
141
168
  timeoutMinutes: config.daemonTimeoutMinutes,
142
169
  headless: ready.headless ?? false,
170
+ portChanged,
143
171
  });
144
172
  }
145
173
  else {
146
- console.log(`守护进程已启动(PID: ${ready.pid})`);
174
+ console.log(`守护进程已启动(PID: ${ready.pid},实例: ${instanceId})`);
147
175
  console.log(` WS Server: ws://127.0.0.1:${ready.wsPort}`);
148
176
  console.log(` Plugin 服务: http://localhost:${ready.devServerPort}`);
149
177
  console.log(` 配置页面: http://127.0.0.1:${ready.configPort}`);
@@ -151,6 +179,17 @@ export async function connectCommand(options = {}) {
151
179
  console.log(` Headless Chrome: 已启动(自动加载 Plugin)`);
152
180
  }
153
181
  console.log(` 超时: ${config.daemonTimeoutMinutes} 分钟无 CLI 交互后自动关闭`);
182
+ // 端口变更提示(标准模式)
183
+ if (ready.wsPort !== originalWsPort) {
184
+ console.log('');
185
+ console.log(`⚠ WS 端口被占用,已回退到 ${ready.wsPort}(Plugin 将自动发现新端口)`);
186
+ }
187
+ if (ready.devServerPort !== originalDevPort) {
188
+ console.log('');
189
+ console.log(`⚠ Plugin 服务端口被占用,已回退到 ${ready.devServerPort}`);
190
+ console.log(' 请在 RemNote 中更新 Native Plugin URL 为:');
191
+ console.log(` http://localhost:${ready.devServerPort}`);
192
+ }
154
193
  }
155
194
  process.exitCode = 0;
156
195
  }
@@ -5,40 +5,43 @@
5
5
  * - 守护进程运行中 → 发送 SIGTERM,等待退出
6
6
  * - 守护进程未运行 → 打印提示,退出码 0
7
7
  */
8
- import { pidFilePath, findProjectRoot } from '../config.js';
9
- import { checkDaemon, removePid } from '../daemon/pid.js';
8
+ import { resolveInstanceId, loadRegistry, cleanStaleSlots, findSlotByInstance, releaseSlot, instancePidPath, } from '../daemon/registry.js';
9
+ import { removePid } from '../daemon/pid.js';
10
10
  import { cleanupOrphanChrome } from '../daemon/headless-browser.js';
11
11
  import { jsonOutput } from '../utils/output.js';
12
12
  const WAIT_TIMEOUT_MS = 10_000;
13
13
  const POLL_INTERVAL_MS = 200;
14
14
  export async function disconnectCommand(options = {}) {
15
15
  const { json } = options;
16
- const projectRoot = findProjectRoot();
17
- const pidPath = pidFilePath(projectRoot);
18
- const status = checkDaemon(pidPath);
19
- if (!status.running) {
16
+ const instanceId = resolveInstanceId(options.instance);
17
+ const registry = loadRegistry();
18
+ cleanStaleSlots(registry);
19
+ const entry = findSlotByInstance(registry, instanceId);
20
+ if (!entry) {
20
21
  if (json) {
21
- jsonOutput({ ok: true, command: 'disconnect', wasRunning: false });
22
+ jsonOutput({ ok: true, command: 'disconnect', wasRunning: false, instance: instanceId });
22
23
  }
23
24
  else {
24
- console.log('守护进程未在运行');
25
+ console.log(`守护进程未在运行(实例: ${instanceId})`);
25
26
  }
26
27
  process.exitCode = 0;
27
28
  return;
28
29
  }
29
- const pid = status.pid;
30
+ const pid = entry.pid;
31
+ const pidPath = instancePidPath(entry.index);
30
32
  if (!json) {
31
- console.log(`正在停止守护进程(PID: ${pid})...`);
33
+ console.log(`正在停止守护进程(PID: ${pid},实例: ${instanceId})...`);
32
34
  }
33
35
  // 发送 SIGTERM
34
36
  try {
35
37
  process.kill(pid, 'SIGTERM');
36
38
  }
37
- catch (err) {
39
+ catch {
38
40
  // 进程可能已经退出
39
41
  removePid(pidPath);
42
+ releaseSlot(registry, instanceId);
40
43
  if (json) {
41
- jsonOutput({ ok: true, command: 'disconnect', wasRunning: true, pid, forced: false });
44
+ jsonOutput({ ok: true, command: 'disconnect', wasRunning: true, instance: instanceId, pid, forced: false });
42
45
  }
43
46
  else {
44
47
  console.log('守护进程已停止');
@@ -50,10 +53,10 @@ export async function disconnectCommand(options = {}) {
50
53
  const exited = await waitForExit(pid, WAIT_TIMEOUT_MS);
51
54
  if (exited) {
52
55
  removePid(pidPath);
53
- // 清理可能残留的孤儿 headless Chrome
56
+ releaseSlot(registry, instanceId);
54
57
  cleanupOrphanChrome(json ? undefined : (msg) => console.log(msg));
55
58
  if (json) {
56
- jsonOutput({ ok: true, command: 'disconnect', wasRunning: true, pid, forced: false });
59
+ jsonOutput({ ok: true, command: 'disconnect', wasRunning: true, instance: instanceId, pid, forced: false });
57
60
  }
58
61
  else {
59
62
  console.log('守护进程已停止');
@@ -68,10 +71,10 @@ export async function disconnectCommand(options = {}) {
68
71
  // 可能已退出
69
72
  }
70
73
  removePid(pidPath);
71
- // daemon 被强杀后 Chrome 更可能成为孤儿,务必清理
74
+ releaseSlot(registry, instanceId);
72
75
  cleanupOrphanChrome(json ? undefined : (msg) => console.log(msg));
73
76
  if (json) {
74
- jsonOutput({ ok: true, command: 'disconnect', wasRunning: true, pid, forced: true });
77
+ jsonOutput({ ok: true, command: 'disconnect', wasRunning: true, instance: instanceId, pid, forced: true });
75
78
  }
76
79
  else {
77
80
  console.error(`守护进程未在 ${WAIT_TIMEOUT_MS / 1000} 秒内退出,尝试强制终止...`);
@@ -85,9 +88,7 @@ function waitForExit(pid, timeoutMs) {
85
88
  const start = Date.now();
86
89
  const check = () => {
87
90
  try {
88
- // kill(pid, 0) 不发信号,只检查进程是否存在
89
91
  process.kill(pid, 0);
90
- // 进程仍在运行
91
92
  if (Date.now() - start >= timeoutMs) {
92
93
  resolve(false);
93
94
  }
@@ -96,7 +97,6 @@ function waitForExit(pid, timeoutMs) {
96
97
  }
97
98
  }
98
99
  catch {
99
- // 进程已退出
100
100
  resolve(true);
101
101
  }
102
102
  };
@@ -5,8 +5,8 @@
5
5
  * 三道防线保证安全:缓存存在性、并发检测、精确匹配。
6
6
  * - 退出码:0 成功 / 1 业务错误 / 2 守护进程不可达
7
7
  */
8
- import { sendDaemonRequest, DaemonNotRunningError, DaemonUnreachableError } from '../daemon/send-request.js';
9
- import { jsonOutput } from '../utils/output.js';
8
+ import { sendDaemonRequest } from '../daemon/send-request.js';
9
+ import { jsonOutput, handleCommandError } from '../utils/output.js';
10
10
  export async function editRemCommand(remId, options) {
11
11
  const { json, oldStr, newStr } = options;
12
12
  let result;
@@ -14,35 +14,7 @@ export async function editRemCommand(remId, options) {
14
14
  result = await sendDaemonRequest('edit_rem', { remId, oldStr, newStr });
15
15
  }
16
16
  catch (err) {
17
- if (err instanceof DaemonNotRunningError) {
18
- if (json) {
19
- jsonOutput({ ok: false, command: 'edit-rem', error: err.message });
20
- }
21
- else {
22
- console.error(`错误: ${err.message}`);
23
- }
24
- process.exitCode = 2;
25
- return;
26
- }
27
- if (err instanceof DaemonUnreachableError) {
28
- if (json) {
29
- jsonOutput({ ok: false, command: 'edit-rem', error: err.message });
30
- }
31
- else {
32
- console.error(`错误: ${err.message}`);
33
- }
34
- process.exitCode = 2;
35
- return;
36
- }
37
- // 业务错误(防线拒绝、Plugin 未连接等)
38
- const errorMsg = err instanceof Error ? err.message : String(err);
39
- if (json) {
40
- jsonOutput({ ok: false, command: 'edit-rem', error: errorMsg });
41
- }
42
- else {
43
- console.error(`错误: ${errorMsg}`);
44
- }
45
- process.exitCode = 1;
17
+ handleCommandError(err, 'edit-rem', json);
46
18
  return;
47
19
  }
48
20
  const editResult = result;
@@ -6,8 +6,8 @@
6
6
  * - --json 结构化 JSON 输出
7
7
  * - 退出码:0 成功 / 1 业务错误 / 2 守护进程不可达
8
8
  */
9
- import { sendDaemonRequest, DaemonNotRunningError, DaemonUnreachableError } from '../daemon/send-request.js';
10
- import { jsonOutput } from '../utils/output.js';
9
+ import { sendDaemonRequest } from '../daemon/send-request.js';
10
+ import { jsonOutput, handleCommandError } from '../utils/output.js';
11
11
  export async function editTreeCommand(remId, options) {
12
12
  const { json, oldStr, newStr } = options;
13
13
  let result;
@@ -15,24 +15,7 @@ export async function editTreeCommand(remId, options) {
15
15
  result = await sendDaemonRequest('edit_tree', { remId, oldStr, newStr });
16
16
  }
17
17
  catch (err) {
18
- if (err instanceof DaemonNotRunningError || err instanceof DaemonUnreachableError) {
19
- if (json) {
20
- jsonOutput({ ok: false, command: 'edit-tree', error: err.message });
21
- }
22
- else {
23
- console.error(`错误: ${err.message}`);
24
- }
25
- process.exitCode = 2;
26
- return;
27
- }
28
- const errorMsg = err instanceof Error ? err.message : String(err);
29
- if (json) {
30
- jsonOutput({ ok: false, command: 'edit-tree', error: errorMsg });
31
- }
32
- else {
33
- console.error(`错误: ${errorMsg}`);
34
- }
35
- process.exitCode = 1;
18
+ handleCommandError(err, 'edit-tree', json);
36
19
  return;
37
20
  }
38
21
  const data = result;
@@ -6,12 +6,12 @@
6
6
  * - 部分不健康 → 退出码 1
7
7
  * - 守护进程不可达 → 退出码 2
8
8
  */
9
- import { findProjectRoot, pidFilePath } from '../config.js';
10
- import { checkDaemon } from '../daemon/pid.js';
9
+ import { resolveInstanceId, loadRegistry, cleanStaleSlots, findSlotByInstance, } from '../daemon/registry.js';
11
10
  import { sendDaemonRequest } from '../daemon/send-request.js';
12
11
  import { jsonOutput } from '../utils/output.js';
13
12
  export async function healthCommand(options = {}) {
14
13
  const { json, diagnose, reload } = options;
14
+ const instanceId = resolveInstanceId(options.instance);
15
15
  // --diagnose 和 --reload 不能同时使用
16
16
  if (diagnose && reload) {
17
17
  const error = '--diagnose 和 --reload 不能同时使用';
@@ -24,21 +24,22 @@ export async function healthCommand(options = {}) {
24
24
  process.exitCode = 1;
25
25
  return;
26
26
  }
27
- const projectRoot = findProjectRoot();
28
- const pidPath = pidFilePath(projectRoot);
29
- // 先检查 PID 文件
30
- const daemonStatus = checkDaemon(pidPath);
31
- if (!daemonStatus.running) {
27
+ // 通过注册表检查 daemon 是否运行
28
+ const registry = loadRegistry();
29
+ cleanStaleSlots(registry);
30
+ const entry = findSlotByInstance(registry, instanceId);
31
+ if (!entry) {
32
32
  if (json) {
33
33
  jsonOutput({
34
34
  ok: false, command: 'health', exitCode: 2,
35
+ instance: instanceId,
35
36
  daemon: { running: false },
36
37
  plugin: { connected: false },
37
38
  sdk: { ready: false },
38
39
  });
39
40
  }
40
41
  else {
41
- console.log('❌ 守护进程 未运行');
42
+ console.log(`❌ 守护进程 未运行(实例: ${instanceId})`);
42
43
  console.log('❌ Plugin 未连接');
43
44
  console.log('❌ SDK 不可用');
44
45
  console.log('\n提示: 执行 `remnote-bridge connect` 启动守护进程');
@@ -49,7 +50,7 @@ export async function healthCommand(options = {}) {
49
50
  // --diagnose 模式
50
51
  if (diagnose) {
51
52
  try {
52
- const result = await sendDaemonRequest('diagnose');
53
+ const result = await sendDaemonRequest('diagnose', {}, { instance: options.instance });
53
54
  if (!result) {
54
55
  const error = '非 headless 模式,不支持 --diagnose';
55
56
  if (json) {
@@ -62,7 +63,7 @@ export async function healthCommand(options = {}) {
62
63
  return;
63
64
  }
64
65
  if (json) {
65
- jsonOutput({ ok: true, command: 'health', mode: 'diagnose', ...result });
66
+ jsonOutput({ ok: true, command: 'health', mode: 'diagnose', instance: instanceId, ...result });
66
67
  }
67
68
  else {
68
69
  console.log('=== Headless Chrome 诊断 ===');
@@ -84,7 +85,6 @@ export async function healthCommand(options = {}) {
84
85
  console.log(` ${err}`);
85
86
  }
86
87
  }
87
- // 排查建议
88
88
  if (!result.headless.chromeConnected) {
89
89
  console.log('\n排查建议: Chrome 已断开,尝试 `health --reload` 重载');
90
90
  }
@@ -111,9 +111,9 @@ export async function healthCommand(options = {}) {
111
111
  // --reload 模式
112
112
  if (reload) {
113
113
  try {
114
- const result = await sendDaemonRequest('headless_reload');
114
+ const result = await sendDaemonRequest('headless_reload', {}, { instance: options.instance });
115
115
  if (json) {
116
- jsonOutput({ ok: result.ok, command: 'health', mode: 'reload', error: result.error });
116
+ jsonOutput({ ok: result.ok, command: 'health', mode: 'reload', instance: instanceId, error: result.error });
117
117
  }
118
118
  else {
119
119
  if (result.ok) {
@@ -140,14 +140,15 @@ export async function healthCommand(options = {}) {
140
140
  // 通过 WS 连接守护进程获取状态
141
141
  let status;
142
142
  try {
143
- status = await sendDaemonRequest('get_status');
143
+ status = await sendDaemonRequest('get_status', {}, { instance: options.instance });
144
144
  }
145
145
  catch (err) {
146
146
  const errorMsg = err instanceof Error ? err.message : String(err);
147
147
  if (json) {
148
148
  jsonOutput({
149
149
  ok: false, command: 'health', exitCode: 2,
150
- daemon: { running: true, pid: daemonStatus.pid, reachable: false },
150
+ instance: instanceId,
151
+ daemon: { running: true, pid: entry.pid, reachable: false },
151
152
  plugin: { connected: false },
152
153
  sdk: { ready: false },
153
154
  error: errorMsg,
@@ -168,7 +169,8 @@ export async function healthCommand(options = {}) {
168
169
  if (json) {
169
170
  jsonOutput({
170
171
  ok: allHealthy, command: 'health', exitCode,
171
- daemon: { running: true, pid: daemonStatus.pid, reachable: true, uptime: status.uptime },
172
+ instance: instanceId, slotIndex: entry.index,
173
+ daemon: { running: true, pid: entry.pid, reachable: true, uptime: status.uptime },
172
174
  plugin: { connected: status.pluginConnected },
173
175
  sdk: { ready: status.sdkReady },
174
176
  timeoutRemaining: status.timeoutRemaining,
@@ -176,7 +178,7 @@ export async function healthCommand(options = {}) {
176
178
  });
177
179
  }
178
180
  else {
179
- console.log(`✅ 守护进程 运行中(PID: ${daemonStatus.pid},已运行 ${formatUptime(status.uptime)})`);
181
+ console.log(`✅ 守护进程 运行中(PID: ${entry.pid},实例: ${instanceId},槽位: ${entry.index},已运行 ${formatUptime(status.uptime)})`);
180
182
  if (status.pluginConnected) {
181
183
  console.log('✅ Plugin 已连接');
182
184
  }
@@ -189,7 +191,6 @@ export async function healthCommand(options = {}) {
189
191
  else {
190
192
  console.log('❌ SDK 未就绪');
191
193
  }
192
- // Headless Chrome 状态行
193
194
  if (status.headless) {
194
195
  const h = status.headless;
195
196
  const icon = h.status === 'running' ? '✅' : '❌';
@@ -10,8 +10,8 @@
10
10
  * - --focus-rem-id <remId> 指定鱼眼中心 Rem ID(仅 focus 模式,默认使用当前焦点)
11
11
  * - --json 结构化 JSON 输出
12
12
  */
13
- import { sendDaemonRequest, DaemonNotRunningError, DaemonUnreachableError } from '../daemon/send-request.js';
14
- import { jsonOutput } from '../utils/output.js';
13
+ import { sendDaemonRequest } from '../daemon/send-request.js';
14
+ import { jsonOutput, handleCommandError } from '../utils/output.js';
15
15
  export async function readContextCommand(options = {}) {
16
16
  const { json } = options;
17
17
  const mode = options.mode || 'focus';
@@ -49,24 +49,7 @@ export async function readContextCommand(options = {}) {
49
49
  result = await sendDaemonRequest('read_context', reqPayload);
50
50
  }
51
51
  catch (err) {
52
- if (err instanceof DaemonNotRunningError || err instanceof DaemonUnreachableError) {
53
- if (json) {
54
- jsonOutput({ ok: false, command: 'read-context', error: err.message });
55
- }
56
- else {
57
- console.error(`错误: ${err.message}`);
58
- }
59
- process.exitCode = 2;
60
- return;
61
- }
62
- const errorMsg = err instanceof Error ? err.message : String(err);
63
- if (json) {
64
- jsonOutput({ ok: false, command: 'read-context', error: errorMsg });
65
- }
66
- else {
67
- console.error(`错误: ${errorMsg}`);
68
- }
69
- process.exitCode = 1;
52
+ handleCommandError(err, 'read-context', json);
70
53
  return;
71
54
  }
72
55
  const data = result;
@@ -7,8 +7,8 @@
7
7
  * - --max-siblings N 每个父节点下展示的 children 上限(默认 20)
8
8
  * - --json 结构化 JSON 输出
9
9
  */
10
- import { sendDaemonRequest, DaemonNotRunningError, DaemonUnreachableError } from '../daemon/send-request.js';
11
- import { jsonOutput } from '../utils/output.js';
10
+ import { sendDaemonRequest } from '../daemon/send-request.js';
11
+ import { jsonOutput, handleCommandError } from '../utils/output.js';
12
12
  export async function readGlobeCommand(options = {}) {
13
13
  const { json } = options;
14
14
  const depth = options.depth !== undefined ? parseInt(options.depth, 10) : undefined;
@@ -30,24 +30,7 @@ export async function readGlobeCommand(options = {}) {
30
30
  result = await sendDaemonRequest('read_globe', { depth, maxNodes, maxSiblings });
31
31
  }
32
32
  catch (err) {
33
- if (err instanceof DaemonNotRunningError || err instanceof DaemonUnreachableError) {
34
- if (json) {
35
- jsonOutput({ ok: false, command: 'read-globe', error: err.message });
36
- }
37
- else {
38
- console.error(`错误: ${err.message}`);
39
- }
40
- process.exitCode = 2;
41
- return;
42
- }
43
- const errorMsg = err instanceof Error ? err.message : String(err);
44
- if (json) {
45
- jsonOutput({ ok: false, command: 'read-globe', error: errorMsg });
46
- }
47
- else {
48
- console.error(`错误: ${errorMsg}`);
49
- }
50
- process.exitCode = 1;
33
+ handleCommandError(err, 'read-globe', json);
51
34
  return;
52
35
  }
53
36
  const data = result;
@@ -7,8 +7,8 @@
7
7
  * - --fields 指定输出字段子集
8
8
  * - 退出码:0 成功 / 1 业务错误 / 2 守护进程不可达
9
9
  */
10
- import { sendDaemonRequest, DaemonNotRunningError, DaemonUnreachableError } from '../daemon/send-request.js';
11
- import { jsonOutput } from '../utils/output.js';
10
+ import { sendDaemonRequest } from '../daemon/send-request.js';
11
+ import { jsonOutput, handleCommandError } from '../utils/output.js';
12
12
  export async function readRemCommand(remId, options = {}) {
13
13
  const { json, fields, full, includePowerup } = options;
14
14
  // 构造 payload
@@ -27,35 +27,7 @@ export async function readRemCommand(remId, options = {}) {
27
27
  result = await sendDaemonRequest('read_rem', payload);
28
28
  }
29
29
  catch (err) {
30
- if (err instanceof DaemonNotRunningError) {
31
- if (json) {
32
- jsonOutput({ ok: false, command: 'read-rem', error: err.message });
33
- }
34
- else {
35
- console.error(`错误: ${err.message}`);
36
- }
37
- process.exitCode = 2;
38
- return;
39
- }
40
- if (err instanceof DaemonUnreachableError) {
41
- if (json) {
42
- jsonOutput({ ok: false, command: 'read-rem', error: err.message });
43
- }
44
- else {
45
- console.error(`错误: ${err.message}`);
46
- }
47
- process.exitCode = 2;
48
- return;
49
- }
50
- // 业务错误(Rem not found, Plugin 未连接等)
51
- const errorMsg = err instanceof Error ? err.message : String(err);
52
- if (json) {
53
- jsonOutput({ ok: false, command: 'read-rem', error: errorMsg });
54
- }
55
- else {
56
- console.error(`错误: ${errorMsg}`);
57
- }
58
- process.exitCode = 1;
30
+ handleCommandError(err, 'read-rem', json);
59
31
  return;
60
32
  }
61
33
  const data = result;
@@ -6,8 +6,8 @@
6
6
  * - --json 结构化 JSON 输出
7
7
  * - 退出码:0 成功 / 1 业务错误 / 2 守护进程不可达
8
8
  */
9
- import { sendDaemonRequest, DaemonNotRunningError, DaemonUnreachableError } from '../daemon/send-request.js';
10
- import { jsonOutput } from '../utils/output.js';
9
+ import { sendDaemonRequest } from '../daemon/send-request.js';
10
+ import { jsonOutput, handleCommandError } from '../utils/output.js';
11
11
  export async function readTreeCommand(remId, options = {}) {
12
12
  const { json } = options;
13
13
  const depth = options.depth !== undefined ? parseInt(options.depth, 10) : undefined;
@@ -33,24 +33,7 @@ export async function readTreeCommand(remId, options = {}) {
33
33
  });
34
34
  }
35
35
  catch (err) {
36
- if (err instanceof DaemonNotRunningError || err instanceof DaemonUnreachableError) {
37
- if (json) {
38
- jsonOutput({ ok: false, command: 'read-tree', error: err.message });
39
- }
40
- else {
41
- console.error(`错误: ${err.message}`);
42
- }
43
- process.exitCode = 2;
44
- return;
45
- }
46
- const errorMsg = err instanceof Error ? err.message : String(err);
47
- if (json) {
48
- jsonOutput({ ok: false, command: 'read-tree', error: errorMsg });
49
- }
50
- else {
51
- console.error(`错误: ${errorMsg}`);
52
- }
53
- process.exitCode = 1;
36
+ handleCommandError(err, 'read-tree', json);
54
37
  return;
55
38
  }
56
39
  const data = result;