mu-agent-bridge 1.0.71 → 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 (195) hide show
  1. package/README.md +17 -10
  2. package/dist/src-ts/agent.js +35 -0
  3. package/dist/src-ts/channel-sessions.js +3 -2
  4. package/dist/src-ts/cli.js +67 -37
  5. package/dist/src-ts/control-server.js +12 -30
  6. package/dist/src-ts/core-updater.js +141 -33
  7. package/dist/src-ts/daemon-process.js +2 -1
  8. package/dist/src-ts/dsh-agent.js +31 -13
  9. package/dist/src-ts/http-agent.js +36 -0
  10. package/dist/src-ts/plugin-catalog.js +11 -2
  11. package/dist/src-ts/plugin-manager.js +351 -50
  12. package/dist/src-ts/plugins/abweb/index.js +1 -1
  13. package/dist/src-ts/plugins/abweb/public/app.js +73 -5
  14. package/dist/src-ts/plugins/abweb/public/index.html +15 -0
  15. package/dist/src-ts/plugins/abweb/public/style.css +4 -0
  16. package/dist/src-ts/plugins/aun/attachments.js +3 -21
  17. package/dist/src-ts/plugins/aun/index.js +155 -21
  18. package/dist/src-ts/plugins/aun/messages.js +4 -1
  19. package/dist/src-ts/plugins/aun/public/app.js +2 -1
  20. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  21. package/dist/src-ts/plugins/cdp-draft.js +5 -2
  22. package/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
  23. package/dist/src-ts/plugins/chatgpt/index.js +80 -19
  24. package/dist/src-ts/plugins/claude-cli/index.js +19 -2
  25. package/dist/src-ts/plugins/codex-cli/index.js +19 -2
  26. package/dist/src-ts/plugins/desktop-launch.js +44 -3
  27. package/dist/src-ts/plugins/doubao/cdp-client.js +234 -45
  28. package/dist/src-ts/plugins/doubao/index.js +152 -8
  29. package/dist/src-ts/plugins/doubao/native-task.js +134 -0
  30. package/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  31. package/dist/src-ts/plugins/dsh/index.js +172 -21
  32. package/dist/src-ts/plugins/dsh/session-history.js +31 -3
  33. package/dist/src-ts/plugins/feishu/index.js +1 -1
  34. package/dist/src-ts/plugins/local-cli/agent.js +74 -9
  35. package/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
  36. package/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
  37. package/dist/src-ts/plugins/personal-assistant/index.js +263 -0
  38. package/dist/src-ts/plugins/personal-assistant/model-client.js +181 -0
  39. package/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
  40. package/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
  41. package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  42. package/dist/src-ts/plugins/personal-assistant/tools.js +167 -0
  43. package/dist/src-ts/plugins/qwenwork/cdp-client.js +90 -18
  44. package/dist/src-ts/plugins/qwenwork/index.js +139 -24
  45. package/dist/src-ts/plugins/workbuddy/cdp-client.js +353 -82
  46. package/dist/src-ts/plugins/workbuddy/index.js +124 -41
  47. package/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  48. package/dist/src-ts/plugins/zcode/index.js +97 -0
  49. package/dist/src-ts/session-management.js +132 -14
  50. package/dist/src-ts/version-manager.js +6 -3
  51. package/dist/tests-ts/abweb-cli.test.js +10 -2
  52. package/dist/tests-ts/abweb-model-config.test.js +73 -0
  53. package/dist/tests-ts/agent.test.js +27 -0
  54. package/dist/tests-ts/aun-attachments.test.js +19 -17
  55. package/dist/tests-ts/aun-default-owner-page.test.js +2 -1
  56. package/dist/tests-ts/aun-messages.test.js +20 -0
  57. package/dist/tests-ts/aun-plugin.test.js +292 -19
  58. package/dist/tests-ts/bundled-package.test.js +12 -2
  59. package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
  60. package/dist/tests-ts/channel-sessions.test.js +27 -0
  61. package/dist/tests-ts/chatgpt-plugin.test.js +118 -0
  62. package/dist/tests-ts/control-server.test.js +56 -0
  63. package/dist/tests-ts/core-updater.test.js +215 -46
  64. package/dist/tests-ts/daemon-process.test.js +126 -1
  65. package/dist/tests-ts/desktop-launch.test.js +73 -0
  66. package/dist/tests-ts/doubao-native-task.test.js +237 -0
  67. package/dist/tests-ts/doubao-plugin.test.js +773 -12
  68. package/dist/tests-ts/dsh-agent.test.js +157 -8
  69. package/dist/tests-ts/dsh-cdp.test.js +148 -0
  70. package/dist/tests-ts/dsh-history.test.js +37 -0
  71. package/dist/tests-ts/feishu-plugin.test.js +1 -1
  72. package/dist/tests-ts/http-agent.test.js +18 -0
  73. package/dist/tests-ts/installer-assets.test.js +1 -1
  74. package/dist/tests-ts/local-cli.test.js +146 -1
  75. package/dist/tests-ts/model-provider-cancellation.test.js +152 -0
  76. package/dist/tests-ts/native-session-management.test.js +8 -7
  77. package/dist/tests-ts/personal-assistant-coordination.test.js +92 -0
  78. package/dist/tests-ts/personal-assistant-extensions.test.js +35 -0
  79. package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
  80. package/dist/tests-ts/personal-assistant-owner-context.test.js +107 -0
  81. package/dist/tests-ts/personal-assistant-remote-ability.test.js +123 -0
  82. package/dist/tests-ts/personal-assistant.test.js +663 -0
  83. package/dist/tests-ts/platform-integration.test.js +183 -1
  84. package/dist/tests-ts/plugin-catalog.test.js +12 -2
  85. package/dist/tests-ts/plugin-manager.test.js +356 -4
  86. package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
  87. package/dist/tests-ts/publish-packages.test.js +42 -0
  88. package/dist/tests-ts/qwenwork-plugin.test.js +135 -1
  89. package/dist/tests-ts/qwenwork-reconnect.test.js +195 -0
  90. package/dist/tests-ts/qwenwork-sse.test.js +33 -0
  91. package/dist/tests-ts/session-access-defaults.test.js +112 -0
  92. package/dist/tests-ts/session-management.test.js +159 -4
  93. package/dist/tests-ts/version-manager.test.js +40 -0
  94. package/dist/tests-ts/workbuddy-cdp-recovery.test.js +311 -0
  95. package/dist/tests-ts/workbuddy-plugin.test.js +499 -4
  96. package/dist/tests-ts/zcode-cdp.test.js +894 -0
  97. package/dist/tests-ts/zcode-plugin.test.js +396 -0
  98. package/package.json +1 -1
  99. package/plugins/abweb/dist/src-ts/agent.js +35 -0
  100. package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
  101. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +73 -5
  102. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +57 -5
  103. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +15 -0
  104. package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +4 -0
  105. package/plugins/abweb/package.json +1 -1
  106. package/plugins/abweb/plugin.json +1 -1
  107. package/plugins/aun/config.schema.json +1 -1
  108. package/plugins/aun/dist/src-ts/agent.js +35 -0
  109. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
  110. package/plugins/aun/dist/src-ts/plugins/aun/index.js +155 -21
  111. package/plugins/aun/dist/src-ts/plugins/aun/messages.js +4 -1
  112. package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +2 -1
  113. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  114. package/plugins/aun/package.json +1 -1
  115. package/plugins/aun/plugin.json +1 -1
  116. package/plugins/chatgpt/dist/src-ts/agent.js +35 -0
  117. package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
  118. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
  119. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +80 -19
  120. package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
  121. package/plugins/chatgpt/package.json +1 -1
  122. package/plugins/chatgpt/plugin.json +1 -1
  123. package/plugins/claude-cli/dist/src-ts/agent.js +35 -0
  124. package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +19 -2
  125. package/plugins/claude-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
  126. package/plugins/claude-cli/package.json +1 -1
  127. package/plugins/claude-cli/plugin.json +3 -2
  128. package/plugins/codex-cli/dist/src-ts/agent.js +35 -0
  129. package/plugins/codex-cli/dist/src-ts/plugins/codex-cli/index.js +19 -2
  130. package/plugins/codex-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
  131. package/plugins/codex-cli/package.json +1 -1
  132. package/plugins/codex-cli/plugin.json +3 -2
  133. package/plugins/doubao/config.schema.json +2 -1
  134. package/plugins/doubao/dist/src-ts/agent.js +35 -0
  135. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
  136. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +234 -45
  137. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +152 -8
  138. package/plugins/doubao/dist/src-ts/plugins/doubao/native-task.js +134 -0
  139. package/plugins/doubao/package.json +1 -1
  140. package/plugins/doubao/plugin.json +3 -2
  141. package/plugins/dsh/config.schema.json +5 -2
  142. package/plugins/dsh/dist/src-ts/agent.js +35 -0
  143. package/plugins/dsh/dist/src-ts/dsh-agent.js +31 -13
  144. package/plugins/dsh/dist/src-ts/plugins/desktop-launch.js +44 -3
  145. package/plugins/dsh/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  146. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +172 -21
  147. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +31 -3
  148. package/plugins/dsh/package.json +1 -1
  149. package/plugins/dsh/plugin.json +3 -2
  150. package/plugins/feishu/dist/src-ts/agent.js +35 -0
  151. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +1 -1
  152. package/plugins/feishu/package.json +1 -1
  153. package/plugins/feishu/plugin.json +1 -1
  154. package/plugins/personal-assistant/config.schema.json +23 -0
  155. package/plugins/personal-assistant/dist/src-ts/agent.js +69 -0
  156. package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
  157. package/plugins/personal-assistant/dist/src-ts/plugin.js +13 -0
  158. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
  159. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
  160. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +263 -0
  161. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +181 -0
  162. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
  163. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
  164. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  165. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +167 -0
  166. package/plugins/personal-assistant/package.json +39 -0
  167. package/plugins/personal-assistant/plugin.json +15 -0
  168. package/plugins/qwenwork/dist/src-ts/agent.js +35 -0
  169. package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +5 -2
  170. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +44 -3
  171. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +90 -18
  172. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +139 -24
  173. package/plugins/qwenwork/package.json +1 -1
  174. package/plugins/qwenwork/plugin.json +3 -2
  175. package/plugins/workbuddy/config.schema.json +1 -1
  176. package/plugins/workbuddy/dist/src-ts/agent.js +35 -0
  177. package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
  178. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +353 -82
  179. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +124 -41
  180. package/plugins/workbuddy/package.json +1 -1
  181. package/plugins/workbuddy/plugin.json +3 -2
  182. package/plugins/zcode/config.schema.json +13 -0
  183. package/plugins/zcode/dist/src-ts/agent.js +69 -0
  184. package/plugins/zcode/dist/src-ts/attachments.js +62 -0
  185. package/plugins/zcode/dist/src-ts/plugin.js +13 -0
  186. package/plugins/zcode/dist/src-ts/plugins/desktop-launch.js +238 -0
  187. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cdp-client.js +896 -0
  188. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
  189. package/plugins/zcode/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  190. package/plugins/zcode/dist/src-ts/plugins/workbuddy/index.js +567 -0
  191. package/plugins/zcode/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  192. package/plugins/zcode/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  193. package/plugins/zcode/dist/src-ts/plugins/zcode/index.js +97 -0
  194. package/plugins/zcode/package.json +16 -0
  195. package/plugins/zcode/plugin.json +11 -0
@@ -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) {
@@ -127,19 +127,40 @@ class QwenCdpClient {
127
127
  const reply = await this.direct(`这是后台健康检测。请只回复 ${token},不要添加任何其它文字。`, 45_000, undefined, token);
128
128
  verifyQwenHeartbeatReply(token, reply);
129
129
  }
130
- async createSession(_name) {
130
+ async createSession(_name, forceNew = false, preserveDraft = false) {
131
131
  await this.ensureVisible();
132
- await this.recoverDraft();
132
+ if (!preserveDraft)
133
+ await this.recoverDraft();
133
134
  const snapshot = `(() => ({ url: location.href, messages: document.querySelectorAll('[data-message-id]').length, input: document.querySelector(${JSON.stringify(exports.inputSelector)})?.innerText ?? null, blank: Boolean(document.querySelector('.new-task-model-selector')) }))()`;
134
135
  const before = await this.evaluate(snapshot);
135
136
  if (before.input?.trim())
136
137
  throw new Error("千问输入框含有未发送内容,不能新建会话");
137
- if (before.input !== null && before.messages === 0 && before.blank)
138
- return;
139
- const clicked = await this.evaluate(`(() => { const button = document.querySelector('button[aria-label="新任务"]'); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`);
138
+ if (!forceNew && before.input !== null && before.messages === 0 && before.blank)
139
+ return this.currentSessionId();
140
+ const clicked = await this.evaluate(`(() => { if (document.querySelector(${JSON.stringify(exports.inputSelector)})?.innerText.trim() || document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')) throw new Error('千问当前有未发送内容或正在生成回复,不能新建会话'); const button = document.querySelector('button[aria-label="新任务"]'); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`);
140
141
  if (clicked !== true)
141
142
  throw new Error("千问新任务入口未找到或不可用");
142
143
  await this.waitFor(`(() => { const state = ${snapshot}; return state.input !== null && state.input.trim() === '' && state.messages === 0 && state.url !== ${JSON.stringify(before.url)}; })()`, "千问新会话状态校验失败");
144
+ return this.currentSessionId();
145
+ }
146
+ async currentSessionId() {
147
+ const href = String(await this.evaluate("location.href"));
148
+ try {
149
+ return new URL(href).searchParams.get("chat") ?? undefined;
150
+ }
151
+ catch {
152
+ return undefined;
153
+ }
154
+ }
155
+ async activateSession(sessionId) {
156
+ if (!sessionId.trim() || sessionId.length > 512 || /[\u0000-\u001f]/.test(sessionId))
157
+ throw new Error("千问模型会话标识无效");
158
+ await this.ensureVisible();
159
+ const target = new URL(String(await this.evaluate("location.href")));
160
+ target.searchParams.set("chat", sessionId);
161
+ if (target.href !== String(await this.evaluate("location.href")))
162
+ await this.evaluate(`(() => { if (document.querySelector(${JSON.stringify(exports.inputSelector)})?.innerText.trim() || document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')) throw new Error('千问当前有未发送内容或正在生成回复,不能切换会话'); location.href = ${JSON.stringify(target.href)}; })()`);
163
+ await this.waitFor(`location.href === ${JSON.stringify(target.href)} && Boolean(document.querySelector(${JSON.stringify(exports.inputSelector)}))`, "千问个人助理模型会话恢复失败");
143
164
  }
144
165
  async connect() {
145
166
  if (this.connected())
@@ -204,7 +225,7 @@ class QwenCdpClient {
204
225
  async direct(text, timeoutMs = 240_000, signal, expectedReply) {
205
226
  return (await this.complete(text, timeoutMs, signal, expectedReply)).text;
206
227
  }
207
- async complete(text, timeoutMs = 240_000, signal, expectedReply, attachments, onMessage) {
228
+ async complete(text, timeoutMs = 240_000, signal, expectedReply, attachments, onMessage, preserveDraft = false) {
208
229
  let stage = "准备千问办公桌面任务";
209
230
  let code = "QWEN_TASK_FAILED";
210
231
  try {
@@ -215,7 +236,8 @@ class QwenCdpClient {
215
236
  throw new Error("千问输入框有用户草稿,跳过后台健康检测");
216
237
  }
217
238
  await this.ensureVisible();
218
- await this.recoverDraft();
239
+ if (!preserveDraft)
240
+ await this.recoverDraft();
219
241
  const ready = await this.evaluate(`(() => { const input = document.querySelector(${JSON.stringify(exports.inputSelector)}); return Boolean(input && !input.innerText.trim() && !document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')); })()`);
220
242
  if (!ready)
221
243
  throw new Error("千问输入框含有未发送内容或正在生成回复,未发送新消息");
@@ -234,15 +256,45 @@ class QwenCdpClient {
234
256
  const addedIds = attachments?.length ? await this.injectAttachments(attachments) : [];
235
257
  stage = "向千问办公提交任务(尚未确认发送)";
236
258
  code = "QWEN_SUBMIT_FAILED";
259
+ let submitUrl = "";
260
+ let inserted = false;
261
+ let submitAttempted = false;
237
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
+ })()`;
238
271
  await this.cdp("Input.insertText", { text });
239
- 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; })()`);
240
- if (sent !== true)
241
- 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
+ }
242
288
  await this.waitFor(`(() => { const input = document.querySelector(${JSON.stringify(exports.inputSelector)}); return Boolean(input && !input.innerText.trim()); })()`, "千问未确认消息已提交");
243
289
  }
244
290
  catch (error) {
245
291
  // CDP 断开时清理也可能失败,保留最初的发送错误。
292
+ if (inserted && !submitAttempted) {
293
+ try {
294
+ await this.recoverDraft({ text, url: submitUrl });
295
+ }
296
+ catch { /* 清理失败不能覆盖原错。 */ }
297
+ }
246
298
  try {
247
299
  await this.cleanupAttachments(addedIds);
248
300
  }
@@ -254,23 +306,38 @@ class QwenCdpClient {
254
306
  code = "QWEN_REPLY_FAILED";
255
307
  let previousReply = "";
256
308
  let stablePolls = 0;
309
+ let seenGenerating = false;
310
+ let stoppedPolls = 0;
257
311
  const forwarded = new Set();
258
312
  const deadline = Date.now() + timeoutMs;
259
- while (Date.now() < deadline) {
260
- await wait(2_000, signal);
313
+ while (Date.now() < deadline || signal?.aborted) {
314
+ // 上游取消不停止桌面生成;确认结束前保留插件队列。
315
+ await wait(2_000);
316
+ await this.restoreVisible();
261
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 })))`);
262
- const fresh = freshAssistantMessages(beforeIds, JSON.parse(messagesJson || "[]"));
318
+ const messages = JSON.parse(messagesJson || "[]");
319
+ const fresh = freshAssistantMessages(beforeIds, messages);
263
320
  const reply = fresh.at(-1)?.text.trim() ?? "";
264
321
  stablePolls = reply && reply === previousReply ? stablePolls + 1 : 0;
265
322
  previousReply = reply;
266
323
  if (reply && stablePolls >= 3 && isQwenApiUnavailableReply(reply))
267
324
  throw new Error("QWEN_REMOTE_ERROR:千问生成接口不可用;桌面仅返回通用错误,未提供服务端错误码;CDP 仍连接,未重启或重新提交任务");
268
325
  const generating = await this.evaluate(`Boolean(document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]'))`);
326
+ seenGenerating ||= generating === true;
327
+ stoppedPolls = seenGenerating && generating === false ? stoppedPolls + 1 : 0;
328
+ if (signal?.aborted && !reply && stoppedPolls >= 2 && messages.some(message => message.id && !beforeIds.has(message.id))) {
329
+ const attachments = expectedReply ? [] : await this.readArtifacts(beforeIds, sessionUrl);
330
+ if (attachments.length)
331
+ return { text: "", sessionId: new URL(sessionUrl).searchParams.get("chat") ?? undefined, attachments };
332
+ code = "QWEN_REQUEST_CANCELLED";
333
+ throw new Error("请求已取消,已确认本轮桌面生成结束,未返回正文或附件");
334
+ }
269
335
  if (!generating && (expectedReply ? isQwenHeartbeatReplyComplete(reply, expectedReply) && stablePolls >= 1 : reply && stablePolls >= 3)) {
270
336
  stage = "读取千问办公已完成任务的产物(尚未上传 AUN FS)";
271
337
  code = "QWEN_ARTIFACT_READ_FAILED";
272
338
  const attachments = expectedReply ? [] : await this.readArtifacts(beforeIds, sessionUrl);
273
- return { text: reply, ...(attachments.length ? { attachments } : {}) };
339
+ const sessionId = new URL(sessionUrl).searchParams.get("chat") ?? undefined;
340
+ return { text: reply, ...(sessionId ? { sessionId } : {}), ...(attachments.length ? { attachments } : {}) };
274
341
  }
275
342
  if (onMessage && generating && reply && stablePolls >= 1 && !forwarded.has(reply) && !isQwenApiUnavailableReply(reply)) {
276
343
  await onMessage(reply);
@@ -510,9 +577,8 @@ class QwenCdpClient {
510
577
  try {
511
578
  await this.evaluate(`(async () => {
512
579
  if (document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')) throw new Error('千问桌面端正在生成回复');
513
- // 隐藏窗口会暂停消息渲染,使桌面端的提交前检查一直等不到确认。
514
- if (document.hidden && typeof window.desktopApi?.focusMainWindow === 'function') await window.desktopApi.focusMainWindow();
515
580
  })()`);
581
+ await this.restoreVisible();
516
582
  }
517
583
  catch (error) {
518
584
  let permission;
@@ -525,8 +591,13 @@ class QwenCdpClient {
525
591
  throw error;
526
592
  }
527
593
  }
528
- recoverDraft() {
529
- 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);
530
601
  }
531
602
  async cdp(method, params = {}, timeoutMs = this.options.timeoutMs) {
532
603
  if (!this.socket)
@@ -588,6 +659,7 @@ class QwenCdpClient {
588
659
  async waitFor(expression, message) {
589
660
  const deadline = Date.now() + this.options.timeoutMs;
590
661
  while (Date.now() < deadline) {
662
+ await this.restoreVisible();
591
663
  if (await this.evaluate(expression))
592
664
  return;
593
665
  await wait(200);
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.QwenworkPlugin = exports.qwenworkManifest = void 0;
4
4
  exports.createQwenworkPlugin = createQwenworkPlugin;
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
5
7
  const agent_1 = require("../../agent");
6
8
  const openai_adapter_1 = require("./openai-adapter");
7
9
  const cdp_client_1 = require("./cdp-client");
@@ -10,21 +12,29 @@ const desktop_launch_1 = require("../desktop-launch");
10
12
  const session_history_1 = require("./session-history");
11
13
  exports.qwenworkManifest = {
12
14
  id: "qwenwork",
13
- version: "1.0.24",
15
+ version: "1.0.28",
14
16
  displayName: "千问办公",
15
17
  entry: "dist/index.js",
16
18
  apiVersion: "1",
17
19
  process: "in-process",
18
- capabilities: ["chat", "openai-compatible", "health"],
20
+ capabilities: ["chat", "openai-compatible", "health", "model-provider"],
19
21
  configSchema: "config.schema.json",
20
22
  defaults: { cdpPort: 9228, httpPort: 17893 },
21
23
  };
22
24
  function asNumber(value, fallback) {
23
25
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
24
26
  }
27
+ function validModelSessionId(value) {
28
+ return typeof value === "string" && Boolean(value.trim()) && value.length <= 512 && !/[\u0000-\u001f]/.test(value);
29
+ }
25
30
  /** 千问插件由后台守护,所有通道共用请求队列和恢复流程。 */
26
31
  class QwenworkPlugin {
27
32
  manifest = exports.qwenworkManifest;
33
+ get modelProvider() {
34
+ if (this.relayTarget || !this.client?.complete || !this.client.createSession || !this.client.activateSession)
35
+ return undefined;
36
+ return { complete: (request) => this.completeModelProvider(request) };
37
+ }
28
38
  sessionHistory;
29
39
  get attachmentInput() { return Boolean(this.relayTarget || this.client?.attachmentInput); }
30
40
  sessionOperations = [];
@@ -42,6 +52,7 @@ class QwenworkPlugin {
42
52
  state = "stopped";
43
53
  message = "";
44
54
  healthIntervalMs;
55
+ restartDesktopHook;
45
56
  healthTimer;
46
57
  recovery;
47
58
  context;
@@ -53,11 +64,14 @@ class QwenworkPlugin {
53
64
  busy = false;
54
65
  taskCount = 0;
55
66
  stopped = true;
67
+ modelSessions = new Map();
68
+ modelSessionsPath;
56
69
  constructor(options = {}) {
57
70
  this.injectedClient = options.client;
58
71
  this.configuredCdpPort = options.cdpPort;
59
72
  this.configuredHttpPort = options.httpPort;
60
73
  this.healthIntervalMs = options.healthIntervalMs ?? 30_000;
74
+ this.restartDesktopHook = options.restartDesktop;
61
75
  }
62
76
  async activate(context) {
63
77
  if (this.state === "ready")
@@ -68,6 +82,21 @@ class QwenworkPlugin {
68
82
  this.lastHeartbeat = undefined;
69
83
  this.context = context;
70
84
  try {
85
+ this.modelSessions.clear();
86
+ this.modelSessionsPath = typeof context.dataDir === "string" && context.dataDir ? (0, node_path_1.join)(context.dataDir, "model-provider-sessions.json") : undefined;
87
+ try {
88
+ if (!this.modelSessionsPath)
89
+ throw Object.assign(new Error("无持久化目录"), { code: "ENOENT" });
90
+ const saved = JSON.parse(await (0, promises_1.readFile)(this.modelSessionsPath, "utf8"));
91
+ if (!saved || typeof saved !== "object" || Array.isArray(saved) || Object.entries(saved).some(([key, value]) => !validModelSessionId(key) || !validModelSessionId(value)))
92
+ throw new Error("千问个人助理模型会话记录无效");
93
+ for (const [key, value] of Object.entries(saved))
94
+ this.modelSessions.set(key, value);
95
+ }
96
+ catch (error) {
97
+ if (error.code !== "ENOENT")
98
+ throw error;
99
+ }
71
100
  const config = context.config;
72
101
  const relayTarget = config.relayTarget;
73
102
  if (relayTarget !== undefined && relayTarget !== "dsh")
@@ -106,6 +135,7 @@ class QwenworkPlugin {
106
135
  if (!executable)
107
136
  throw new Error("未找到 QwenWorkCN.exe,请配置 executablePath");
108
137
  context.logger?.info(`正在启动千问办公,等待 CDP ${this.activeCdpPort}`, { executablePath: executable });
138
+ await this.restartDesktopProcess(executable);
109
139
  try {
110
140
  await (0, desktop_launch_1.launchAndWait)(executable, [`--remote-debugging-port=${this.activeCdpPort}`], () => client.connect(), typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000);
111
141
  }
@@ -140,8 +170,15 @@ class QwenworkPlugin {
140
170
  throw error;
141
171
  }
142
172
  }
143
- async deactivate(_reason) {
173
+ async deactivate(reason, options) {
174
+ const reloadDesktop = !options?.preserveDesktop && reason === "reload" && Boolean(this.client) && !this.relayTarget;
144
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
+ }
145
182
  this.state = "stopped";
146
183
  this.stopped = true;
147
184
  }
@@ -154,8 +191,6 @@ class QwenworkPlugin {
154
191
  return { status: this.state, ...(this.message ? { message: this.message } : {}), details };
155
192
  }
156
193
  async handle(request) {
157
- if (this.recovery)
158
- await this.recovery;
159
194
  if (this.state === "stopped" || (!this.relayTarget && !this.client))
160
195
  throw new Error("qwenwork 插件未就绪");
161
196
  return this.enqueueTask(() => this.completeRequest(request));
@@ -170,9 +205,8 @@ class QwenworkPlugin {
170
205
  throw new Error("千问办公当前忙,无法创建新会话");
171
206
  if (!this.client?.createSession)
172
207
  throw new Error("千问办公当前传输不支持新建会话");
173
- if (!this.client.connected())
174
- throw new Error("千问办公 CDP 未连接");
175
208
  return this.enqueueTask(async () => {
209
+ await this.ensureConnected();
176
210
  await this.client.createSession(request.name);
177
211
  return { text: "千问办公已创建新会话", sessionId };
178
212
  });
@@ -185,8 +219,7 @@ class QwenworkPlugin {
185
219
  if (!this.client)
186
220
  throw new Error("qwenwork 插件未就绪");
187
221
  try {
188
- if (!this.client.connected())
189
- throw new Error("千问 CDP 已断开,等待空闲后自动恢复");
222
+ await this.ensureConnected(signal);
190
223
  const reply = this.client.complete ? (await this.client.complete(prompt, timeoutMs, signal, undefined, undefined, onMessage)).text : await this.client.direct(prompt, timeoutMs, signal);
191
224
  if ((0, cdp_client_1.isQwenApiUnavailableReply)(reply))
192
225
  throw new agent_1.AgentOperationError("QWEN_REMOTE_ERROR", "等待千问办公回复", new Error("千问生成接口不可用;桌面未提供具体服务端错误码,CDP 仍连接,未重启或重新提交任务"));
@@ -196,7 +229,7 @@ class QwenworkPlugin {
196
229
  return reply;
197
230
  }
198
231
  catch (error) {
199
- if (!signal?.aborted) {
232
+ if (!this.stopped && !signal?.aborted) {
200
233
  this.message = error instanceof Error ? error.message : String(error);
201
234
  this.failures = this.client.connected() ? 0 : 2;
202
235
  this.state = "degraded";
@@ -204,21 +237,23 @@ class QwenworkPlugin {
204
237
  throw error;
205
238
  }
206
239
  }
207
- async completeRequest(request) {
240
+ async completeRequest(request, modelRequest = false) {
208
241
  if (this.relayTarget) {
209
242
  return this.callPlugin(this.relayTarget, request);
210
243
  }
211
244
  if (!this.client)
212
245
  throw new Error("qwenwork 插件未就绪");
213
246
  try {
214
- if (!this.client.connected())
215
- throw new Error("千问 CDP 已断开,等待空闲后自动恢复");
247
+ if (modelRequest && !this.client.connected())
248
+ throw new Error("千问 CDP 已断开,模型请求不会启动或重启桌面");
249
+ if (!modelRequest)
250
+ await this.ensureConnected(request.signal);
216
251
  if (request.attachments?.length && (!this.client.attachmentInput || !this.client.complete))
217
252
  throw new Error("千问当前客户端不支持附件输入");
218
253
  const timeoutMs = request.timeoutMs ?? asNumber(this.context?.config.timeoutMs, 240_000);
219
254
  const response = this.client.complete
220
- ? await this.client.complete(request.task, timeoutMs, undefined, undefined, request.attachments, request.onMessage)
221
- : { text: await this.complete(request.task, timeoutMs) };
255
+ ? await this.client.complete(request.task, timeoutMs, request.signal, undefined, request.attachments, request.onMessage, modelRequest)
256
+ : { text: await this.complete(request.task, timeoutMs, request.signal) };
222
257
  if ((0, cdp_client_1.isQwenApiUnavailableReply)(response.text))
223
258
  throw new agent_1.AgentOperationError("QWEN_REMOTE_ERROR", "等待千问办公回复", new Error("千问生成接口不可用;桌面未提供具体服务端错误码,CDP 仍连接,未重启或重新提交任务"));
224
259
  this.failures = 0;
@@ -227,12 +262,66 @@ class QwenworkPlugin {
227
262
  return response;
228
263
  }
229
264
  catch (error) {
230
- this.message = error instanceof Error ? error.message : String(error);
231
- this.failures = this.client.connected() ? 0 : 2;
232
- this.state = "degraded";
265
+ if (!this.stopped && !request.signal?.aborted && !modelRequest) {
266
+ this.message = error instanceof Error ? error.message : String(error);
267
+ this.failures = this.client.connected() ? 0 : 2;
268
+ this.state = "degraded";
269
+ }
233
270
  throw error;
234
271
  }
235
272
  }
273
+ async completeModelProvider(request) {
274
+ let stage = "千问办公模型排队(queue)";
275
+ try {
276
+ request.signal.throwIfAborted();
277
+ const run = this.enqueueTask(async () => {
278
+ request.signal.throwIfAborted();
279
+ stage = "连接千问办公模型(connect)";
280
+ const client = this.client;
281
+ if (this.state !== "ready" || !client?.connected() || !client.createSession || !client.activateSession || !client.complete)
282
+ throw new Error("千问办公未连接或不支持隔离模型会话,模型请求不会启动或重启桌面");
283
+ const previousSessionId = await client.currentSessionId?.();
284
+ try {
285
+ stage = "准备千问办公模型会话(submit)";
286
+ const desktopSessionId = this.modelSessions.get(request.sessionId);
287
+ if (desktopSessionId)
288
+ await client.activateSession(desktopSessionId);
289
+ else
290
+ await client.createSession(undefined, true, true);
291
+ request.signal.throwIfAborted();
292
+ stage = "等待千问办公模型生成(generate)";
293
+ const response = await this.completeRequest({ task: request.prompt, sessionId: request.sessionId, timeoutMs: request.timeoutMs, signal: request.signal }, true);
294
+ const actualSessionId = response.sessionId ?? await client.currentSessionId?.();
295
+ if (!validModelSessionId(actualSessionId))
296
+ throw new Error("千问未返回可恢复的个人助理模型会话标识");
297
+ if (actualSessionId !== desktopSessionId) {
298
+ this.modelSessions.set(request.sessionId, actualSessionId);
299
+ const path = this.modelSessionsPath;
300
+ if (path) {
301
+ await (0, promises_1.mkdir)(this.context.dataDir ?? ".", { recursive: true });
302
+ await (0, promises_1.writeFile)(`${path}.tmp`, JSON.stringify(Object.fromEntries(this.modelSessions)), { mode: 0o600 });
303
+ await (0, promises_1.rename)(`${path}.tmp`, path);
304
+ }
305
+ }
306
+ return { text: response.text };
307
+ }
308
+ finally {
309
+ if (client.connected()) {
310
+ if (previousSessionId)
311
+ await client.activateSession(previousSessionId);
312
+ else
313
+ await client.createSession(undefined, false, true);
314
+ }
315
+ }
316
+ });
317
+ return await (0, agent_1.abortable)(run, request.signal);
318
+ }
319
+ catch (error) {
320
+ if (error instanceof agent_1.AgentOperationError)
321
+ throw error;
322
+ throw new agent_1.AgentOperationError("QWEN_MODEL_FAILED", stage, error);
323
+ }
324
+ }
236
325
  registerRoutes(http) {
237
326
  if (!this.adapter)
238
327
  return;
@@ -262,6 +351,14 @@ class QwenworkPlugin {
262
351
  this.callPlugin = undefined;
263
352
  this.pluginStatus = undefined;
264
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
+ }
265
362
  startHealthWatch() {
266
363
  if (this.relayTarget || !this.client || this.healthTimer)
267
364
  return;
@@ -284,9 +381,26 @@ class QwenworkPlugin {
284
381
  });
285
382
  }
286
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() {
287
401
  if (this.recovery)
288
402
  return this.recovery;
289
- if (this.stopped || this.busy)
403
+ if (this.stopped)
290
404
  return;
291
405
  const client = this.client;
292
406
  if (!client || client.connected())
@@ -307,11 +421,12 @@ class QwenworkPlugin {
307
421
  this.restarts += 1;
308
422
  this.context?.logger?.info("千问 CDP 自动重连成功", { restarts: this.restarts });
309
423
  })().catch((error) => {
310
- if (this.stopped)
311
- return;
312
- this.state = "failed";
313
- this.message = error instanceof Error ? error.message : String(error);
314
- 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;
315
430
  }).finally(() => { this.recovery = undefined; });
316
431
  return this.recovery;
317
432
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-qwenwork",
3
- "version": "1.0.24",
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.24",
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,8 @@
9
9
  "capabilities": [
10
10
  "chat",
11
11
  "openai-compatible",
12
- "health"
12
+ "health",
13
+ "model-provider"
13
14
  ],
14
15
  "configSchema": "config.schema.json",
15
16
  "clientDownloadUrl": "https://qwenwork.cn/download"
@@ -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": "每日免费积分领取时间(本机分钟)" }