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.
- package/README.md +147 -28
- package/README.zh-CN.md +374 -0
- package/dist/cli/commands/health.js +231 -112
- package/dist/cli/commands/read-rem-in-tree.js +84 -0
- package/dist/cli/config.js +2 -0
- package/dist/cli/daemon/registry.js +8 -0
- package/dist/cli/handlers/edit-handler.js +14 -0
- package/dist/cli/handlers/patch-engine.js +347 -0
- package/dist/cli/handlers/read-handler.js +2 -53
- package/dist/cli/handlers/rem-field-filter.js +102 -0
- package/dist/cli/handlers/tree-edit-handler.js +67 -7
- package/dist/cli/handlers/tree-read-handler.js +4 -1
- package/dist/cli/handlers/tree-rem-read-handler.js +73 -0
- package/dist/cli/main.js +53 -2
- package/dist/cli/server/ws-server.js +9 -1
- package/dist/mcp/daemon-client.js +22 -2
- package/dist/mcp/instructions.js +99 -58
- package/dist/mcp/tools/edit-tools.js +7 -2
- package/dist/mcp/tools/infra-tools.js +20 -11
- package/dist/mcp/tools/read-tools.js +88 -2
- package/package.json +1 -1
- package/remnote-plugin/dist/index-sandbox.js +24 -24
- package/remnote-plugin/dist/index.js +24 -24
- 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 +3 -0
- package/remnote-plugin/src/services/read-rem-in-tree.ts +43 -0
- package/remnote-plugin/src/services/read-rem.ts +31 -16
- package/remnote-plugin/src/services/read-tree.ts +5 -0
- package/remnote-plugin/src/settings.ts +1 -1
- package/skills/remnote-bridge/SKILL.md +50 -8
- package/skills/remnote-bridge/instructions/connect.md +31 -8
- package/skills/remnote-bridge/instructions/disconnect.md +5 -0
- package/skills/remnote-bridge/instructions/edit-tree.md +117 -51
- package/skills/remnote-bridge/instructions/health.md +81 -53
- package/skills/remnote-bridge/instructions/overall.md +39 -8
- package/skills/remnote-bridge/instructions/read-rem-in-tree.md +100 -0
- package/skills/remnote-bridge/instructions/read-rem.md +30 -11
- package/skills/remnote-bridge-test/SKILL.md +847 -0
- package/skills/remnote-bridge-test/references/regression-suite.md +960 -0
- 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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
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
|
-
//
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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('
|
|
151
|
+
console.log('没有活跃的实例。执行 `remnote-bridge connect` 启动守护进程。');
|
|
187
152
|
}
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
292
|
+
else {
|
|
293
|
+
console.error(`重载失败: ${errorMsg}`);
|
|
294
|
+
}
|
|
295
|
+
process.exitCode = 1;
|
|
200
296
|
}
|
|
201
|
-
|
|
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
|
+
}
|
package/dist/cli/config.js
CHANGED
|
@@ -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
|
+
}
|