mu-agent-bridge 1.0.68 → 1.0.70
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/src-ts/cli.js +1 -1
- package/dist/src-ts/core-updater.js +1 -1
- package/dist/src-ts/plugin-manager.js +9 -7
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +5 -3
- package/dist/src-ts/plugins/chatgpt/index.js +1 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +22 -3
- package/dist/src-ts/plugins/workbuddy/index.js +4 -8
- package/dist/src-ts/version-manager.js +1 -1
- package/dist/tests-ts/chatgpt-plugin.test.js +11 -0
- package/dist/tests-ts/installer-assets.test.js +2 -2
- package/dist/tests-ts/plugin-manager.test.js +35 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +69 -6
- package/package.json +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +5 -3
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +1 -0
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +22 -3
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +4 -8
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
package/dist/src-ts/cli.js
CHANGED
|
@@ -119,7 +119,7 @@ async function update() {
|
|
|
119
119
|
return 2;
|
|
120
120
|
}
|
|
121
121
|
await stopDaemon();
|
|
122
|
-
await execFileAsync("npm.cmd", ["install", "-g", "mu-agent-bridge@latest", "--no-fund", "--no-audit"], { timeout: 120_000, shell: true });
|
|
122
|
+
await execFileAsync("npm.cmd", ["install", "-g", "mu-agent-bridge@latest", "--no-fund", "--no-audit"], { timeout: 120_000, shell: true, windowsHide: true });
|
|
123
123
|
await startDaemon();
|
|
124
124
|
console.log("核心已更新,旧 daemon 已结束并启动新版本");
|
|
125
125
|
return 0;
|
|
@@ -21,7 +21,7 @@ async function sameDirectory(left, right) {
|
|
|
21
21
|
}
|
|
22
22
|
async function runNpm(args) {
|
|
23
23
|
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
24
|
-
const { stdout } = await execFileAsync(command, args, { timeout: 180_000, maxBuffer: 4 * 1024 * 1024, ...(process.platform === "win32" ? { shell: true } : {}) });
|
|
24
|
+
const { stdout } = await execFileAsync(command, args, { timeout: 180_000, maxBuffer: 4 * 1024 * 1024, windowsHide: true, ...(process.platform === "win32" ? { shell: true } : {}) });
|
|
25
25
|
return stdout.trim();
|
|
26
26
|
}
|
|
27
27
|
async function updateCoreAtStartup(options) {
|
|
@@ -102,7 +102,7 @@ class PluginManager {
|
|
|
102
102
|
try {
|
|
103
103
|
const spec = version ? `${packageName}@${version}` : packageName;
|
|
104
104
|
if (process.platform === "win32")
|
|
105
|
-
await execFileAsync("npm.cmd", ["install", "--prefix", target, "--no-save", "--ignore-scripts", spec], { shell: true });
|
|
105
|
+
await execFileAsync("npm.cmd", ["install", "--prefix", target, "--no-save", "--ignore-scripts", spec], { shell: true, windowsHide: true });
|
|
106
106
|
else
|
|
107
107
|
await execFileAsync("npm", ["install", "--prefix", target, "--no-save", "--ignore-scripts", spec]);
|
|
108
108
|
const candidate = (0, node_path_1.join)(target, "node_modules", packageName);
|
|
@@ -276,7 +276,7 @@ class PluginManager {
|
|
|
276
276
|
this.log("info", `开始安装插件:${packageName}`, undefined, "插件安装");
|
|
277
277
|
try {
|
|
278
278
|
if (process.platform === "win32")
|
|
279
|
-
await execFileAsync("npm.cmd", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName], { shell: true });
|
|
279
|
+
await execFileAsync("npm.cmd", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName], { shell: true, windowsHide: true });
|
|
280
280
|
else
|
|
281
281
|
await execFileAsync("npm", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName]);
|
|
282
282
|
const directory = (0, node_path_1.join)(prefix, "node_modules", name);
|
|
@@ -315,13 +315,15 @@ class PluginManager {
|
|
|
315
315
|
await this.discover();
|
|
316
316
|
return this.plugins.get(manifest.id).record;
|
|
317
317
|
}
|
|
318
|
-
async start(id) { await this.startWithDependencies(id, new Set()); }
|
|
319
|
-
async startWithDependencies(id, path) {
|
|
318
|
+
async start(id) { await this.startWithDependencies(id, new Set(), true); }
|
|
319
|
+
async startWithDependencies(id, path, checkHealth = false) {
|
|
320
320
|
const loaded = this.plugins.get(id);
|
|
321
321
|
if (!loaded)
|
|
322
322
|
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
323
|
-
if (loaded.instance && loaded.record.status === "ready")
|
|
324
|
-
|
|
323
|
+
if (loaded.instance && loaded.record.status === "ready") {
|
|
324
|
+
if (!checkHealth || (await loaded.instance.health()).status === "ready")
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
325
327
|
if (loaded.instance) {
|
|
326
328
|
try {
|
|
327
329
|
await loaded.instance.deactivate("reload");
|
|
@@ -763,7 +765,7 @@ class PluginManager {
|
|
|
763
765
|
const req = (0, node_module_1.createRequire)((0, node_path_1.resolve)(process.cwd(), "package.json"));
|
|
764
766
|
const roots = [process.cwd()];
|
|
765
767
|
try {
|
|
766
|
-
roots.push((await execFileAsync("npm", ["root", "-g"])).stdout.trim());
|
|
768
|
+
roots.push((await execFileAsync("npm", ["root", "-g"], { windowsHide: true })).stdout.trim());
|
|
767
769
|
}
|
|
768
770
|
catch { /* npm 不可用时仍扫描本地依赖 */ }
|
|
769
771
|
for (const name of names) {
|
|
@@ -59,6 +59,7 @@ class ChatgptCdpClient {
|
|
|
59
59
|
pending = new Map();
|
|
60
60
|
port;
|
|
61
61
|
timeoutMs;
|
|
62
|
+
connectionTimeoutMs;
|
|
62
63
|
fetchImpl;
|
|
63
64
|
webSocketFactory;
|
|
64
65
|
ssh;
|
|
@@ -66,6 +67,7 @@ class ChatgptCdpClient {
|
|
|
66
67
|
constructor(options = {}) {
|
|
67
68
|
this.port = options.port ?? options.ssh?.localPort ?? 9230;
|
|
68
69
|
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
70
|
+
this.connectionTimeoutMs = options.connectionTimeoutMs ?? Math.min(this.timeoutMs, 30_000);
|
|
69
71
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
70
72
|
this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
71
73
|
this.ssh = options.ssh;
|
|
@@ -254,7 +256,7 @@ class ChatgptCdpClient {
|
|
|
254
256
|
};
|
|
255
257
|
await new Promise((resolve, reject) => {
|
|
256
258
|
rejectOpening = reject;
|
|
257
|
-
const timer = setTimeout(() => reject(new Error("ChatGPT CDP WebSocket 连接超时")), this.
|
|
259
|
+
const timer = setTimeout(() => reject(new Error("ChatGPT CDP WebSocket 连接超时")), this.connectionTimeoutMs);
|
|
258
260
|
socket.onopen = () => {
|
|
259
261
|
opened = true;
|
|
260
262
|
clearTimeout(timer);
|
|
@@ -272,12 +274,12 @@ class ChatgptCdpClient {
|
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
276
|
async findPage() {
|
|
275
|
-
const deadline = Date.now() + this.
|
|
277
|
+
const deadline = Date.now() + this.connectionTimeoutMs;
|
|
276
278
|
let last;
|
|
277
279
|
while (Date.now() < deadline) {
|
|
278
280
|
try {
|
|
279
281
|
const response = await this.fetchImpl(`http://127.0.0.1:${this.port}/json`, {
|
|
280
|
-
signal: AbortSignal.timeout(Math.min(this.
|
|
282
|
+
signal: AbortSignal.timeout(Math.min(this.connectionTimeoutMs, 3_000)),
|
|
281
283
|
});
|
|
282
284
|
if (!response.ok)
|
|
283
285
|
throw new Error(`ChatGPT CDP /json HTTP ${response.status}`);
|
|
@@ -90,6 +90,7 @@ class ChatgptPlugin {
|
|
|
90
90
|
port: port(config.localCdpPort, 9230),
|
|
91
91
|
dataDir: context.dataDir,
|
|
92
92
|
timeoutMs: typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 30_000,
|
|
93
|
+
connectionTimeoutMs: typeof config.startupTimeoutMs === "number" && config.startupTimeoutMs > 0 ? config.startupTimeoutMs : 30_000,
|
|
93
94
|
ssh,
|
|
94
95
|
});
|
|
95
96
|
const desktopExecutable = !ssh && config.autoStart !== false && !this.options.client && process.platform === "win32"
|
|
@@ -127,9 +127,28 @@ class WorkbuddyCdpClient {
|
|
|
127
127
|
const before = await this.evaluate(`(() => { const state = ${snapshot}; const loginRequired = [...document.querySelectorAll('a,button,[role="button"]')].some(item => /^(?:登录|立即登录|sign in)$/i.test(item.textContent?.trim() ?? '')); return { ...state, loginRequired }; })()`);
|
|
128
128
|
if (before.loginRequired)
|
|
129
129
|
throw new Error("WorkBuddy 尚未登录,请先在桌面端登录后重试");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
try {
|
|
131
|
+
await this.waitFor(`(() => { const button = [...document.querySelectorAll('button,[role="button"],[role="tab"]')].find(item => { const label = [item.innerText, item.textContent, item.getAttribute('aria-label'), item.getAttribute('title'), item.getAttribute('data-testid')].filter(Boolean).map(value => String(value).normalize('NFKC').replace(/\\s+/g, ' ').trim().toLowerCase()); return label.some(value => /^(?:新建任务|新建会话|新建对话|开始新任务|new (?:task|chat|conversation))$/.test(value)) && (!item.getClientRects || item.getClientRects().length > 0); }); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`, "WorkBuddy 新建任务入口未找到或不可用");
|
|
132
|
+
// WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
|
|
133
|
+
await this.waitFor(`(() => { const state = ${snapshot}; const blank = state.editor !== null && state.editor.trim() === '' && state.messages === 0; const clearedToUnselected = ${before.selected} && !state.selected && state.messages < ${before.messages}; return blank && ((state.conversationId && state.conversationId !== ${JSON.stringify(before.conversationId)}) || state.url !== ${JSON.stringify(before.url)} || (state.home && !state.selected) || clearedToUnselected); })()`, "WorkBuddy 新任务状态校验失败");
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const summarize = (state) => {
|
|
137
|
+
let url = "[无效地址]";
|
|
138
|
+
try {
|
|
139
|
+
const parsed = new URL(state.url);
|
|
140
|
+
url = parsed.protocol === "file:" ? "file:///[本地路径已隐藏]" : `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
141
|
+
}
|
|
142
|
+
catch { /* 无效地址不输出原文,避免带入凭证。 */ }
|
|
143
|
+
return { url, selected: state.selected, messages: state.messages, editorPresent: state.editor !== null, editorEmpty: state.editor === "", home: state.home };
|
|
144
|
+
};
|
|
145
|
+
let after = "快照不可用";
|
|
146
|
+
try {
|
|
147
|
+
after = summarize(await this.evaluate(snapshot));
|
|
148
|
+
}
|
|
149
|
+
catch { /* 诊断失败不覆盖原始错误。 */ }
|
|
150
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)};页面状态:${JSON.stringify({ before: summarize(before), after })}`, { cause: error });
|
|
151
|
+
}
|
|
133
152
|
this.pendingInteraction = undefined;
|
|
134
153
|
const temporaryPaths = this.pendingInteractionCleanup;
|
|
135
154
|
this.pendingInteractionCleanup = undefined;
|
|
@@ -16,7 +16,7 @@ const session_history_1 = require("./session-history");
|
|
|
16
16
|
const desktop_launch_1 = require("../desktop-launch");
|
|
17
17
|
const desktop_launch_2 = require("./desktop-launch");
|
|
18
18
|
exports.workbuddyManifest = {
|
|
19
|
-
id: "workbuddy", version: "1.1.
|
|
19
|
+
id: "workbuddy", version: "1.1.32", displayName: "WorkBuddy", type: "agent",
|
|
20
20
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
21
21
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
22
22
|
};
|
|
@@ -229,7 +229,7 @@ class WorkbuddyPlugin {
|
|
|
229
229
|
async health() {
|
|
230
230
|
if (this.client?.connected())
|
|
231
231
|
return { status: "ready", details: { transport: "cdp" } };
|
|
232
|
-
if (this.cliPath && (0, node_fs_1.existsSync)(this.cliPath))
|
|
232
|
+
if (this.transport !== "cdp" && this.cliPath && (0, node_fs_1.existsSync)(this.cliPath))
|
|
233
233
|
return { status: "ready", details: { transport: "cli" } };
|
|
234
234
|
return { status: "failed", message: this.failure ?? "WorkBuddy 未连接", details: { transport: this.transport } };
|
|
235
235
|
}
|
|
@@ -365,12 +365,8 @@ class WorkbuddyPlugin {
|
|
|
365
365
|
const binding = this.sessionBindings.get(sessionId);
|
|
366
366
|
const conversationId = binding?.conversationId ?? (this.recoverableSessionId === sessionId ? this.recoverableConversationId : undefined);
|
|
367
367
|
if (!conversationId) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
this.sessionBindings.delete(sessionId);
|
|
371
|
-
await this.persistKnownSessions();
|
|
372
|
-
await this.createSession(sessionId);
|
|
373
|
-
return;
|
|
368
|
+
this.dailyPointsLogger?.warn("WorkBuddy 会话缺少桌面绑定,已保留记录且未创建新任务", { sessionId });
|
|
369
|
+
throw new Error("WorkBuddy 会话缺少桌面任务标识,无法安全恢复上下文;请确认后使用 /new 创建新会话");
|
|
374
370
|
}
|
|
375
371
|
if (!this.client.activateConversation)
|
|
376
372
|
throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
|
|
@@ -76,6 +76,6 @@ class VersionManager {
|
|
|
76
76
|
this.saveTask = this.saveTask.catch(() => undefined).then(async () => { await (0, promises_1.mkdir)(this.options.stateDir, { recursive: true }); await (0, promises_1.writeFile)((0, node_path_1.join)(this.options.stateDir, "versions.json"), JSON.stringify(this.list(), null, 2)); });
|
|
77
77
|
await this.saveTask;
|
|
78
78
|
}
|
|
79
|
-
async queryNpm(args) { return (await execFileAsync(process.platform === "win32" ? "npm.cmd" : "npm", args, { timeout: 30_000, maxBuffer: 1024 * 1024, ...(process.platform === "win32" ? { shell: true } : {}) })).stdout; }
|
|
79
|
+
async queryNpm(args) { return (await execFileAsync(process.platform === "win32" ? "npm.cmd" : "npm", args, { timeout: 30_000, maxBuffer: 1024 * 1024, windowsHide: true, ...(process.platform === "win32" ? { shell: true } : {}) })).stdout; }
|
|
80
80
|
}
|
|
81
81
|
exports.VersionManager = VersionManager;
|
|
@@ -210,6 +210,17 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
|
|
|
210
210
|
strict_1.default.equal(connectedUrl, "ws://127.0.0.1:9230/devtools/page/2");
|
|
211
211
|
client.close();
|
|
212
212
|
});
|
|
213
|
+
(0, node_test_1.default)("ChatGPT CDP 建连使用独立于任务的超时", async () => {
|
|
214
|
+
const options = {
|
|
215
|
+
timeoutMs: 5_000,
|
|
216
|
+
connectionTimeoutMs: 10,
|
|
217
|
+
fetchImpl: async () => new Response(JSON.stringify([])),
|
|
218
|
+
};
|
|
219
|
+
const client = new chatgpt_1.ChatgptCdpClient(options);
|
|
220
|
+
const started = Date.now();
|
|
221
|
+
await strict_1.default.rejects(() => client.connect(), /ChatGPT CDP 连接超时/);
|
|
222
|
+
strict_1.default.ok(Date.now() - started < 1_000);
|
|
223
|
+
});
|
|
213
224
|
(0, node_test_1.default)("ChatGPT CDP 新建会话必须确认空编辑页", async () => {
|
|
214
225
|
let onMessage;
|
|
215
226
|
let activeId = "old";
|
|
@@ -78,7 +78,7 @@ const node_util_1 = require("node:util");
|
|
|
78
78
|
strict_1.default.match(setup, /环境与桥接服务/);
|
|
79
79
|
strict_1.default.match(setup, /Get-ChildItem -LiteralPath \$root -Filter \$fileName -File -Recurse/);
|
|
80
80
|
strict_1.default.match(setup, /api\/plugins\/\$agent\/config/);
|
|
81
|
-
strict_1.default.
|
|
81
|
+
strict_1.default.doesNotMatch(setup, /TopMost = \$true/);
|
|
82
82
|
strict_1.default.match(setup, /BringToFront\(\)/);
|
|
83
83
|
strict_1.default.match(setup, /Invoke-AunCommand @\('list'\)/);
|
|
84
84
|
strict_1.default.match(setup, /自动解除未安装千问办公的残留绑定/);
|
|
@@ -109,7 +109,7 @@ const node_util_1 = require("node:util");
|
|
|
109
109
|
strict_1.default.match(setup, /AddSeconds\(180\)/);
|
|
110
110
|
strict_1.default.match(setup, /System.Windows.Forms.LinkLabel/);
|
|
111
111
|
strict_1.default.match(setup, /重新检测/);
|
|
112
|
-
for (const domain of ["qwenwork.cn/download", "www.doubao.com", "www.workbuddy.ai", "
|
|
112
|
+
for (const domain of ["qwenwork.cn/download", "www.doubao.com", "www.workbuddy.ai", "download.deepseek.com/desktop/dsh-latest-windows-x64.exe"])
|
|
113
113
|
strict_1.default.ok(setup.includes(domain));
|
|
114
114
|
});
|
|
115
115
|
(0, node_test_1.default)("先检测并显示缺失 App,再配置 AID,最后单独绑定 Agent", async () => {
|
|
@@ -45,6 +45,41 @@ const node_http_1 = require("node:http");
|
|
|
45
45
|
const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
46
46
|
const version_manager_1 = require("../src-ts/version-manager");
|
|
47
47
|
const channel_sessions_1 = require("../src-ts/channel-sessions");
|
|
48
|
+
(0, node_test_1.default)("启动按钮恢复已退出的桌面 Agent,健康实例和依赖保持运行", async () => {
|
|
49
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-desktop-start-"));
|
|
50
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state") });
|
|
51
|
+
const ids = ["doubao", "qwenwork", "workbuddy", "chatgpt", "dsh"];
|
|
52
|
+
try {
|
|
53
|
+
for (const id of ids) {
|
|
54
|
+
const dir = (0, node_path_1.join)(root, "plugins", id);
|
|
55
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
56
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id, type: "agent", version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: [], ...(id === "dsh" ? { dependencies: { doubao: ">=1.0.0" } } : {}) }));
|
|
57
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), "module.exports=class{connected=false;stops=0;async activate(){this.connected=true}async deactivate(){this.stops++}async health(){return {status:this.connected?'ready':'degraded',message:this.connected?'':'桌面已退出'}}};");
|
|
58
|
+
}
|
|
59
|
+
await manager.discover();
|
|
60
|
+
for (const id of ids) {
|
|
61
|
+
await manager.start(id);
|
|
62
|
+
const first = manager.get(id).instance;
|
|
63
|
+
await manager.start(id);
|
|
64
|
+
strict_1.default.equal(manager.get(id).instance, first, `${id} 仍连接时不能重载`);
|
|
65
|
+
first.connected = false;
|
|
66
|
+
await manager.start(id);
|
|
67
|
+
strict_1.default.notEqual(manager.get(id).instance, first, `${id} 断开后必须重新激活`);
|
|
68
|
+
strict_1.default.equal(first.stops, 1);
|
|
69
|
+
strict_1.default.equal((await manager.health(id)).status, "ready");
|
|
70
|
+
}
|
|
71
|
+
const dependency = manager.get("doubao").instance;
|
|
72
|
+
dependency.connected = false;
|
|
73
|
+
await manager.reload("dsh");
|
|
74
|
+
strict_1.default.equal(manager.get("doubao").instance, dependency, "启动依赖不能探测或重启其它桌面");
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
for (const id of ids)
|
|
78
|
+
if (manager.get(id)?.instance)
|
|
79
|
+
await manager.stop(id);
|
|
80
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
48
83
|
(0, node_test_1.default)("启动时内置新版替代状态目录旧插件,保留较新及同版托管插件", async () => {
|
|
49
84
|
for (const [bundled, managed, expectedManaged] of [["1.0.31", "1.0.29", false], ["1.0.31", "1.0.32", true], ["1.0.31", "1.0.31", true], ["1.0.10", "1.0.9", false], ["1.0.9", "1.0.10", true]]) {
|
|
50
85
|
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-plugin-precedence-"));
|
|
@@ -223,6 +223,22 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
223
223
|
strict_1.default.equal((await plugin.health()).status, "ready");
|
|
224
224
|
await plugin.deactivate("stop");
|
|
225
225
|
});
|
|
226
|
+
(0, node_test_1.default)("workbuddy 强制 CDP 时桌面断开不能由本地 CLI 冒充就绪", async () => {
|
|
227
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-cdp-health-"));
|
|
228
|
+
const cliPath = (0, node_path_1.join)(root, "codebuddy.cjs");
|
|
229
|
+
let connected = true;
|
|
230
|
+
const plugin = new workbuddy_1.WorkbuddyPlugin({ client: { connect: async () => undefined, close() { }, connected: () => connected, direct: async () => "回复" } });
|
|
231
|
+
try {
|
|
232
|
+
await (0, promises_1.writeFile)(cliPath, "");
|
|
233
|
+
await plugin.activate({ config: { transport: "cdp", cliPath }, logger: { info() { }, warn() { }, error() { } } });
|
|
234
|
+
connected = false;
|
|
235
|
+
strict_1.default.equal((await plugin.health()).status, "failed");
|
|
236
|
+
await plugin.deactivate("stop");
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
226
242
|
(0, node_test_1.default)("workbuddy 结构化客户端接收附件并保留附件响应和逻辑会话", async () => {
|
|
227
243
|
const attachment = { kind: "file", data: new Uint8Array([1, 2]), filename: "输入.txt", contentType: "text/plain" };
|
|
228
244
|
const output = { kind: "file", data: new Uint8Array([3]), filename: "输出.txt", contentType: "text/plain" };
|
|
@@ -921,7 +937,7 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
921
937
|
release();
|
|
922
938
|
await running;
|
|
923
939
|
});
|
|
924
|
-
(0, node_test_1.default)("workbuddy
|
|
940
|
+
(0, node_test_1.default)("workbuddy 控制失败后缺少桌面绑定的旧会话不得静默新建", async () => {
|
|
925
941
|
let creates = 0;
|
|
926
942
|
let fail = false;
|
|
927
943
|
const plugin = new workbuddy_1.WorkbuddyPlugin({ client: {
|
|
@@ -936,8 +952,8 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
936
952
|
strict_1.default.equal(creates, 1);
|
|
937
953
|
fail = true;
|
|
938
954
|
await strict_1.default.rejects(() => plugin.controlSession({ operation: "new", sessionId: "failed-session" }), /校验失败/);
|
|
939
|
-
await strict_1.default.rejects(() => plugin.handle({ task: "续聊", sessionId: "new-session" }),
|
|
940
|
-
strict_1.default.equal(creates,
|
|
955
|
+
await strict_1.default.rejects(() => plugin.handle({ task: "续聊", sessionId: "new-session" }), /无法安全恢复.*\/new/);
|
|
956
|
+
strict_1.default.equal(creates, 2);
|
|
941
957
|
});
|
|
942
958
|
(0, node_test_1.default)("workbuddy auto 选择 CLI 时不会创建无关桌面任务", async () => {
|
|
943
959
|
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-cli-session-"));
|
|
@@ -1178,7 +1194,7 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
1178
1194
|
await strict_1.default.rejects(() => plugin.handle({ task: "调用文案写作专家改写" }), /sessionId|会话/);
|
|
1179
1195
|
strict_1.default.equal(calls, 1);
|
|
1180
1196
|
});
|
|
1181
|
-
(0, node_test_1.default)("workbuddy
|
|
1197
|
+
(0, node_test_1.default)("workbuddy 跨插件重启保留无桌面绑定的会话且明确拒绝丢失上下文", async () => {
|
|
1182
1198
|
const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-sessions-"));
|
|
1183
1199
|
let creates = 0;
|
|
1184
1200
|
const client = {
|
|
@@ -1193,8 +1209,10 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
1193
1209
|
await first.deactivate("reload");
|
|
1194
1210
|
const second = new workbuddy_1.WorkbuddyPlugin({ client });
|
|
1195
1211
|
await second.activate(context);
|
|
1196
|
-
strict_1.default.
|
|
1197
|
-
strict_1.default.equal(creates,
|
|
1212
|
+
await strict_1.default.rejects(() => second.handle({ task: "恢复旧会话", sessionId: "session-a" }), /无法安全恢复.*\/new/);
|
|
1213
|
+
strict_1.default.equal(creates, 1);
|
|
1214
|
+
const saved = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(dataDir, "known-sessions.json"), "utf8"));
|
|
1215
|
+
strict_1.default.ok(saved.sessionIds.includes("session-a"));
|
|
1198
1216
|
await second.deactivate("stop");
|
|
1199
1217
|
}
|
|
1200
1218
|
finally {
|
|
@@ -1405,3 +1423,48 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
1405
1423
|
await plugin.deactivate("stop");
|
|
1406
1424
|
}
|
|
1407
1425
|
});
|
|
1426
|
+
(0, node_test_1.default)("WorkBuddy 新任务校验失败包含前后状态且不泄露草稿和 URL 凭证", async () => {
|
|
1427
|
+
let clicked = false;
|
|
1428
|
+
const button = { textContent: "新建任务", disabled: false, getAttribute: () => null, click: () => { clicked = true; } };
|
|
1429
|
+
const editor = { querySelector: () => null, cloneNode: () => ({ textContent: "私密草稿内容", querySelectorAll: () => [] }) };
|
|
1430
|
+
const document = {
|
|
1431
|
+
querySelectorAll: (selector) => selector === '.cr-agent[data-message-id]' ? Array(clicked ? 2 : 1).fill({}) : selector === '[data-conversation-id]' ? [{ className: "_selected_old", getAttribute: () => "old" }] : [button],
|
|
1432
|
+
querySelector: () => editor,
|
|
1433
|
+
};
|
|
1434
|
+
const client = new cdp_client_1.WorkbuddyCdpClient({ timeoutMs: 1 });
|
|
1435
|
+
client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document, location: { href: "https://user:password@example.com/task?token=secret#private" } });
|
|
1436
|
+
await strict_1.default.rejects(client.createTask(), error => {
|
|
1437
|
+
const message = String(error);
|
|
1438
|
+
strict_1.default.match(message, /新任务状态校验失败/);
|
|
1439
|
+
strict_1.default.match(message, /before/);
|
|
1440
|
+
strict_1.default.match(message, /after/);
|
|
1441
|
+
strict_1.default.match(message, /"messages":1/);
|
|
1442
|
+
strict_1.default.match(message, /"messages":2/);
|
|
1443
|
+
strict_1.default.match(message, /"editorEmpty":false/);
|
|
1444
|
+
strict_1.default.match(message, /"home":false/);
|
|
1445
|
+
strict_1.default.match(message, /"selected":true/);
|
|
1446
|
+
strict_1.default.match(message, /https:\/\/example.com\/task/);
|
|
1447
|
+
strict_1.default.doesNotMatch(message, /私密草稿|password|token|secret|private|user:/);
|
|
1448
|
+
return true;
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1451
|
+
(0, node_test_1.default)("WorkBuddy 诊断快照读取失败时保留原始校验错误", async () => {
|
|
1452
|
+
const client = new cdp_client_1.WorkbuddyCdpClient({ timeoutMs: 1 });
|
|
1453
|
+
let calls = 0;
|
|
1454
|
+
client.evaluate = async () => {
|
|
1455
|
+
if (++calls === 1)
|
|
1456
|
+
return { url: "file:///C:/Users/private/index.html?token=secret", messages: 0, editor: "", selected: false, home: false };
|
|
1457
|
+
if (calls === 2)
|
|
1458
|
+
return true;
|
|
1459
|
+
if (calls === 3)
|
|
1460
|
+
return false;
|
|
1461
|
+
throw new Error("读取快照失败");
|
|
1462
|
+
};
|
|
1463
|
+
await strict_1.default.rejects(client.createTask(), error => {
|
|
1464
|
+
const message = String(error);
|
|
1465
|
+
strict_1.default.match(message, /新任务状态校验失败/);
|
|
1466
|
+
strict_1.default.match(message, /快照不可用/);
|
|
1467
|
+
strict_1.default.doesNotMatch(message, /private|token|secret/);
|
|
1468
|
+
return true;
|
|
1469
|
+
});
|
|
1470
|
+
});
|
package/package.json
CHANGED
|
@@ -59,6 +59,7 @@ class ChatgptCdpClient {
|
|
|
59
59
|
pending = new Map();
|
|
60
60
|
port;
|
|
61
61
|
timeoutMs;
|
|
62
|
+
connectionTimeoutMs;
|
|
62
63
|
fetchImpl;
|
|
63
64
|
webSocketFactory;
|
|
64
65
|
ssh;
|
|
@@ -66,6 +67,7 @@ class ChatgptCdpClient {
|
|
|
66
67
|
constructor(options = {}) {
|
|
67
68
|
this.port = options.port ?? options.ssh?.localPort ?? 9230;
|
|
68
69
|
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
70
|
+
this.connectionTimeoutMs = options.connectionTimeoutMs ?? Math.min(this.timeoutMs, 30_000);
|
|
69
71
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
70
72
|
this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
71
73
|
this.ssh = options.ssh;
|
|
@@ -254,7 +256,7 @@ class ChatgptCdpClient {
|
|
|
254
256
|
};
|
|
255
257
|
await new Promise((resolve, reject) => {
|
|
256
258
|
rejectOpening = reject;
|
|
257
|
-
const timer = setTimeout(() => reject(new Error("ChatGPT CDP WebSocket 连接超时")), this.
|
|
259
|
+
const timer = setTimeout(() => reject(new Error("ChatGPT CDP WebSocket 连接超时")), this.connectionTimeoutMs);
|
|
258
260
|
socket.onopen = () => {
|
|
259
261
|
opened = true;
|
|
260
262
|
clearTimeout(timer);
|
|
@@ -272,12 +274,12 @@ class ChatgptCdpClient {
|
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
276
|
async findPage() {
|
|
275
|
-
const deadline = Date.now() + this.
|
|
277
|
+
const deadline = Date.now() + this.connectionTimeoutMs;
|
|
276
278
|
let last;
|
|
277
279
|
while (Date.now() < deadline) {
|
|
278
280
|
try {
|
|
279
281
|
const response = await this.fetchImpl(`http://127.0.0.1:${this.port}/json`, {
|
|
280
|
-
signal: AbortSignal.timeout(Math.min(this.
|
|
282
|
+
signal: AbortSignal.timeout(Math.min(this.connectionTimeoutMs, 3_000)),
|
|
281
283
|
});
|
|
282
284
|
if (!response.ok)
|
|
283
285
|
throw new Error(`ChatGPT CDP /json HTTP ${response.status}`);
|
|
@@ -90,6 +90,7 @@ class ChatgptPlugin {
|
|
|
90
90
|
port: port(config.localCdpPort, 9230),
|
|
91
91
|
dataDir: context.dataDir,
|
|
92
92
|
timeoutMs: typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 30_000,
|
|
93
|
+
connectionTimeoutMs: typeof config.startupTimeoutMs === "number" && config.startupTimeoutMs > 0 ? config.startupTimeoutMs : 30_000,
|
|
93
94
|
ssh,
|
|
94
95
|
});
|
|
95
96
|
const desktopExecutable = !ssh && config.autoStart !== false && !this.options.client && process.platform === "win32"
|
|
@@ -127,9 +127,28 @@ class WorkbuddyCdpClient {
|
|
|
127
127
|
const before = await this.evaluate(`(() => { const state = ${snapshot}; const loginRequired = [...document.querySelectorAll('a,button,[role="button"]')].some(item => /^(?:登录|立即登录|sign in)$/i.test(item.textContent?.trim() ?? '')); return { ...state, loginRequired }; })()`);
|
|
128
128
|
if (before.loginRequired)
|
|
129
129
|
throw new Error("WorkBuddy 尚未登录,请先在桌面端登录后重试");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
try {
|
|
131
|
+
await this.waitFor(`(() => { const button = [...document.querySelectorAll('button,[role="button"],[role="tab"]')].find(item => { const label = [item.innerText, item.textContent, item.getAttribute('aria-label'), item.getAttribute('title'), item.getAttribute('data-testid')].filter(Boolean).map(value => String(value).normalize('NFKC').replace(/\\s+/g, ' ').trim().toLowerCase()); return label.some(value => /^(?:新建任务|新建会话|新建对话|开始新任务|new (?:task|chat|conversation))$/.test(value)) && (!item.getClientRects || item.getClientRects().length > 0); }); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`, "WorkBuddy 新建任务入口未找到或不可用");
|
|
132
|
+
// WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
|
|
133
|
+
await this.waitFor(`(() => { const state = ${snapshot}; const blank = state.editor !== null && state.editor.trim() === '' && state.messages === 0; const clearedToUnselected = ${before.selected} && !state.selected && state.messages < ${before.messages}; return blank && ((state.conversationId && state.conversationId !== ${JSON.stringify(before.conversationId)}) || state.url !== ${JSON.stringify(before.url)} || (state.home && !state.selected) || clearedToUnselected); })()`, "WorkBuddy 新任务状态校验失败");
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const summarize = (state) => {
|
|
137
|
+
let url = "[无效地址]";
|
|
138
|
+
try {
|
|
139
|
+
const parsed = new URL(state.url);
|
|
140
|
+
url = parsed.protocol === "file:" ? "file:///[本地路径已隐藏]" : `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
141
|
+
}
|
|
142
|
+
catch { /* 无效地址不输出原文,避免带入凭证。 */ }
|
|
143
|
+
return { url, selected: state.selected, messages: state.messages, editorPresent: state.editor !== null, editorEmpty: state.editor === "", home: state.home };
|
|
144
|
+
};
|
|
145
|
+
let after = "快照不可用";
|
|
146
|
+
try {
|
|
147
|
+
after = summarize(await this.evaluate(snapshot));
|
|
148
|
+
}
|
|
149
|
+
catch { /* 诊断失败不覆盖原始错误。 */ }
|
|
150
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)};页面状态:${JSON.stringify({ before: summarize(before), after })}`, { cause: error });
|
|
151
|
+
}
|
|
133
152
|
this.pendingInteraction = undefined;
|
|
134
153
|
const temporaryPaths = this.pendingInteractionCleanup;
|
|
135
154
|
this.pendingInteractionCleanup = undefined;
|
|
@@ -16,7 +16,7 @@ const session_history_1 = require("./session-history");
|
|
|
16
16
|
const desktop_launch_1 = require("../desktop-launch");
|
|
17
17
|
const desktop_launch_2 = require("./desktop-launch");
|
|
18
18
|
exports.workbuddyManifest = {
|
|
19
|
-
id: "workbuddy", version: "1.1.
|
|
19
|
+
id: "workbuddy", version: "1.1.32", displayName: "WorkBuddy", type: "agent",
|
|
20
20
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
21
21
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
22
22
|
};
|
|
@@ -229,7 +229,7 @@ class WorkbuddyPlugin {
|
|
|
229
229
|
async health() {
|
|
230
230
|
if (this.client?.connected())
|
|
231
231
|
return { status: "ready", details: { transport: "cdp" } };
|
|
232
|
-
if (this.cliPath && (0, node_fs_1.existsSync)(this.cliPath))
|
|
232
|
+
if (this.transport !== "cdp" && this.cliPath && (0, node_fs_1.existsSync)(this.cliPath))
|
|
233
233
|
return { status: "ready", details: { transport: "cli" } };
|
|
234
234
|
return { status: "failed", message: this.failure ?? "WorkBuddy 未连接", details: { transport: this.transport } };
|
|
235
235
|
}
|
|
@@ -365,12 +365,8 @@ class WorkbuddyPlugin {
|
|
|
365
365
|
const binding = this.sessionBindings.get(sessionId);
|
|
366
366
|
const conversationId = binding?.conversationId ?? (this.recoverableSessionId === sessionId ? this.recoverableConversationId : undefined);
|
|
367
367
|
if (!conversationId) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
this.sessionBindings.delete(sessionId);
|
|
371
|
-
await this.persistKnownSessions();
|
|
372
|
-
await this.createSession(sessionId);
|
|
373
|
-
return;
|
|
368
|
+
this.dailyPointsLogger?.warn("WorkBuddy 会话缺少桌面绑定,已保留记录且未创建新任务", { sessionId });
|
|
369
|
+
throw new Error("WorkBuddy 会话缺少桌面任务标识,无法安全恢复上下文;请确认后使用 /new 创建新会话");
|
|
374
370
|
}
|
|
375
371
|
if (!this.client.activateConversation)
|
|
376
372
|
throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
|