remnote-bridge 0.1.10 → 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.
- package/dist/cli/addon/addon-manager.js +163 -0
- package/dist/cli/addon/registry.js +24 -0
- package/dist/cli/commands/addon.js +149 -0
- package/dist/cli/commands/clean.js +121 -52
- package/dist/cli/commands/connect.js +72 -33
- package/dist/cli/commands/disconnect.js +19 -19
- package/dist/cli/commands/edit-rem.js +3 -31
- package/dist/cli/commands/edit-tree.js +3 -20
- package/dist/cli/commands/health.js +19 -18
- package/dist/cli/commands/read-context.js +8 -23
- package/dist/cli/commands/read-globe.js +3 -20
- package/dist/cli/commands/read-rem.js +3 -31
- package/dist/cli/commands/read-tree.js +3 -20
- package/dist/cli/commands/search.js +97 -21
- package/dist/cli/config.js +148 -72
- package/dist/cli/daemon/daemon.js +104 -24
- package/dist/cli/daemon/dev-server.js +9 -1
- package/dist/cli/daemon/pid.js +36 -22
- package/dist/cli/daemon/registry.js +160 -0
- package/dist/cli/daemon/send-request.js +11 -11
- package/dist/cli/daemon/static-server.js +97 -34
- package/dist/cli/handlers/context-read-handler.js +5 -3
- package/dist/cli/handlers/read-handler.js +4 -3
- package/dist/cli/handlers/tree-parser.js +16 -9
- package/dist/cli/main.js +51 -9
- package/dist/cli/protocol.js +18 -4
- package/dist/cli/server/config-server.js +280 -14
- package/dist/cli/server/ws-server.js +93 -44
- package/dist/cli/utils/output.js +29 -0
- package/dist/mcp/instructions.js +103 -10
- package/dist/mcp/resources/edit-rem-guide.js +3 -4
- package/dist/mcp/resources/error-reference.js +5 -3
- package/dist/mcp/resources/rem-object-fields.js +3 -3
- package/dist/mcp/tools/infra-tools.js +54 -6
- package/dist/mcp/tools/read-tools.js +16 -3
- package/package.json +2 -2
- package/remnote-plugin/dist/bridge_widget-sandbox.js +17 -17
- package/remnote-plugin/dist/bridge_widget.js +17 -17
- package/remnote-plugin/dist/index-sandbox.js +31 -31
- package/remnote-plugin/dist/index.js +31 -31
- package/remnote-plugin/dist/manifest.json +1 -1
- package/remnote-plugin/package.json +1 -1
- package/remnote-plugin/public/manifest.json +1 -1
- package/remnote-plugin/src/bridge/message-router.ts +1 -1
- package/remnote-plugin/src/bridge/multi-connection-manager.ts +151 -0
- package/remnote-plugin/src/bridge/websocket-client.ts +62 -16
- package/remnote-plugin/src/services/index.ts +0 -8
- package/remnote-plugin/src/services/read-context.ts +13 -4
- package/remnote-plugin/src/services/read-rem.ts +1 -9
- package/remnote-plugin/src/services/search.ts +13 -10
- package/remnote-plugin/src/settings.ts +9 -7
- package/remnote-plugin/src/utils/index.ts +0 -5
- package/remnote-plugin/src/widgets/bridge_widget.tsx +105 -20
- package/remnote-plugin/src/widgets/index.tsx +41 -44
- package/remnote-plugin/webpack.config.js +35 -0
- package/skills/remnote-bridge/SKILL.md +14 -9
- package/skills/remnote-bridge/instructions/addon.md +134 -0
- package/skills/remnote-bridge/instructions/clean.md +110 -0
- package/skills/remnote-bridge/instructions/connect.md +80 -37
- package/skills/remnote-bridge/instructions/disconnect.md +22 -9
- package/skills/remnote-bridge/instructions/edit-rem.md +37 -9
- package/skills/remnote-bridge/instructions/health.md +23 -13
- package/skills/remnote-bridge/instructions/install-skill.md +58 -0
- package/skills/remnote-bridge/instructions/overall.md +76 -21
- package/skills/remnote-bridge/instructions/read-context.md +34 -8
- package/skills/remnote-bridge/instructions/read-rem.md +10 -10
- package/skills/remnote-bridge/instructions/search.md +73 -14
- 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,
|
|
16
|
-
import {
|
|
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
|
|
27
|
-
const config = loadConfig(
|
|
28
|
-
|
|
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 (
|
|
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
|
|
48
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
...(
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
147
|
+
jsonOutput({ ok: false, command: 'connect', error: errorMsg });
|
|
129
148
|
}
|
|
130
149
|
else {
|
|
131
|
-
console.error(`守护进程启动失败: ${
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
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' ? '✅' : '❌';
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* - --depth N 展开深度(默认 3,仅 page 模式)
|
|
8
8
|
* - --max-nodes N 全局节点上限(默认 200)
|
|
9
9
|
* - --max-siblings N 每个父节点下展示的 children 上限(默认 20)
|
|
10
|
+
* - --focus-rem-id <remId> 指定鱼眼中心 Rem ID(仅 focus 模式,默认使用当前焦点)
|
|
10
11
|
* - --json 结构化 JSON 输出
|
|
11
12
|
*/
|
|
12
|
-
import { sendDaemonRequest
|
|
13
|
-
import { jsonOutput } from '../utils/output.js';
|
|
13
|
+
import { sendDaemonRequest } from '../daemon/send-request.js';
|
|
14
|
+
import { jsonOutput, handleCommandError } from '../utils/output.js';
|
|
14
15
|
export async function readContextCommand(options = {}) {
|
|
15
16
|
const { json } = options;
|
|
16
17
|
const mode = options.mode || 'focus';
|
|
@@ -42,29 +43,13 @@ export async function readContextCommand(options = {}) {
|
|
|
42
43
|
}
|
|
43
44
|
let result;
|
|
44
45
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const reqPayload = { mode, ancestorLevels, depth, maxNodes, maxSiblings };
|
|
47
|
+
if (options.focusRemId)
|
|
48
|
+
reqPayload.focusRemId = options.focusRemId;
|
|
49
|
+
result = await sendDaemonRequest('read_context', reqPayload);
|
|
48
50
|
}
|
|
49
51
|
catch (err) {
|
|
50
|
-
|
|
51
|
-
if (json) {
|
|
52
|
-
jsonOutput({ ok: false, command: 'read-context', error: err.message });
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
console.error(`错误: ${err.message}`);
|
|
56
|
-
}
|
|
57
|
-
process.exitCode = 2;
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
61
|
-
if (json) {
|
|
62
|
-
jsonOutput({ ok: false, command: 'read-context', error: errorMsg });
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
console.error(`错误: ${errorMsg}`);
|
|
66
|
-
}
|
|
67
|
-
process.exitCode = 1;
|
|
52
|
+
handleCommandError(err, 'read-context', json);
|
|
68
53
|
return;
|
|
69
54
|
}
|
|
70
55
|
const data = result;
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* - --max-siblings N 每个父节点下展示的 children 上限(默认 20)
|
|
8
8
|
* - --json 结构化 JSON 输出
|
|
9
9
|
*/
|
|
10
|
-
import { sendDaemonRequest
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|