mu-agent-bridge 1.0.73 → 1.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +12 -8
  2. package/dist/src-ts/cli.js +67 -37
  3. package/dist/src-ts/control-server.js +7 -28
  4. package/dist/src-ts/core-updater.js +141 -33
  5. package/dist/src-ts/daemon-process.js +2 -1
  6. package/dist/src-ts/dsh-agent.js +7 -2
  7. package/dist/src-ts/http-agent.js +36 -0
  8. package/dist/src-ts/plugin-manager.js +185 -58
  9. package/dist/src-ts/plugins/abweb/index.js +1 -1
  10. package/dist/src-ts/plugins/abweb/public/app.js +8 -6
  11. package/dist/src-ts/plugins/abweb/public/index.html +3 -3
  12. package/dist/src-ts/plugins/aun/attachments.js +3 -21
  13. package/dist/src-ts/plugins/aun/index.js +89 -11
  14. package/dist/src-ts/plugins/aun/messages.js +1 -1
  15. package/dist/src-ts/plugins/aun/public/app.js +2 -1
  16. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  17. package/dist/src-ts/plugins/cdp-draft.js +5 -2
  18. package/dist/src-ts/plugins/chatgpt/index.js +3 -2
  19. package/dist/src-ts/plugins/claude-cli/index.js +1 -1
  20. package/dist/src-ts/plugins/desktop-launch.js +44 -3
  21. package/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
  22. package/dist/src-ts/plugins/doubao/index.js +119 -7
  23. package/dist/src-ts/plugins/doubao/native-task.js +134 -0
  24. package/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  25. package/dist/src-ts/plugins/dsh/index.js +102 -23
  26. package/dist/src-ts/plugins/dsh/session-history.js +31 -3
  27. package/dist/src-ts/plugins/feishu/index.js +1 -1
  28. package/dist/src-ts/plugins/personal-assistant/index.js +29 -8
  29. package/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
  30. package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  31. package/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
  32. package/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
  33. package/dist/src-ts/plugins/qwenwork/index.js +52 -18
  34. package/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
  35. package/dist/src-ts/plugins/workbuddy/index.js +24 -12
  36. package/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  37. package/dist/src-ts/plugins/zcode/index.js +97 -0
  38. package/dist/src-ts/session-management.js +27 -8
  39. package/dist/src-ts/version-manager.js +6 -3
  40. package/dist/tests-ts/abweb-model-config.test.js +44 -41
  41. package/dist/tests-ts/aun-attachments.test.js +19 -17
  42. package/dist/tests-ts/aun-default-owner-page.test.js +2 -1
  43. package/dist/tests-ts/aun-messages.test.js +14 -0
  44. package/dist/tests-ts/aun-plugin.test.js +207 -19
  45. package/dist/tests-ts/bundled-package.test.js +1 -1
  46. package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
  47. package/dist/tests-ts/chatgpt-plugin.test.js +19 -0
  48. package/dist/tests-ts/control-server.test.js +20 -0
  49. package/dist/tests-ts/core-updater.test.js +215 -46
  50. package/dist/tests-ts/daemon-process.test.js +126 -1
  51. package/dist/tests-ts/desktop-launch.test.js +73 -0
  52. package/dist/tests-ts/doubao-native-task.test.js +237 -0
  53. package/dist/tests-ts/doubao-plugin.test.js +688 -19
  54. package/dist/tests-ts/dsh-agent.test.js +91 -19
  55. package/dist/tests-ts/dsh-cdp.test.js +148 -0
  56. package/dist/tests-ts/dsh-history.test.js +37 -0
  57. package/dist/tests-ts/feishu-plugin.test.js +1 -1
  58. package/dist/tests-ts/http-agent.test.js +18 -0
  59. package/dist/tests-ts/local-cli.test.js +29 -2
  60. package/dist/tests-ts/model-provider-cancellation.test.js +2 -0
  61. package/dist/tests-ts/native-session-management.test.js +8 -7
  62. package/dist/tests-ts/personal-assistant-coordination.test.js +16 -0
  63. package/dist/tests-ts/personal-assistant-extensions.test.js +7 -1
  64. package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
  65. package/dist/tests-ts/personal-assistant.test.js +277 -3
  66. package/dist/tests-ts/platform-integration.test.js +60 -1
  67. package/dist/tests-ts/plugin-manager.test.js +78 -6
  68. package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
  69. package/dist/tests-ts/publish-packages.test.js +1 -1
  70. package/dist/tests-ts/qwenwork-plugin.test.js +30 -0
  71. package/dist/tests-ts/qwenwork-reconnect.test.js +195 -0
  72. package/dist/tests-ts/qwenwork-sse.test.js +33 -0
  73. package/dist/tests-ts/session-access-defaults.test.js +112 -0
  74. package/dist/tests-ts/session-management.test.js +8 -8
  75. package/dist/tests-ts/version-manager.test.js +40 -0
  76. package/dist/tests-ts/workbuddy-cdp-recovery.test.js +311 -0
  77. package/dist/tests-ts/workbuddy-plugin.test.js +356 -13
  78. package/dist/tests-ts/zcode-cdp.test.js +894 -0
  79. package/dist/tests-ts/zcode-plugin.test.js +396 -0
  80. package/package.json +1 -1
  81. package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
  82. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +8 -6
  83. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +7 -5
  84. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +3 -3
  85. package/plugins/abweb/package.json +1 -1
  86. package/plugins/abweb/plugin.json +1 -1
  87. package/plugins/aun/config.schema.json +1 -1
  88. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
  89. package/plugins/aun/dist/src-ts/plugins/aun/index.js +89 -11
  90. package/plugins/aun/dist/src-ts/plugins/aun/messages.js +1 -1
  91. package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +2 -1
  92. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  93. package/plugins/aun/package.json +1 -1
  94. package/plugins/aun/plugin.json +1 -1
  95. package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
  96. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +3 -2
  97. package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
  98. package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +1 -1
  99. package/plugins/claude-cli/package.json +1 -1
  100. package/plugins/claude-cli/plugin.json +1 -1
  101. package/plugins/doubao/config.schema.json +2 -1
  102. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
  103. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
  104. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +119 -7
  105. package/plugins/doubao/dist/src-ts/plugins/doubao/native-task.js +134 -0
  106. package/plugins/doubao/package.json +1 -1
  107. package/plugins/doubao/plugin.json +1 -1
  108. package/plugins/dsh/config.schema.json +5 -2
  109. package/plugins/dsh/dist/src-ts/dsh-agent.js +7 -2
  110. package/plugins/dsh/dist/src-ts/plugins/desktop-launch.js +44 -3
  111. package/plugins/dsh/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  112. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +102 -23
  113. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +31 -3
  114. package/plugins/dsh/package.json +1 -1
  115. package/plugins/dsh/plugin.json +1 -1
  116. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +1 -1
  117. package/plugins/feishu/package.json +1 -1
  118. package/plugins/feishu/plugin.json +1 -1
  119. package/plugins/personal-assistant/config.schema.json +3 -2
  120. package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
  121. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +29 -8
  122. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
  123. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  124. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
  125. package/plugins/personal-assistant/package.json +2 -2
  126. package/plugins/personal-assistant/plugin.json +1 -1
  127. package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +5 -2
  128. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +44 -3
  129. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
  130. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +52 -18
  131. package/plugins/qwenwork/package.json +1 -1
  132. package/plugins/qwenwork/plugin.json +1 -1
  133. package/plugins/workbuddy/config.schema.json +1 -1
  134. package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
  135. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
  136. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +24 -12
  137. package/plugins/workbuddy/package.json +1 -1
  138. package/plugins/workbuddy/plugin.json +1 -1
  139. package/plugins/zcode/config.schema.json +13 -0
  140. package/plugins/zcode/dist/src-ts/agent.js +69 -0
  141. package/plugins/zcode/dist/src-ts/attachments.js +62 -0
  142. package/plugins/zcode/dist/src-ts/plugin.js +13 -0
  143. package/plugins/zcode/dist/src-ts/plugins/desktop-launch.js +238 -0
  144. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cdp-client.js +896 -0
  145. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
  146. package/plugins/zcode/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  147. package/plugins/zcode/dist/src-ts/plugins/workbuddy/index.js +567 -0
  148. package/plugins/zcode/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  149. package/plugins/zcode/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  150. package/plugins/zcode/dist/src-ts/plugins/zcode/index.js +97 -0
  151. package/plugins/zcode/package.json +16 -0
  152. package/plugins/zcode/plugin.json +11 -0
@@ -256,15 +256,45 @@ class QwenCdpClient {
256
256
  const addedIds = attachments?.length ? await this.injectAttachments(attachments) : [];
257
257
  stage = "向千问办公提交任务(尚未确认发送)";
258
258
  code = "QWEN_SUBMIT_FAILED";
259
+ let submitUrl = "";
260
+ let inserted = false;
261
+ let submitAttempted = false;
259
262
  try {
263
+ submitUrl = String(await this.evaluate("location.href"));
264
+ const readyToSubmit = `(() => {
265
+ const input = document.querySelector(${JSON.stringify(exports.inputSelector)});
266
+ const button = document.querySelector('button[aria-label="发送消息"]');
267
+ const generating = document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]');
268
+ return Boolean(!document.hidden && location.href === ${JSON.stringify(submitUrl)} && input && input.innerText.trim() === ${JSON.stringify(text.trim())}
269
+ && button && !button.disabled && button.getAttribute('aria-disabled') !== 'true' && !generating);
270
+ })()`;
260
271
  await this.cdp("Input.insertText", { text });
261
- const sent = await this.evaluate(`(() => { const input = document.querySelector(${JSON.stringify(exports.inputSelector)}); const button = document.querySelector('button[aria-label="发送消息"]'); if (!input || input.innerText.trim() !== ${JSON.stringify(text.trim())} || !button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`);
262
- if (sent !== true)
263
- throw new Error("千问发送按钮未就绪或输入校验失败,未发送消息");
272
+ inserted = true;
273
+ // 只有复查确认尚未点击时才再试一次,不能重发提交结果未知的任务。
274
+ for (let attempt = 0; attempt < 2; attempt++) {
275
+ await this.waitFor(readyToSubmit, "千问发送按钮未就绪或输入校验失败,未发送消息");
276
+ submitAttempted = true;
277
+ const sent = await this.evaluate(`(() => {
278
+ if (!${readyToSubmit}) return false;
279
+ document.querySelector('button[aria-label="发送消息"]').click();
280
+ return true;
281
+ })()`);
282
+ if (sent === true)
283
+ break;
284
+ submitAttempted = false;
285
+ if (attempt === 1)
286
+ throw new Error("千问发送按钮未就绪或输入校验失败,未发送消息");
287
+ }
264
288
  await this.waitFor(`(() => { const input = document.querySelector(${JSON.stringify(exports.inputSelector)}); return Boolean(input && !input.innerText.trim()); })()`, "千问未确认消息已提交");
265
289
  }
266
290
  catch (error) {
267
291
  // CDP 断开时清理也可能失败,保留最初的发送错误。
292
+ if (inserted && !submitAttempted) {
293
+ try {
294
+ await this.recoverDraft({ text, url: submitUrl });
295
+ }
296
+ catch { /* 清理失败不能覆盖原错。 */ }
297
+ }
268
298
  try {
269
299
  await this.cleanupAttachments(addedIds);
270
300
  }
@@ -283,6 +313,7 @@ class QwenCdpClient {
283
313
  while (Date.now() < deadline || signal?.aborted) {
284
314
  // 上游取消不停止桌面生成;确认结束前保留插件队列。
285
315
  await wait(2_000);
316
+ await this.restoreVisible();
286
317
  const messagesJson = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.assistantMessageSelector)})].map(element => ({ id: element.getAttribute('data-message-id'), text: element.innerText })))`);
287
318
  const messages = JSON.parse(messagesJson || "[]");
288
319
  const fresh = freshAssistantMessages(beforeIds, messages);
@@ -546,9 +577,8 @@ class QwenCdpClient {
546
577
  try {
547
578
  await this.evaluate(`(async () => {
548
579
  if (document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')) throw new Error('千问桌面端正在生成回复');
549
- // 隐藏窗口会暂停消息渲染,使桌面端的提交前检查一直等不到确认。
550
- if (document.hidden && typeof window.desktopApi?.focusMainWindow === 'function') await window.desktopApi.focusMainWindow();
551
580
  })()`);
581
+ await this.restoreVisible();
552
582
  }
553
583
  catch (error) {
554
584
  let permission;
@@ -561,8 +591,13 @@ class QwenCdpClient {
561
591
  throw error;
562
592
  }
563
593
  }
564
- recoverDraft() {
565
- return (0, cdp_draft_1.recoverCdpDraft)(this.evaluate.bind(this), exports.inputSelector, 'button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]', this.options.dataDir);
594
+ async restoreVisible() {
595
+ await this.evaluate(`(async () => {
596
+ if (document.hidden && typeof window.desktopApi?.focusMainWindow === 'function') await window.desktopApi.focusMainWindow();
597
+ })()`);
598
+ }
599
+ recoverDraft(expected) {
600
+ return (0, cdp_draft_1.recoverCdpDraft)(this.evaluate.bind(this), exports.inputSelector, 'button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]', this.options.dataDir, expected);
566
601
  }
567
602
  async cdp(method, params = {}, timeoutMs = this.options.timeoutMs) {
568
603
  if (!this.socket)
@@ -624,6 +659,7 @@ class QwenCdpClient {
624
659
  async waitFor(expression, message) {
625
660
  const deadline = Date.now() + this.options.timeoutMs;
626
661
  while (Date.now() < deadline) {
662
+ await this.restoreVisible();
627
663
  if (await this.evaluate(expression))
628
664
  return;
629
665
  await wait(200);
@@ -12,7 +12,7 @@ const desktop_launch_1 = require("../desktop-launch");
12
12
  const session_history_1 = require("./session-history");
13
13
  exports.qwenworkManifest = {
14
14
  id: "qwenwork",
15
- version: "1.0.25",
15
+ version: "1.0.28",
16
16
  displayName: "千问办公",
17
17
  entry: "dist/index.js",
18
18
  apiVersion: "1",
@@ -52,6 +52,7 @@ class QwenworkPlugin {
52
52
  state = "stopped";
53
53
  message = "";
54
54
  healthIntervalMs;
55
+ restartDesktopHook;
55
56
  healthTimer;
56
57
  recovery;
57
58
  context;
@@ -70,6 +71,7 @@ class QwenworkPlugin {
70
71
  this.configuredCdpPort = options.cdpPort;
71
72
  this.configuredHttpPort = options.httpPort;
72
73
  this.healthIntervalMs = options.healthIntervalMs ?? 30_000;
74
+ this.restartDesktopHook = options.restartDesktop;
73
75
  }
74
76
  async activate(context) {
75
77
  if (this.state === "ready")
@@ -133,6 +135,7 @@ class QwenworkPlugin {
133
135
  if (!executable)
134
136
  throw new Error("未找到 QwenWorkCN.exe,请配置 executablePath");
135
137
  context.logger?.info(`正在启动千问办公,等待 CDP ${this.activeCdpPort}`, { executablePath: executable });
138
+ await this.restartDesktopProcess(executable);
136
139
  try {
137
140
  await (0, desktop_launch_1.launchAndWait)(executable, [`--remote-debugging-port=${this.activeCdpPort}`], () => client.connect(), typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000);
138
141
  }
@@ -167,8 +170,15 @@ class QwenworkPlugin {
167
170
  throw error;
168
171
  }
169
172
  }
170
- async deactivate(_reason) {
173
+ async deactivate(reason, options) {
174
+ const reloadDesktop = !options?.preserveDesktop && reason === "reload" && Boolean(this.client) && !this.relayTarget;
171
175
  await this.cleanup();
176
+ if (reloadDesktop) {
177
+ if (this.restartDesktopHook)
178
+ await this.restartDesktopHook();
179
+ else if (!this.injectedClient && process.platform === "win32")
180
+ await this.restartDesktopProcess();
181
+ }
172
182
  this.state = "stopped";
173
183
  this.stopped = true;
174
184
  }
@@ -181,8 +191,6 @@ class QwenworkPlugin {
181
191
  return { status: this.state, ...(this.message ? { message: this.message } : {}), details };
182
192
  }
183
193
  async handle(request) {
184
- if (this.recovery)
185
- await this.recovery;
186
194
  if (this.state === "stopped" || (!this.relayTarget && !this.client))
187
195
  throw new Error("qwenwork 插件未就绪");
188
196
  return this.enqueueTask(() => this.completeRequest(request));
@@ -197,9 +205,8 @@ class QwenworkPlugin {
197
205
  throw new Error("千问办公当前忙,无法创建新会话");
198
206
  if (!this.client?.createSession)
199
207
  throw new Error("千问办公当前传输不支持新建会话");
200
- if (!this.client.connected())
201
- throw new Error("千问办公 CDP 未连接");
202
208
  return this.enqueueTask(async () => {
209
+ await this.ensureConnected();
203
210
  await this.client.createSession(request.name);
204
211
  return { text: "千问办公已创建新会话", sessionId };
205
212
  });
@@ -212,8 +219,7 @@ class QwenworkPlugin {
212
219
  if (!this.client)
213
220
  throw new Error("qwenwork 插件未就绪");
214
221
  try {
215
- if (!this.client.connected())
216
- throw new Error("千问 CDP 已断开,等待空闲后自动恢复");
222
+ await this.ensureConnected(signal);
217
223
  const reply = this.client.complete ? (await this.client.complete(prompt, timeoutMs, signal, undefined, undefined, onMessage)).text : await this.client.direct(prompt, timeoutMs, signal);
218
224
  if ((0, cdp_client_1.isQwenApiUnavailableReply)(reply))
219
225
  throw new agent_1.AgentOperationError("QWEN_REMOTE_ERROR", "等待千问办公回复", new Error("千问生成接口不可用;桌面未提供具体服务端错误码,CDP 仍连接,未重启或重新提交任务"));
@@ -223,7 +229,7 @@ class QwenworkPlugin {
223
229
  return reply;
224
230
  }
225
231
  catch (error) {
226
- if (!signal?.aborted) {
232
+ if (!this.stopped && !signal?.aborted) {
227
233
  this.message = error instanceof Error ? error.message : String(error);
228
234
  this.failures = this.client.connected() ? 0 : 2;
229
235
  this.state = "degraded";
@@ -238,8 +244,10 @@ class QwenworkPlugin {
238
244
  if (!this.client)
239
245
  throw new Error("qwenwork 插件未就绪");
240
246
  try {
241
- if (!this.client.connected())
242
- throw new Error("千问 CDP 已断开,等待空闲后自动恢复");
247
+ if (modelRequest && !this.client.connected())
248
+ throw new Error("千问 CDP 已断开,模型请求不会启动或重启桌面");
249
+ if (!modelRequest)
250
+ await this.ensureConnected(request.signal);
243
251
  if (request.attachments?.length && (!this.client.attachmentInput || !this.client.complete))
244
252
  throw new Error("千问当前客户端不支持附件输入");
245
253
  const timeoutMs = request.timeoutMs ?? asNumber(this.context?.config.timeoutMs, 240_000);
@@ -254,7 +262,7 @@ class QwenworkPlugin {
254
262
  return response;
255
263
  }
256
264
  catch (error) {
257
- if (!request.signal?.aborted && !modelRequest) {
265
+ if (!this.stopped && !request.signal?.aborted && !modelRequest) {
258
266
  this.message = error instanceof Error ? error.message : String(error);
259
267
  this.failures = this.client.connected() ? 0 : 2;
260
268
  this.state = "degraded";
@@ -343,6 +351,14 @@ class QwenworkPlugin {
343
351
  this.callPlugin = undefined;
344
352
  this.pluginStatus = undefined;
345
353
  }
354
+ async restartDesktopProcess(configuredExecutable) {
355
+ const config = this.context?.config;
356
+ const executable = configuredExecutable ?? await (0, desktop_launch_1.findWindowsExecutable)("QwenWorkCN.exe", typeof config?.executablePath === "string" ? config.executablePath : undefined);
357
+ if (!executable)
358
+ throw new Error("未找到 QwenWorkCN.exe,请配置 executablePath");
359
+ const processIds = await (0, desktop_launch_1.terminateWindowsDesktop)(executable);
360
+ this.context?.logger?.info("千问办公重载前已清理旧进程,将以 CDP 模式启动", { processIds, executable });
361
+ }
346
362
  startHealthWatch() {
347
363
  if (this.relayTarget || !this.client || this.healthTimer)
348
364
  return;
@@ -365,9 +381,26 @@ class QwenworkPlugin {
365
381
  });
366
382
  }
367
383
  async recover() {
384
+ if (this.stopped || this.busy)
385
+ return;
386
+ try {
387
+ await this.reconnect();
388
+ }
389
+ catch { /* 失败已记录,留待下次空闲检查。 */ }
390
+ }
391
+ async ensureConnected(signal) {
392
+ signal?.throwIfAborted();
393
+ if (this.stopped || !this.client)
394
+ throw new Error("qwenwork 插件未就绪");
395
+ await this.reconnect();
396
+ signal?.throwIfAborted();
397
+ if (this.stopped)
398
+ throw new Error("qwenwork 插件未就绪");
399
+ }
400
+ async reconnect() {
368
401
  if (this.recovery)
369
402
  return this.recovery;
370
- if (this.stopped || this.busy)
403
+ if (this.stopped)
371
404
  return;
372
405
  const client = this.client;
373
406
  if (!client || client.connected())
@@ -388,11 +421,12 @@ class QwenworkPlugin {
388
421
  this.restarts += 1;
389
422
  this.context?.logger?.info("千问 CDP 自动重连成功", { restarts: this.restarts });
390
423
  })().catch((error) => {
391
- if (this.stopped)
392
- return;
393
- this.state = "failed";
394
- this.message = error instanceof Error ? error.message : String(error);
395
- this.context?.logger?.error(`千问 CDP 重连失败,桌面未重启,下次空闲检查重试:${this.message}`);
424
+ if (!this.stopped) {
425
+ this.state = "failed";
426
+ this.message = error instanceof Error ? error.message : String(error);
427
+ this.context?.logger?.error(`千问 CDP 重连失败,桌面未重启,下次空闲检查重试:${this.message}`);
428
+ }
429
+ throw error;
396
430
  }).finally(() => { this.recovery = undefined; });
397
431
  return this.recovery;
398
432
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-qwenwork",
3
- "version": "1.0.25",
3
+ "version": "1.0.28",
4
4
  "description": "agent-bridge 的千问办公插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "qwenwork",
3
- "version": "1.0.25",
3
+ "version": "1.0.28",
4
4
  "type": "agent",
5
5
  "displayName": "千问办公",
6
6
  "entry": "dist/src-ts/plugins/qwenwork/index.js",
@@ -9,7 +9,7 @@
9
9
  "cdpPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 9229 },
10
10
  "autoStart": { "type": "boolean", "default": true, "description": "未连接时自动启动 WorkBuddy" },
11
11
  "startupTimeoutMs": { "type": "integer", "minimum": 1000, "default": 30000 },
12
- "timeoutMs": { "type": "integer", "minimum": 1, "default": 3600000, "description": "单次回复等待时间(毫秒),默认一小时;超时不终止桌面任务" },
12
+ "timeoutMs": { "type": "integer", "minimum": 1, "default": 1200000, "description": "单次回复等待时间(毫秒),最多二十分钟;较短配置继续生效,超时不终止桌面任务" },
13
13
  "dailyPoints": { "type": "boolean", "default": true, "description": "每天自动检查并领取页面明确提供的免费积分" },
14
14
  "dailyPointsHour": { "type": "integer", "minimum": 0, "maximum": 23, "default": 9, "description": "每日免费积分领取时间(本机小时)" },
15
15
  "dailyPointsMinute": { "type": "integer", "minimum": 0, "maximum": 59, "default": 0, "description": "每日免费积分领取时间(本机分钟)" }
@@ -16,8 +16,8 @@ const node_path_1 = require("node:path");
16
16
  const node_util_1 = require("node:util");
17
17
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
18
18
  const versionDirectory = /^(?:app[-_ ]?)?v?\d+(?:[.-]\d+)+$/i;
19
- /** 在 Windows 常见安装目录中查找桌面程序,避免依赖单一盘符。 */
20
- async function findWindowsExecutable(fileName, configured) {
19
+ /** 先从 Windows 注册信息和开始菜单解析,失败时再查常见安装目录。 */
20
+ async function findWindowsExecutable(fileName, configured, run = execFileAsync) {
21
21
  if (configured?.trim()) {
22
22
  const configuredPath = (0, node_path_1.resolve)(configured);
23
23
  if (fileName.toLowerCase() === "doubao.exe") {
@@ -42,12 +42,16 @@ async function findWindowsExecutable(fileName, configured) {
42
42
  }
43
43
  if (process.platform !== "win32")
44
44
  return undefined;
45
- const pattern = /doubao|豆包|qwen|qwwork|千问|deepseek|harness|dsh/i;
45
+ const registered = await findRegisteredWindowsExecutable(fileName, run);
46
+ if (registered)
47
+ return registered;
48
+ const pattern = /doubao|豆包|qwen|qwwork|千问|deepseek|harness|dsh|zcode/i;
46
49
  const roots = [...new Set([
47
50
  process.env.DSH_INSTALL_DIR,
48
51
  "D:\\Programs\\DeepSeek Harness",
49
52
  "D:\\DSH",
50
53
  process.env.LOCALAPPDATA && (0, node_path_1.join)(process.env.LOCALAPPDATA, "Programs"),
54
+ process.env.LOCALAPPDATA,
51
55
  process.env.ProgramFiles,
52
56
  process.env["ProgramFiles(x86)"],
53
57
  ...Array.from({ length: 24 }, (_, index) => `${String.fromCharCode(67 + index)}:\\`),
@@ -59,6 +63,36 @@ async function findWindowsExecutable(fileName, configured) {
59
63
  }
60
64
  return undefined;
61
65
  }
66
+ async function findRegisteredWindowsExecutable(fileName, run) {
67
+ const target = fileName.replace(/'/g, "''");
68
+ const script = `$ErrorActionPreference='SilentlyContinue';[Console]::OutputEncoding=[Text.Encoding]::UTF8;$target='${target}'
69
+ function Emit-Path($value){if(-not $value){return};$value=[Environment]::ExpandEnvironmentVariables(([string]$value).Trim());if($value.StartsWith('"')){$end=$value.IndexOf('"',1);if($end -gt 1){$value=$value.Substring(1,$end-1)}}elseif($value -match '^(\S+?\.exe)(?:\s|$)'){$value=$Matches[1]};if($value -match '(?i)\.exe$'){[Console]::WriteLine($value)}}
70
+ $keys=@("HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\$target","HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\$target","HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\$target","HKCU:\\Software\\Classes\\Applications\\$target\\shell\\open\\command","HKLM:\\Software\\Classes\\Applications\\$target\\shell\\open\\command")
71
+ foreach($key in $keys){$item=Get-Item $key -ErrorAction SilentlyContinue;if($item){Emit-Path $item.GetValue('')}}
72
+ $dirs=@();if($env:APPDATA){$dirs+=[IO.Path]::Combine($env:APPDATA,'Microsoft\\Windows\\Start Menu\\Programs')};if($env:ProgramData){$dirs+=[IO.Path]::Combine($env:ProgramData,'Microsoft\\Windows\\Start Menu\\Programs')};$shell=New-Object -ComObject WScript.Shell
73
+ foreach($dir in $dirs){Get-ChildItem $dir -Filter '*.lnk' -File -Recurse -ErrorAction SilentlyContinue|ForEach-Object{$link=$shell.CreateShortcut($_.FullName);if($link.TargetPath -and [IO.Path]::GetFileName($link.TargetPath) -ieq $target){Emit-Path $link.TargetPath}}}`;
74
+ try {
75
+ const { stdout } = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { timeout: 10_000, windowsHide: true });
76
+ for (const candidate of stdout.split(/\r?\n/).map(value => value.trim()).filter(Boolean)) {
77
+ const found = await resolveRegisteredCandidate(fileName, candidate);
78
+ if (found)
79
+ return found;
80
+ }
81
+ }
82
+ catch { /* 注册表或开始菜单不可读时继续使用目录兜底 */ }
83
+ return undefined;
84
+ }
85
+ async function resolveRegisteredCandidate(fileName, candidate) {
86
+ const candidatePath = (0, node_path_1.resolve)(candidate.replace(/^"|"$/g, ""));
87
+ if (fileName.toLowerCase() === "doubao.exe" && (0, node_path_1.basename)((0, node_path_1.dirname)(candidatePath)).toLowerCase() === "application") {
88
+ const client = (0, node_path_1.join)((0, node_path_1.dirname)(candidatePath), "app", fileName);
89
+ if ((0, node_fs_1.existsSync)(client))
90
+ return client;
91
+ }
92
+ if ((0, node_fs_1.existsSync)(candidatePath) && (0, node_path_1.basename)(candidatePath).toLowerCase() === fileName.toLowerCase())
93
+ return candidatePath;
94
+ return findExecutableInTree((0, node_path_1.dirname)(candidatePath), fileName, /^/, 3);
95
+ }
62
96
  async function findExecutableInTree(directory, fileName, pattern, depth) {
63
97
  return search(directory, fileName, pattern, depth, pattern.test(directory));
64
98
  }
@@ -104,9 +138,16 @@ async function launchManagedAndWait(executable, args, profileDirectory, connect,
104
138
  if (profileDirectory)
105
139
  await (0, promises_1.mkdir)(profileDirectory, { recursive: true });
106
140
  const child = launch(executable, profileDirectory ? [...args, `--user-data-dir=${profileDirectory}`] : args);
141
+ // 先等待 spawn,避免异步 error 终止 daemon 或在启动失败后继续轮询。
142
+ const events = child;
143
+ const started = typeof events.once === "function" ? new Promise((resolve, reject) => {
144
+ events.once("error", reject);
145
+ events.once("spawn", resolve);
146
+ }) : Promise.resolve();
107
147
  child.unref();
108
148
  onProcess(child);
109
149
  try {
150
+ await started;
110
151
  return await waitFor(connect, timeoutMs, 250);
111
152
  }
112
153
  catch (error) {