mu-agent-bridge 1.0.68 → 1.0.69

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.
@@ -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
- return;
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");
@@ -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.timeoutMs);
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.timeoutMs;
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.timeoutMs, 3_000)),
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
- 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 新建任务入口未找到或不可用");
131
- // WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
132
- 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 新任务状态校验失败");
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.31", displayName: "WorkBuddy", type: "agent",
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
- // 旧版本可能在新任务校验前写入会话 ID,无法恢复时允许重新创建。
369
- this.knownSessions.delete(sessionId);
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 会话");
@@ -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";
@@ -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 控制成功后首轮直接续用,控制失败保留绑定", async () => {
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, 3);
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 跨插件重启清理没有桌面绑定的旧会话并重新创建", async () => {
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.equal((await second.handle({ task: "恢复旧会话", sessionId: "session-a" })).text, "恢复旧会话");
1197
- strict_1.default.equal(creates, 2);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.68",
3
+ "version": "1.0.69",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -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.timeoutMs);
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.timeoutMs;
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.timeoutMs, 3_000)),
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
- 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 新建任务入口未找到或不可用");
131
- // WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
132
- 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 新任务状态校验失败");
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.31", displayName: "WorkBuddy", type: "agent",
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
- // 旧版本可能在新任务校验前写入会话 ID,无法恢复时允许重新创建。
369
- this.knownSessions.delete(sessionId);
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 会话");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-workbuddy",
3
- "version": "1.1.31",
3
+ "version": "1.1.32",
4
4
  "description": "mu-agent-bridge 的 WorkBuddy 桌面端插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "workbuddy",
3
- "version": "1.1.31",
3
+ "version": "1.1.32",
4
4
  "type": "agent",
5
5
  "displayName": "WorkBuddy",
6
6
  "entry": "dist/src-ts/plugins/workbuddy/index.js",