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
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WorkbuddyCdpClient = exports.chineseLocaleScript = exports.assistantSelector = exports.editorSelector = void 0;
3
+ exports.WorkbuddyCdpClient = exports.chineseLocaleScript = exports.assistantSelector = exports.editorSelector = exports.WORKBUDDY_MAX_WAIT_MS = void 0;
4
+ exports.formatInteraction = formatInteraction;
5
+ exports.interactionChoice = interactionChoice;
4
6
  const agent_1 = require("../../agent");
5
7
  const node_crypto_1 = require("node:crypto");
6
8
  const promises_1 = require("node:fs/promises");
@@ -8,6 +10,9 @@ const node_url_1 = require("node:url");
8
10
  const attachments_1 = require("../../attachments");
9
11
  function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
10
12
  const interactionOptionSelector = '.conversation-input-area [role="dialog"] button,.pending-sandbox-panel button,button[class*="conversation-"][class*="__option"]';
13
+ exports.WORKBUDDY_MAX_WAIT_MS = 20 * 60 * 1000;
14
+ class WorkbuddyCdpTransportError extends Error {
15
+ }
11
16
  function interactionText(value) { return value.normalize("NFKC").replace(/[\uFEFF\u200B]/g, "").trim(); }
12
17
  function formatInteraction(value) { return [value.prompt, ...value.options.map((option, index) => `${index + 1}. ${option}`)].filter(Boolean).join("\n"); }
13
18
  function interactionChoice(value, options) {
@@ -20,6 +25,23 @@ function interactionChoice(value, options) {
20
25
  }
21
26
  exports.editorSelector = '[data-slate-editor][contenteditable="true"]';
22
27
  exports.assistantSelector = '.cr-agent[data-message-id] [data-assistant-content]';
28
+ const conversationMatchesFunction = `(id => {
29
+ const selected = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id') || null;
30
+ const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
31
+ if (selected !== id || !editor) return false;
32
+ const key = Object.keys(editor).find(key => key.startsWith('__reactFiber'));
33
+ for (let fiber = editor[key]; fiber; fiber = fiber.return) {
34
+ if (fiber.memoizedProps && 'viewConversationId' in fiber.memoizedProps) return (fiber.memoizedProps.viewConversationId || null) === id;
35
+ }
36
+ // 旧版界面没有输入组件会话属性,沿用侧栏校验。
37
+ return true;
38
+ })`;
39
+ function conversationMatchesScript(conversationId) {
40
+ return `${conversationMatchesFunction}(${JSON.stringify(conversationId)})`;
41
+ }
42
+ function conversationChangedError() {
43
+ return new agent_1.AgentOperationError("WORKBUDDY_CONVERSATION_CHANGED", "等待 WorkBuddy 回复", new Error("桌面会话已切换或新任务标识未确认,未读取其他任务结果;原桌面任务未被停止,请勿重复提交"));
44
+ }
23
45
  exports.chineseLocaleScript = `(() => {
24
46
  const key = 'CODEBUDDY_IDE_STORAGE_LANG';
25
47
  const legacyKey = 'workbuddy-language';
@@ -36,6 +58,8 @@ class WorkbuddyCdpClient {
36
58
  pending = new Map();
37
59
  pendingInteraction;
38
60
  pendingInteractionCleanup;
61
+ recentlyCreatedConversationId;
62
+ replyDeadline;
39
63
  port;
40
64
  timeoutMs;
41
65
  fetchImpl;
@@ -46,11 +70,12 @@ class WorkbuddyCdpClient {
46
70
  const value = await this.evaluate(`([...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_')))?.getAttribute('data-conversation-id')`);
47
71
  return typeof value === "string" && value.trim() ? value : undefined;
48
72
  }
49
- async activateConversation(conversationId) {
73
+ async activateConversation(conversationId, preserveDraft = false) {
50
74
  const id = conversationId.trim();
51
75
  if (!id)
52
76
  throw new Error("WorkBuddy 会话标识不能为空");
53
77
  const clicked = await this.evaluate(`(() => {
78
+ if (${preserveDraft}) { const editor = document.querySelector(${JSON.stringify(exports.editorSelector)})?.cloneNode(true); editor?.querySelectorAll('[data-slate-placeholder="true"],[data-slate-zero-width]').forEach(item => item.remove()); if (editor?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim() || document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]') || document.querySelector(${JSON.stringify(interactionOptionSelector)})) throw new Error('WorkBuddy 当前有用户草稿、待确认操作或正在生成回复,模型请求未切换会话'); }
54
79
  const button = [...document.querySelectorAll('[data-conversation-id]')]
55
80
  .filter(element => element.getAttribute('data-conversation-id') === ${JSON.stringify(id)})
56
81
  .map(item => item.matches('[role="button"],button') ? item : item.querySelector('[role="button"],button') || item.closest('[role="button"],button'))
@@ -60,7 +85,7 @@ class WorkbuddyCdpClient {
60
85
  })()`);
61
86
  if (clicked !== true)
62
87
  throw new Error(`WorkBuddy 未找到会话 ${id}`);
63
- await this.waitFor(`(() => [...document.querySelectorAll('[data-conversation-id]')].some(element => element.getAttribute('data-conversation-id') === ${JSON.stringify(id)} && String(element.className).includes('_selected_')))()`, "WorkBuddy 原会话切换未完成");
88
+ await this.waitFor(conversationMatchesScript(id), "WorkBuddy 原会话切换未完成");
64
89
  }
65
90
  async readHistorySnapshot() {
66
91
  return await this.evaluate(`(() => {
@@ -72,7 +97,7 @@ class WorkbuddyCdpClient {
72
97
  const assistant = element.querySelector('[data-assistant-content]');
73
98
  if (!id || (!user && !assistant)) return [];
74
99
  const content = (user || assistant).cloneNode(true);
75
- content.querySelectorAll('.cr-reasoning,[class*="reasoning"],[data-testid*="reasoning"],[class*="tool"],[data-testid*="tool"],[class*="artifact"],[data-testid*="artifact"],[class*="attachment"],[data-testid*="attachment"],[data-slate-placeholder="true"],[data-slate-zero-width]').forEach(node => node.remove());
100
+ content.querySelectorAll('.cr-reasoning,.cr-process-row,[class*="reasoning"],[data-testid*="reasoning"],[class*="tool"],[data-testid*="tool"],[class*="artifact"],[data-testid*="artifact"],[class*="attachment"],[data-testid*="attachment"],[data-slate-placeholder="true"],[data-slate-zero-width]').forEach(node => node.remove());
76
101
  const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]'));
77
102
  const text = content.innerText?.trim() || content.textContent?.trim() || '';
78
103
  if (!user && !finished) return [];
@@ -93,13 +118,21 @@ class WorkbuddyCdpClient {
93
118
  return await this.evaluate(`(() => ({
94
119
  conversationId: [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id') || undefined,
95
120
  waiting: Boolean(document.querySelector(${JSON.stringify(interactionOptionSelector)})),
121
+ generating: Boolean(document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]')),
122
+ draft: (() => { const text = document.querySelector(${JSON.stringify(exports.editorSelector)})?.cloneNode(true); text?.querySelectorAll('[data-slate-placeholder="true"],[data-slate-zero-width]').forEach(item => item.remove()); return Boolean(text?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim()); })(),
96
123
  }))()`);
97
124
  }
98
125
  async connect() {
99
126
  if (this.connected())
100
127
  return;
101
128
  this.close();
102
- const response = await this.fetchImpl(`http://127.0.0.1:${this.port}/json`);
129
+ let response;
130
+ try {
131
+ response = await this.fetchImpl(`http://127.0.0.1:${this.port}/json`, { signal: AbortSignal.timeout(this.commandTimeout()) });
132
+ }
133
+ catch (error) {
134
+ throw new Error(`WorkBuddy CDP 连接失败(端口 ${this.port});桌面升级后请在后台使用“重启桌面”恢复连接`, { cause: error });
135
+ }
103
136
  if (!response.ok)
104
137
  throw new Error(`WorkBuddy CDP /json HTTP ${response.status}`);
105
138
  const targets = await response.json();
@@ -112,7 +145,13 @@ class WorkbuddyCdpClient {
112
145
  socket.onclose = () => { if (this.socket === socket)
113
146
  this.close(); };
114
147
  try {
115
- await new Promise((resolve, reject) => { const socket = this.socket; socket.onopen = () => resolve(); socket.onerror = () => reject(new Error("WorkBuddy CDP WebSocket 连接失败")); });
148
+ await new Promise((resolve, reject) => {
149
+ const timer = setTimeout(() => reject(new WorkbuddyCdpTransportError(`WorkBuddy CDP WebSocket 连接超时(端口 ${this.port})`)), this.commandTimeout());
150
+ socket.onopen = () => { clearTimeout(timer); resolve(); };
151
+ socket.onerror = () => { clearTimeout(timer); reject(new WorkbuddyCdpTransportError(`WorkBuddy CDP WebSocket 连接失败(端口 ${this.port})`)); };
152
+ socket.onclose = () => { clearTimeout(timer); if (this.socket === socket)
153
+ this.close(); reject(new WorkbuddyCdpTransportError(`WorkBuddy CDP WebSocket 已关闭(端口 ${this.port})`)); };
154
+ });
116
155
  await this.evaluate(exports.chineseLocaleScript);
117
156
  }
118
157
  catch (error) {
@@ -120,9 +159,16 @@ class WorkbuddyCdpClient {
120
159
  throw error;
121
160
  }
122
161
  }
123
- close() { this.socket?.close(); this.socket = undefined; for (const pending of this.pending.values())
124
- pending.reject(new Error("WorkBuddy CDP 已关闭")); this.pending.clear(); }
162
+ close() {
163
+ const socket = this.socket;
164
+ this.socket = undefined;
165
+ for (const pending of this.pending.values())
166
+ pending.reject(new WorkbuddyCdpTransportError(`WorkBuddy CDP 已关闭(端口 ${this.port})`));
167
+ this.pending.clear();
168
+ socket?.close();
169
+ }
125
170
  async createTask() {
171
+ this.recentlyCreatedConversationId = undefined;
126
172
  const snapshot = `(() => { const selected = [...document.querySelectorAll('[data-conversation-id]')].find((item) => String(item.className).includes('_selected_')); const editor = document.querySelector(${JSON.stringify(exports.editorSelector)}); const text = editor?.cloneNode(true); text?.querySelectorAll('[data-slate-placeholder="true"],[data-slate-zero-width]').forEach((item) => item.remove()); return { url: location.href, conversationId: selected?.getAttribute('data-conversation-id') ?? null, selected: Boolean(selected), messages: document.querySelectorAll('.cr-agent[data-message-id]').length, editor: text?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim() ?? null, home: Boolean(editor?.querySelector('[data-slate-placeholder="true"]')) }; })()`;
127
173
  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
174
  if (before.loginRequired)
@@ -130,7 +176,11 @@ class WorkbuddyCdpClient {
130
176
  try {
131
177
  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
178
  // 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 新任务状态校验失败");
179
+ const created = 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}; const valid = blank && ((state.conversationId && state.conversationId !== ${JSON.stringify(before.conversationId)}) || state.url !== ${JSON.stringify(before.url)} || (state.home && !state.selected) || clearedToUnselected); return valid && { conversationId: state.conversationId }; })()`, "WorkBuddy 新任务状态校验失败");
180
+ if (created?.conversationId) {
181
+ await this.waitFor(conversationMatchesScript(created.conversationId), "WorkBuddy 新会话输入组件未就绪");
182
+ this.recentlyCreatedConversationId = created.conversationId;
183
+ }
134
184
  }
135
185
  catch (error) {
136
186
  const summarize = (state) => {
@@ -192,17 +242,23 @@ class WorkbuddyCdpClient {
192
242
  return { clicked, skipped };
193
243
  })()`);
194
244
  }
195
- async direct(text, timeoutMs = 3_600_000, expert, newTask = false) {
245
+ async direct(text, timeoutMs = exports.WORKBUDDY_MAX_WAIT_MS, expert, newTask = false) {
196
246
  return (await this.complete({ task: text, timeoutMs }, expert, newTask)).text;
197
247
  }
198
- async complete(request, expert, newTask = false) {
248
+ async complete(request, expert, newTask = false, expectedConversationId) {
199
249
  let stage = "准备 WorkBuddy 桌面任务";
200
250
  try {
201
251
  (0, attachments_1.validateAttachments)(request.attachments);
202
252
  const text = request.task;
203
- const timeoutMs = request.timeoutMs ?? 3_600_000;
253
+ const timeoutMs = Math.min(request.timeoutMs ?? exports.WORKBUDDY_MAX_WAIT_MS, exports.WORKBUDDY_MAX_WAIT_MS);
254
+ if (expectedConversationId === null && this.recentlyCreatedConversationId)
255
+ expectedConversationId = this.recentlyCreatedConversationId;
256
+ this.recentlyCreatedConversationId = undefined;
257
+ let conversationMatches = expectedConversationId === undefined ? "true" : conversationMatchesScript(expectedConversationId);
258
+ if (expectedConversationId !== undefined && !await this.evaluate(conversationMatches))
259
+ throw new Error("WorkBuddy 桌面任务与预期会话不符,未输入消息或确认选项");
204
260
  if (!this.pendingInteraction && request.sessionId && !newTask && !expert) {
205
- const interaction = await this.readInteraction();
261
+ const interaction = await this.readInteraction(expectedConversationId);
206
262
  if (interaction)
207
263
  this.pendingInteraction = { ...interaction, sessionId: request.sessionId };
208
264
  }
@@ -210,18 +266,30 @@ class WorkbuddyCdpClient {
210
266
  if (this.pendingInteraction.sessionId && this.pendingInteraction.sessionId !== request.sessionId)
211
267
  throw new Error("WorkBuddy 正在等待另一个会话的选择");
212
268
  stage = "继续 WorkBuddy 待确认的任务";
213
- return await this.continueInteraction(text, timeoutMs, request.onMessage);
269
+ return await this.continueInteraction(text, timeoutMs, request.onMessage, request.signal, expectedConversationId);
270
+ }
271
+ if (expert) {
272
+ const assignedId = await this.selectExpert(expert, expectedConversationId);
273
+ if (expectedConversationId !== undefined && assignedId) {
274
+ expectedConversationId = assignedId;
275
+ conversationMatches = conversationMatchesScript(assignedId);
276
+ }
214
277
  }
215
- if (expert)
216
- await this.selectExpert(expert);
217
278
  else if (newTask)
218
279
  await this.createTask();
280
+ if (expectedConversationId === null && this.recentlyCreatedConversationId) {
281
+ expectedConversationId = this.recentlyCreatedConversationId;
282
+ this.recentlyCreatedConversationId = undefined;
283
+ conversationMatches = conversationMatchesScript(expectedConversationId);
284
+ if (!await this.evaluate(conversationMatches))
285
+ throw new Error("WorkBuddy 新会话已切换,未输入消息");
286
+ }
219
287
  stage = "填写 WorkBuddy 输入框";
220
288
  await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "WorkBuddy 输入框未就绪");
221
289
  const before = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => ({ id: element.getAttribute('data-message-id') })))`)));
222
290
  const beforeIds = new Set(before.map((item) => item.id).filter((id) => Boolean(id)));
223
- if (!await this.evaluate(`(() => { const element = document.querySelector(${JSON.stringify(exports.editorSelector)}); if (!element) return false; element.focus(); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(element); selection?.removeAllRanges(); selection?.addRange(range); return true; })()`))
224
- throw new Error("WorkBuddy 输入框未找到");
291
+ if (!await this.evaluate(`(() => { if (!(${conversationMatches})) return false; const element = document.querySelector(${JSON.stringify(exports.editorSelector)}); if (!element) return false; element.focus(); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(element); selection?.removeAllRanges(); selection?.addRange(range); return true; })()`))
292
+ throw new Error("WorkBuddy 输入框未找到或桌面会话已切换,未输入消息");
225
293
  await this.cdp("Input.dispatchKeyEvent", { type: "keyDown", key: "Backspace", code: "Backspace" });
226
294
  await this.cdp("Input.dispatchKeyEvent", { type: "keyUp", key: "Backspace", code: "Backspace" });
227
295
  await this.cdp("Input.insertText", { text });
@@ -245,10 +313,52 @@ class WorkbuddyCdpClient {
245
313
  }
246
314
  stage = "向 WorkBuddy 提交任务(尚未确认发送)";
247
315
  await this.waitFor(`(() => { const button = document.querySelector('button[aria-label="发送"]'); return Boolean(button && !button.disabled); })()`, "WorkBuddy 发送按钮未就绪");
248
- if (!await this.evaluate(`(() => { const button = document.querySelector('button[aria-label="发送"]'); if (!button || button.disabled) return false; button.click(); return true; })()`))
249
- throw new Error("WorkBuddy 发送按钮不可用");
316
+ let submissionError;
317
+ if (expectedConversationId === null) {
318
+ const assignedId = await this.evaluate(`(async () => {
319
+ if (!(${conversationMatches})) return false;
320
+ const button = document.querySelector('button[aria-label="发送"]');
321
+ if (!button || button.disabled) return false;
322
+ const existingIds = new Set([...document.querySelectorAll('[data-conversation-id]')].map(item => item.getAttribute('data-conversation-id')));
323
+ button.click();
324
+ return await new Promise(resolve => {
325
+ let candidate; let timer; let timeout;
326
+ const finish = value => { clearInterval(timer); clearTimeout(timeout); document.removeEventListener('click', changed, true); document.removeEventListener('keydown', changed, true); resolve(value); };
327
+ const changed = () => finish(null);
328
+ document.addEventListener('click', changed, true);
329
+ document.addEventListener('keydown', changed, true);
330
+ const check = () => {
331
+ const id = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id');
332
+ if (!id) { if (candidate) finish(null); return; }
333
+ if (existingIds.has(id) || candidate && candidate !== id) { finish(null); return; }
334
+ candidate = id;
335
+ if (${conversationMatchesFunction}(id)) finish(id);
336
+ };
337
+ timer = setInterval(check, 50);
338
+ timeout = setTimeout(() => finish(null), ${Math.max(1, Math.min(timeoutMs, this.timeoutMs * 0.9))});
339
+ check();
340
+ });
341
+ })()`);
342
+ if (assignedId === false)
343
+ throw new Error("WorkBuddy 发送按钮不可用或桌面会话已切换,未发送消息");
344
+ if (typeof assignedId !== "string" || !assignedId)
345
+ throw conversationChangedError();
346
+ expectedConversationId = assignedId;
347
+ conversationMatches = conversationMatchesScript(assignedId);
348
+ }
349
+ else {
350
+ try {
351
+ if (!await this.evaluate(`(() => { if (!(${conversationMatches})) return false; const button = document.querySelector('button[aria-label="发送"]'); if (!button || button.disabled) return false; button.click(); return true; })()`))
352
+ throw new Error("WorkBuddy 发送按钮不可用或桌面会话已切换,未发送消息");
353
+ }
354
+ catch (error) {
355
+ if (!(error instanceof WorkbuddyCdpTransportError))
356
+ throw error;
357
+ submissionError = error;
358
+ }
359
+ }
250
360
  stage = "等待 WorkBuddy 回复(任务已提交,桌面未被停止)";
251
- const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage);
361
+ const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage, request.signal, expectedConversationId ?? undefined, submissionError);
252
362
  if (this.pendingInteraction && temporaryPaths.length)
253
363
  this.pendingInteractionCleanup = temporaryPaths;
254
364
  return response;
@@ -278,7 +388,7 @@ class WorkbuddyCdpClient {
278
388
  throw new agent_1.AgentOperationError("WORKBUDDY_TASK_FAILED", stage, error);
279
389
  }
280
390
  }
281
- async continueInteraction(answer, timeoutMs, onMessage) {
391
+ async continueInteraction(answer, timeoutMs, onMessage, signal, expectedConversationId) {
282
392
  const interaction = this.pendingInteraction;
283
393
  const index = interactionChoice(answer, interaction.options);
284
394
  if (index === undefined)
@@ -286,12 +396,21 @@ class WorkbuddyCdpClient {
286
396
  const temporaryPaths = this.pendingInteractionCleanup;
287
397
  const before = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => element.getAttribute('data-message-id'))) `)));
288
398
  const beforeIds = new Set((Array.isArray(before) ? before : []).filter((id) => typeof id === "string"));
289
- if (!await this.evaluate(`(() => { const options = [...document.querySelectorAll(${JSON.stringify(interactionOptionSelector)})]; const option = options[${index}]; if (!option || option.disabled || option.getAttribute('aria-disabled') === 'true') return false; option.click(); return true; })()`))
290
- throw new Error("WorkBuddy 选择项不可用");
399
+ const conversationMatches = expectedConversationId === undefined ? "true" : conversationMatchesScript(expectedConversationId);
400
+ let submissionError;
401
+ try {
402
+ if (!await this.evaluate(`(() => { if (!(${conversationMatches})) return false; const options = [...document.querySelectorAll(${JSON.stringify(interactionOptionSelector)})]; const option = options[${index}]; if (!option || option.disabled || option.getAttribute('aria-disabled') === 'true') return false; option.click(); return true; })()`))
403
+ throw new Error("WorkBuddy 选择项不可用或桌面会话已切换,未确认选项");
404
+ }
405
+ catch (error) {
406
+ if (!(error instanceof WorkbuddyCdpTransportError))
407
+ throw error;
408
+ submissionError = error;
409
+ }
291
410
  this.pendingInteraction = undefined;
292
411
  let failure;
293
412
  try {
294
- const response = await this.waitForReply(beforeIds, timeoutMs, interaction.messageId, interaction.initialText, interaction.sessionId, onMessage);
413
+ const response = await this.waitForReply(beforeIds, timeoutMs, interaction.messageId, interaction.initialText, interaction.sessionId, onMessage, signal, expectedConversationId ?? undefined, submissionError);
295
414
  if (!this.pendingInteraction)
296
415
  this.pendingInteractionCleanup = undefined;
297
416
  return response;
@@ -314,60 +433,151 @@ class WorkbuddyCdpClient {
314
433
  }
315
434
  }
316
435
  }
317
- async waitForReply(beforeIds, timeoutMs, changedId, initialText, sessionId, onMessage) {
318
- const deadline = Date.now() + timeoutMs;
436
+ async waitForReply(beforeIds, timeoutMs, changedId, initialText, sessionId, onMessage, signal, expectedConversationId, initialReadError) {
437
+ if (expectedConversationId === null)
438
+ throw conversationChangedError();
439
+ timeoutMs = Math.min(timeoutMs, exports.WORKBUDDY_MAX_WAIT_MS);
440
+ const started = Date.now();
441
+ const deadline = started + timeoutMs;
442
+ let originalConversationId = expectedConversationId;
443
+ let recoveryAttempts = 0;
444
+ let firstCdpError;
445
+ let lastCdpError;
446
+ let progressError;
447
+ let progressTask;
448
+ const timeoutError = () => new agent_1.AgentOperationError("WORKBUDDY_REPLY_TIMEOUT", "等待 WorkBuddy 回复", new Error(`已等待 ${Math.round((Date.now() - started) / 1000)} 秒,未读取到可确认完成的回复${firstCdpError ? `;首次 CDP 故障(端口 ${this.port}):${(0, agent_1.errorDetails)(firstCdpError)}` : ""}${lastCdpError && lastCdpError !== firstCdpError ? `;最后恢复失败:${(0, agent_1.errorDetails)(lastCdpError)}` : ""};桌面任务未被停止,请勿重复提交`));
449
+ const read = async (operation) => {
450
+ for (;;) {
451
+ if (progressError)
452
+ throw progressError;
453
+ try {
454
+ if (initialReadError) {
455
+ const error = initialReadError;
456
+ initialReadError = undefined;
457
+ throw error;
458
+ }
459
+ return await operation();
460
+ }
461
+ catch (error) {
462
+ if (!(error instanceof WorkbuddyCdpTransportError))
463
+ throw error;
464
+ firstCdpError ??= error;
465
+ lastCdpError = error;
466
+ this.replyDeadline = signal?.aborted ? started + exports.WORKBUDDY_MAX_WAIT_MS : deadline;
467
+ if (Date.now() >= this.replyDeadline)
468
+ throw timeoutError();
469
+ if (onMessage && !signal?.aborted)
470
+ await onMessage(`WorkBuddy CDP 读取中断(端口 ${this.port}):${(0, agent_1.errorDetails)(error)};正在恢复连接,原任务未重新提交。`);
471
+ let recovered = false;
472
+ let recoveryError = error;
473
+ while (recoveryAttempts < 3 && Date.now() < this.replyDeadline) {
474
+ recoveryAttempts++;
475
+ try {
476
+ this.close();
477
+ await this.connect();
478
+ const currentId = await this.conversationId();
479
+ if (!originalConversationId || currentId !== originalConversationId)
480
+ throw new Error(`重连后桌面会话与原任务不一致或无法确认(原会话 ${originalConversationId ?? "未知"},当前 ${currentId ?? "未知"});未切换任务`);
481
+ recovered = true;
482
+ break;
483
+ }
484
+ catch (failure) {
485
+ recoveryError = failure;
486
+ lastCdpError = failure;
487
+ if (recoveryAttempts < 3 && Date.now() < this.replyDeadline)
488
+ await sleep(Math.min(1000, this.replyDeadline - Date.now()));
489
+ }
490
+ }
491
+ if (!recovered) {
492
+ if (Date.now() >= this.replyDeadline)
493
+ throw timeoutError();
494
+ throw new agent_1.AgentOperationError("WORKBUDDY_CDP_RECOVERY_FAILED", `恢复 WorkBuddy CDP 读取(端口 ${this.port},共尝试 ${recoveryAttempts} 次)`, new Error(`首次故障:${(0, agent_1.errorDetails)(firstCdpError)};恢复失败:${(0, agent_1.errorDetails)(recoveryError)};桌面任务未被停止,未重新提交任务`, { cause: recoveryError }));
495
+ }
496
+ if (onMessage && !signal?.aborted)
497
+ await onMessage(`WorkBuddy CDP 已恢复(端口 ${this.port}),继续读取原任务;未重新提交任务。`);
498
+ }
499
+ }
500
+ };
319
501
  const previousText = new Map();
320
502
  const stablePolls = new Map();
321
503
  const forwarded = new Set();
322
- while (Date.now() < deadline) {
323
- await sleep(1_000);
324
- const current = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => { const content = element.querySelector('[data-assistant-content]'); let text = content?.innerText?.trim() || ''; for (const thought of content?.querySelectorAll?.('.cr-reasoning') || []) text = text.replace(thought.innerText || '', ''); text = text.trim(); const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]')); const stopped = Boolean(text) && !document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]'); return { id: element.getAttribute('data-message-id'), text, finished: finished, stopped: stopped }; }))`)));
325
- const interaction = await this.readInteraction();
326
- const candidates = current.filter((item) => item.id && (!beforeIds.has(item.id) || item.id === changedId && item.text !== initialText));
327
- for (const item of candidates) {
328
- const id = item.id;
329
- const text = item.text?.trim() ?? "";
330
- const count = text && text === previousText.get(id) ? (stablePolls.get(id) ?? 0) + 1 : text ? 1 : 0;
331
- previousText.set(id, text);
332
- stablePolls.set(id, count);
333
- }
334
- if (interaction) {
335
- const body = candidates.map((item) => item.text?.trim()).filter((text) => Boolean(text && !forwarded.has(text)));
336
- if (onMessage)
337
- for (const text of body) {
338
- await onMessage(text);
339
- forwarded.add(text);
340
- }
341
- const latest = candidates.at(-1) ?? current.at(-1);
342
- this.pendingInteraction = { ...interaction, messageId: interaction.messageId ?? latest?.id ?? undefined, initialText: interaction.initialText ?? latest?.text ?? "", sessionId };
343
- return { text: [...(onMessage ? [] : body), formatInteraction(this.pendingInteraction)].join("\n\n") };
344
- }
345
- const latest = candidates.at(-1);
346
- const completed = candidates.filter((item) => item.finished || Boolean(item.stopped && (stablePolls.get(item.id) ?? 0) >= 2));
347
- const reply = candidates.map((item) => item.text?.trim()).filter((text) => text && !forwarded.has(text)).join("\n\n");
348
- const latestComplete = latest && (latest.finished || Boolean(latest.stopped && (stablePolls.get(latest.id) ?? 0) >= 2));
349
- if (latestComplete && completed.length) {
350
- const attachments = await this.readArtifacts(new Set(completed.map((item) => item.id)))
351
- .catch(error => { throw new agent_1.AgentOperationError("WORKBUDDY_ARTIFACT_READ_FAILED", "读取 WorkBuddy 已完成任务的产物(尚未上传 AUN FS)", error); });
352
- if (attachments.length)
353
- return { text: reply ?? "", attachments };
354
- if (reply || forwarded.size)
355
- return { text: reply };
356
- }
357
- if (onMessage) {
504
+ const progressTimer = onMessage ? setInterval(() => {
505
+ if (signal?.aborted || progressTask || progressError || Date.now() >= deadline)
506
+ return;
507
+ progressTask = Promise.resolve().then(() => onMessage(`WorkBuddy 已等待 ${Math.floor((Date.now() - started) / 1000)} 秒,尚未读取到可确认完成的回复;继续等待原任务,本次上限 ${Math.round(timeoutMs / 1000)} 秒。`))
508
+ .catch(error => { progressError = new agent_1.AgentOperationError("WORKBUDDY_PROGRESS_SEND_FAILED", "向原通道发送 WorkBuddy 进度", error); })
509
+ .finally(() => { progressTask = undefined; });
510
+ }, 30_000) : undefined;
511
+ try {
512
+ while (Date.now() < (signal?.aborted ? started + exports.WORKBUDDY_MAX_WAIT_MS : deadline)) {
513
+ this.replyDeadline = signal?.aborted ? started + exports.WORKBUDDY_MAX_WAIT_MS : deadline;
514
+ await sleep(Math.min(1000, this.replyDeadline - Date.now()));
515
+ this.replyDeadline = signal?.aborted ? started + exports.WORKBUDDY_MAX_WAIT_MS : deadline;
516
+ const current = JSON.parse(String(await read(() => this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => { const conversationId = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id'); const content = element.querySelector('[data-assistant-content]'); const textProperty = content?.innerText?.trim() ? 'innerText' : 'textContent'; let text = content?.[textProperty]?.trim() || ''; for (const thought of content?.querySelectorAll?.('.cr-reasoning,.cr-process-row') || []) text = text.replace(thought[textProperty] || '', ''); text = text.trim(); const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]')); const stopped = Boolean(text) && !document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]'); return { id: element.getAttribute('data-message-id'), text, finished: finished, stopped: stopped, conversationId }; }))`))));
517
+ const currentId = current.find(item => item.conversationId)?.conversationId;
518
+ if (originalConversationId && currentId && currentId !== originalConversationId)
519
+ throw conversationChangedError();
520
+ originalConversationId ??= currentId;
521
+ const interaction = await read(() => this.readInteraction(originalConversationId));
522
+ const candidates = current.filter((item) => item.id && (!beforeIds.has(item.id) || item.id === changedId && item.text !== initialText));
358
523
  for (const item of candidates) {
359
- const text = item.text?.trim();
360
- if (text && (stablePolls.get(item.id) ?? 0) >= 2 && !forwarded.has(text)) {
361
- await onMessage(text);
362
- forwarded.add(text);
524
+ const id = item.id;
525
+ const text = item.text?.trim() ?? "";
526
+ const count = text && text === previousText.get(id) ? (stablePolls.get(id) ?? 0) + 1 : text ? 1 : 0;
527
+ previousText.set(id, text);
528
+ stablePolls.set(id, count);
529
+ }
530
+ if (interaction) {
531
+ const body = candidates.map((item) => item.text?.trim()).filter((text) => Boolean(text && !forwarded.has(text)));
532
+ if (onMessage)
533
+ for (const text of body) {
534
+ await onMessage(text);
535
+ forwarded.add(text);
536
+ }
537
+ const latest = candidates.at(-1) ?? current.at(-1);
538
+ this.pendingInteraction = { ...interaction, messageId: interaction.messageId ?? latest?.id ?? undefined, initialText: interaction.initialText ?? latest?.text ?? "", sessionId };
539
+ return { text: [...(onMessage ? [] : body), formatInteraction(this.pendingInteraction)].join("\n\n") };
540
+ }
541
+ const latest = candidates.at(-1);
542
+ const completed = candidates.filter((item) => item.finished || Boolean(item.stopped && (stablePolls.get(item.id) ?? 0) >= 2));
543
+ const reply = candidates.map((item) => item.text?.trim()).filter((text) => text && !forwarded.has(text)).join("\n\n");
544
+ const latestComplete = latest && (latest.finished || Boolean(latest.stopped && (stablePolls.get(latest.id) ?? 0) >= 2));
545
+ if (latestComplete && completed.length) {
546
+ const attachments = await read(() => this.readArtifacts(new Set(completed.map((item) => item.id)), originalConversationId))
547
+ .catch(error => { if (error instanceof agent_1.AgentOperationError)
548
+ throw error; throw new agent_1.AgentOperationError("WORKBUDDY_ARTIFACT_READ_FAILED", "读取 WorkBuddy 已完成任务的产物(尚未上传 AUN FS)", error); });
549
+ if (attachments.length)
550
+ return { text: reply ?? "", attachments };
551
+ if (reply || forwarded.size)
552
+ return { text: reply };
553
+ if (signal?.aborted && latest.finished)
554
+ throw new agent_1.AgentOperationError("WORKBUDDY_REQUEST_CANCELLED", "等待 WorkBuddy 取消后的任务结束", new Error("请求已取消,已确认本轮桌面生成结束,未返回正文或附件"));
555
+ }
556
+ if (onMessage) {
557
+ for (const item of candidates) {
558
+ const text = item.text?.trim();
559
+ if (text && (stablePolls.get(item.id) ?? 0) >= 2 && !forwarded.has(text)) {
560
+ await onMessage(text);
561
+ forwarded.add(text);
562
+ }
363
563
  }
364
564
  }
365
565
  }
566
+ throw timeoutError();
567
+ }
568
+ finally {
569
+ clearInterval(progressTimer);
570
+ await progressTask;
571
+ this.replyDeadline = undefined;
572
+ if (progressError)
573
+ throw progressError;
366
574
  }
367
- throw new agent_1.AgentOperationError("WORKBUDDY_REPLY_TIMEOUT", "等待 WorkBuddy 回复", new Error(`已等待 ${Math.round(timeoutMs / 1000)} 秒,未确认任务完成;桌面任务未被停止,请勿重复提交`));
368
575
  }
369
- async readInteraction() {
370
- const raw = await this.evaluate(`JSON.stringify((() => { const nodes = [...document.querySelectorAll(${JSON.stringify(interactionOptionSelector)})]; if (!nodes.length) return null; const root = nodes[0].closest('[role="dialog"],.pending-sandbox-panel,.conversation-high-credit-approval') || nodes[0].parentElement; const rawOptions = nodes.map(node => (node.innerText || node.textContent)?.trim() || '').filter(Boolean); let prompt = root?.innerText?.trim() || ''; for (const option of rawOptions) prompt = prompt.replace(option, ''); return { prompt: prompt.replace(/\\n{2,}/g, '\\n').trim(), options: rawOptions.map(option => option.replace(/^\\d+\\s*[.、::)]?\\s*/, '').trim()), messageId: nodes[0].closest('.cr-agent[data-message-id]')?.getAttribute('data-message-id') || undefined, initialText: nodes[0].closest('.cr-agent[data-message-id]')?.querySelector('[data-assistant-content]')?.innerText?.trim() || '' }; })())`);
576
+ async readInteraction(expectedConversationId) {
577
+ const conversationMatches = expectedConversationId === undefined ? "true" : conversationMatchesScript(expectedConversationId);
578
+ const raw = await this.evaluate(`(() => { if (!(${conversationMatches})) return false; return JSON.stringify((() => { const nodes = [...document.querySelectorAll(${JSON.stringify(interactionOptionSelector)})]; if (!nodes.length) return null; const root = nodes[0].closest('[role="dialog"],.pending-sandbox-panel,.conversation-high-credit-approval') || nodes[0].parentElement; const rawOptions = nodes.map(node => (node.innerText || node.textContent)?.trim() || '').filter(Boolean); let prompt = root?.innerText?.trim() || ''; for (const option of rawOptions) prompt = prompt.replace(option, ''); return { prompt: prompt.replace(/\\n{2,}/g, '\\n').trim(), options: rawOptions.map(option => option.replace(/^\\d+\\s*[.、::)]?\\s*/, '').trim()), messageId: nodes[0].closest('.cr-agent[data-message-id]')?.getAttribute('data-message-id') || undefined, initialText: nodes[0].closest('.cr-agent[data-message-id]')?.querySelector('[data-assistant-content]')?.innerText?.trim() || '' }; })()); })()`);
579
+ if (raw === false)
580
+ throw conversationChangedError();
371
581
  if (!raw || raw === "null" || raw === "undefined")
372
582
  return undefined;
373
583
  const value = JSON.parse(String(raw));
@@ -461,8 +671,10 @@ class WorkbuddyCdpClient {
461
671
  if (failed) throw new Error('WorkBuddy 临时附件清理失败');
462
672
  })()`);
463
673
  }
464
- async readArtifacts(messageIds) {
674
+ async readArtifacts(messageIds, expectedConversationId) {
675
+ const conversationMatches = expectedConversationId === undefined ? "true" : conversationMatchesScript(expectedConversationId);
465
676
  const result = await this.evaluate(`(async () => {
677
+ if (!(${conversationMatches})) return null;
466
678
  const ids = new Set(${JSON.stringify([...messageIds])});
467
679
  const urls = [];
468
680
  for (const message of document.querySelectorAll('.cr-agent[data-message-id]')) {
@@ -513,6 +725,8 @@ class WorkbuddyCdpClient {
513
725
  }
514
726
  return output;
515
727
  })()`);
728
+ if (result === null)
729
+ throw conversationChangedError();
516
730
  const attachments = [];
517
731
  for (const item of result) {
518
732
  let data;
@@ -551,9 +765,11 @@ class WorkbuddyCdpClient {
551
765
  attachments.push({ kind: imageType ? "image" : "file", data, filename: (0, attachments_1.attachmentFilename)(item.filename), contentType });
552
766
  (0, attachments_1.validateAttachments)(attachments);
553
767
  }
768
+ if (expectedConversationId !== undefined && !await this.evaluate(conversationMatches))
769
+ throw conversationChangedError();
554
770
  return attachments;
555
771
  }
556
- async selectExpert(name) {
772
+ async selectExpert(name, expectedConversationId) {
557
773
  if (!await this.evaluate(`(() => { const element = [...document.querySelectorAll('button,[role="button"],a')].find((item) => item.textContent?.trim() === '专家·技能·连接器'); if (!element) return false; element.click(); return true; })()`))
558
774
  throw new Error("WorkBuddy 专家入口未找到");
559
775
  await this.waitFor(`Boolean(document.querySelector('input[placeholder="搜索专家"]'))`, "WorkBuddy 专家搜索框未就绪");
@@ -564,19 +780,50 @@ class WorkbuddyCdpClient {
564
780
  throw new Error("WorkBuddy 专家搜索框未找到");
565
781
  await this.cdp("Input.insertText", { text: query });
566
782
  await this.waitFor(`(() => { const input = document.querySelector('input[placeholder="搜索专家"]'); const cards = document.querySelectorAll('article[role="button"] button[data-track-id="expert_summon_click"]'); return input?.value === ${JSON.stringify(query)} && cards.length > 0; })()`, "WorkBuddy 专家搜索超时");
567
- const result = await this.evaluate(`(() => {
783
+ const result = await this.waitFor(`(async () => {
784
+ const expected = ${JSON.stringify(expectedConversationId)};
785
+ const selected = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id') || null;
786
+ // 专家目录会卸载输入框;保留侧栏校验,召唤完成后再核对新输入组件。
787
+ if (expected !== undefined && (selected !== expected || document.querySelector(${JSON.stringify(exports.editorSelector)}) && !(${expectedConversationId === undefined ? "true" : conversationMatchesScript(expectedConversationId)}))) return { changed: true };
568
788
  const cards = [...document.querySelectorAll('article[role="button"]')].filter(card => card.querySelector('button[data-track-id="expert_summon_click"]'));
569
789
  const title = card => card.querySelector('[class*="_role_"]')?.textContent?.trim() || card.querySelector('img')?.alt || card.innerText.split('\\n').filter(Boolean)[1];
570
790
  const exact = cards.filter(card => title(card) === ${JSON.stringify(name)});
571
791
  const alias = cards.filter(card => card.querySelector('img')?.alt === ${JSON.stringify(name)} || card.querySelector('button[data-track-id="expert_summon_click"]')?.getAttribute('aria-label') === ${JSON.stringify(`召唤${name}`)});
572
792
  const related = cards.filter(card => title(card)?.includes(${JSON.stringify(name)}) || ${JSON.stringify(name)}.includes(title(card) || ''));
573
793
  const matches = exact.length ? exact : related.length === 1 ? related : alias.length === 1 ? alias : related;
794
+ if (!matches.length) return false;
574
795
  if (matches.length !== 1) return { names: matches.map(title) };
575
796
  const button = matches[0].querySelector('button[data-track-id="expert_summon_click"]');
576
- button.click(); return { selected: true };
577
- })()`);
578
- if (result.selected)
579
- return;
797
+ const existingIds = new Set([...document.querySelectorAll('[data-conversation-id]')].map(item => item.getAttribute('data-conversation-id')));
798
+ button.click();
799
+ const conversationId = await new Promise(resolve => {
800
+ let candidate; let timer; let timeout;
801
+ const finish = value => { clearInterval(timer); clearTimeout(timeout); document.removeEventListener('click', changed, true); document.removeEventListener('keydown', changed, true); resolve(value); };
802
+ const changed = () => finish(null);
803
+ // 专家会话可能异步创建,只接受本次召唤的新 ID;人工操作使绑定失效。
804
+ document.addEventListener('click', changed, true);
805
+ document.addEventListener('keydown', changed, true);
806
+ const check = () => {
807
+ const id = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id');
808
+ if (!id || existingIds.has(id)) { if (candidate) finish(null); return; }
809
+ if (candidate && candidate !== id) { finish(null); return; }
810
+ candidate = id;
811
+ if (${conversationMatchesFunction}(id)) finish(id);
812
+ };
813
+ timer = setInterval(check, 50);
814
+ timeout = setTimeout(() => finish(null), ${Math.max(1, this.timeoutMs * 0.9)});
815
+ check();
816
+ });
817
+ return { selected: true, conversationId };
818
+ })()`, "WorkBuddy 专家匹配超时").catch(error => {
819
+ if (!(error instanceof Error) || error.message !== "WorkBuddy 专家匹配超时")
820
+ throw error;
821
+ return {};
822
+ });
823
+ if (result.changed || result.selected && !result.conversationId)
824
+ throw new Error("WorkBuddy 专家新会话未就绪或桌面会话已切换,未输入消息");
825
+ if (result.selected && result.conversationId)
826
+ return result.conversationId;
580
827
  if (result.names?.length)
581
828
  throw new Error(`WorkBuddy 专家匹配不唯一,请指定完整名称:${result.names.join("、")}`);
582
829
  }
@@ -585,8 +832,9 @@ class WorkbuddyCdpClient {
585
832
  async waitFor(expression, message) {
586
833
  const deadline = Date.now() + this.timeoutMs;
587
834
  while (Date.now() < deadline) {
588
- if (await this.evaluate(expression))
589
- return;
835
+ const value = await this.evaluate(expression);
836
+ if (value)
837
+ return value;
590
838
  await sleep(200);
591
839
  }
592
840
  throw new Error(message);
@@ -599,10 +847,28 @@ class WorkbuddyCdpClient {
599
847
  } if (message.id === undefined)
600
848
  return; const pending = this.pending.get(message.id); if (!pending)
601
849
  return; this.pending.delete(message.id); pending.resolve(message); }
602
- async cdp(method, params = {}) { if (!this.socket)
603
- throw new Error("WorkBuddy CDP 尚未连接"); const id = ++this.sequence; const result = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject })); this.socket.send(JSON.stringify({ id, method, params })); return Promise.race([result, sleep(this.timeoutMs).then(() => { throw new Error(`CDP ${method} 超时`); })]); }
850
+ commandTimeout() { return Math.max(1, Math.min(this.timeoutMs, (this.replyDeadline ?? Infinity) - Date.now())); }
851
+ async cdp(method, params = {}) {
852
+ const socket = this.socket;
853
+ if (!socket || socket.readyState !== WebSocket.OPEN)
854
+ throw new WorkbuddyCdpTransportError(`WorkBuddy CDP 尚未连接(端口 ${this.port},方法 ${method})`);
855
+ const id = ++this.sequence;
856
+ return await new Promise((resolve, reject) => {
857
+ const cleanup = () => { clearTimeout(timer); this.pending.delete(id); };
858
+ const timer = setTimeout(() => { cleanup(); reject(new WorkbuddyCdpTransportError(`WorkBuddy CDP ${method} 超时(端口 ${this.port})`)); }, this.commandTimeout());
859
+ this.pending.set(id, { resolve: value => { cleanup(); resolve(value); }, reject: error => { cleanup(); reject(error); } });
860
+ try {
861
+ socket.send(JSON.stringify({ id, method, params }));
862
+ }
863
+ catch (error) {
864
+ cleanup();
865
+ reject(new WorkbuddyCdpTransportError(`WorkBuddy CDP ${method} 发送失败(端口 ${this.port}):${(0, agent_1.errorDetails)(error)}`, { cause: error }));
866
+ }
867
+ });
868
+ }
604
869
  async evaluate(expression) {
605
870
  const slot = `__agentBridgeEvaluation_${(0, node_crypto_1.randomUUID)().replaceAll("-", "")}`;
871
+ let failure;
606
872
  try {
607
873
  const wrapped = `(() => { const state = {}; globalThis[${JSON.stringify(slot)}] = state; state.promise = Promise.resolve((${expression})); return state.promise; })()`;
608
874
  const response = await this.cdp("Runtime.evaluate", { expression: wrapped, returnByValue: true, awaitPromise: true });
@@ -614,9 +880,14 @@ class WorkbuddyCdpClient {
614
880
  }
615
881
  return response.result?.result?.value;
616
882
  }
883
+ catch (error) {
884
+ failure = error;
885
+ throw error;
886
+ }
617
887
  finally {
618
888
  try {
619
- await this.cdp("Runtime.evaluate", { expression: `delete globalThis[${JSON.stringify(slot)}]`, returnByValue: true });
889
+ if (!(failure instanceof WorkbuddyCdpTransportError))
890
+ await this.cdp("Runtime.evaluate", { expression: `delete globalThis[${JSON.stringify(slot)}]`, returnByValue: true });
620
891
  }
621
892
  catch { /* 连接中断时保留原始错误,页面重载会释放槽位。 */ }
622
893
  }