mu-agent-bridge 1.0.73 → 1.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/dist/src-ts/cli.js +67 -37
- package/dist/src-ts/control-server.js +7 -28
- package/dist/src-ts/core-updater.js +141 -33
- package/dist/src-ts/daemon-process.js +2 -1
- package/dist/src-ts/dsh-agent.js +7 -2
- package/dist/src-ts/http-agent.js +36 -0
- package/dist/src-ts/plugin-manager.js +185 -58
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +8 -6
- package/dist/src-ts/plugins/abweb/public/index.html +3 -3
- package/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/dist/src-ts/plugins/aun/index.js +89 -11
- package/dist/src-ts/plugins/aun/messages.js +1 -1
- package/dist/src-ts/plugins/aun/public/app.js +2 -1
- package/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/dist/src-ts/plugins/chatgpt/index.js +3 -2
- package/dist/src-ts/plugins/claude-cli/index.js +1 -1
- package/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
- package/dist/src-ts/plugins/doubao/index.js +119 -7
- package/dist/src-ts/plugins/doubao/native-task.js +134 -0
- package/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
- package/dist/src-ts/plugins/dsh/index.js +102 -23
- package/dist/src-ts/plugins/dsh/session-history.js +31 -3
- package/dist/src-ts/plugins/feishu/index.js +1 -1
- package/dist/src-ts/plugins/personal-assistant/index.js +29 -8
- package/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
- package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
- package/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
- package/dist/src-ts/plugins/qwenwork/index.js +52 -18
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
- package/dist/src-ts/plugins/workbuddy/index.js +24 -12
- package/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
- package/dist/src-ts/plugins/zcode/index.js +97 -0
- package/dist/src-ts/session-management.js +27 -8
- package/dist/src-ts/version-manager.js +6 -3
- package/dist/tests-ts/abweb-model-config.test.js +44 -41
- package/dist/tests-ts/aun-attachments.test.js +19 -17
- package/dist/tests-ts/aun-default-owner-page.test.js +2 -1
- package/dist/tests-ts/aun-messages.test.js +14 -0
- package/dist/tests-ts/aun-plugin.test.js +207 -19
- package/dist/tests-ts/bundled-package.test.js +1 -1
- package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
- package/dist/tests-ts/chatgpt-plugin.test.js +19 -0
- package/dist/tests-ts/control-server.test.js +20 -0
- package/dist/tests-ts/core-updater.test.js +215 -46
- package/dist/tests-ts/daemon-process.test.js +126 -1
- package/dist/tests-ts/desktop-launch.test.js +73 -0
- package/dist/tests-ts/doubao-native-task.test.js +237 -0
- package/dist/tests-ts/doubao-plugin.test.js +688 -19
- package/dist/tests-ts/dsh-agent.test.js +91 -19
- package/dist/tests-ts/dsh-cdp.test.js +148 -0
- package/dist/tests-ts/dsh-history.test.js +37 -0
- package/dist/tests-ts/feishu-plugin.test.js +1 -1
- package/dist/tests-ts/http-agent.test.js +18 -0
- package/dist/tests-ts/local-cli.test.js +29 -2
- package/dist/tests-ts/model-provider-cancellation.test.js +2 -0
- package/dist/tests-ts/native-session-management.test.js +8 -7
- package/dist/tests-ts/personal-assistant-coordination.test.js +16 -0
- package/dist/tests-ts/personal-assistant-extensions.test.js +7 -1
- package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
- package/dist/tests-ts/personal-assistant.test.js +277 -3
- package/dist/tests-ts/platform-integration.test.js +60 -1
- package/dist/tests-ts/plugin-manager.test.js +78 -6
- package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
- package/dist/tests-ts/publish-packages.test.js +1 -1
- package/dist/tests-ts/qwenwork-plugin.test.js +30 -0
- package/dist/tests-ts/qwenwork-reconnect.test.js +195 -0
- package/dist/tests-ts/qwenwork-sse.test.js +33 -0
- package/dist/tests-ts/session-access-defaults.test.js +112 -0
- package/dist/tests-ts/session-management.test.js +8 -8
- package/dist/tests-ts/version-manager.test.js +40 -0
- package/dist/tests-ts/workbuddy-cdp-recovery.test.js +311 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +356 -13
- package/dist/tests-ts/zcode-cdp.test.js +894 -0
- package/dist/tests-ts/zcode-plugin.test.js +396 -0
- package/package.json +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +8 -6
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +7 -5
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +3 -3
- package/plugins/abweb/package.json +1 -1
- package/plugins/abweb/plugin.json +1 -1
- package/plugins/aun/config.schema.json +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +89 -11
- package/plugins/aun/dist/src-ts/plugins/aun/messages.js +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +2 -1
- package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +3 -2
- package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +1 -1
- package/plugins/claude-cli/package.json +1 -1
- package/plugins/claude-cli/plugin.json +1 -1
- package/plugins/doubao/config.schema.json +2 -1
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +119 -7
- package/plugins/doubao/dist/src-ts/plugins/doubao/native-task.js +134 -0
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -1
- package/plugins/dsh/config.schema.json +5 -2
- package/plugins/dsh/dist/src-ts/dsh-agent.js +7 -2
- package/plugins/dsh/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/dsh/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +102 -23
- package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +31 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/plugin.json +1 -1
- package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +1 -1
- package/plugins/feishu/package.json +1 -1
- package/plugins/feishu/plugin.json +1 -1
- package/plugins/personal-assistant/config.schema.json +3 -2
- package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +29 -8
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
- package/plugins/personal-assistant/package.json +2 -2
- package/plugins/personal-assistant/plugin.json +1 -1
- package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +52 -18
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/config.schema.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +24 -12
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
- package/plugins/zcode/config.schema.json +13 -0
- package/plugins/zcode/dist/src-ts/agent.js +69 -0
- package/plugins/zcode/dist/src-ts/attachments.js +62 -0
- package/plugins/zcode/dist/src-ts/plugin.js +13 -0
- package/plugins/zcode/dist/src-ts/plugins/desktop-launch.js +238 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/cdp-client.js +896 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/index.js +567 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/plugins/zcode/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
- package/plugins/zcode/dist/src-ts/plugins/zcode/index.js +97 -0
- package/plugins/zcode/package.json +16 -0
- 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;
|
|
@@ -61,7 +85,7 @@ class WorkbuddyCdpClient {
|
|
|
61
85
|
})()`);
|
|
62
86
|
if (clicked !== true)
|
|
63
87
|
throw new Error(`WorkBuddy 未找到会话 ${id}`);
|
|
64
|
-
await this.waitFor(
|
|
88
|
+
await this.waitFor(conversationMatchesScript(id), "WorkBuddy 原会话切换未完成");
|
|
65
89
|
}
|
|
66
90
|
async readHistorySnapshot() {
|
|
67
91
|
return await this.evaluate(`(() => {
|
|
@@ -73,7 +97,7 @@ class WorkbuddyCdpClient {
|
|
|
73
97
|
const assistant = element.querySelector('[data-assistant-content]');
|
|
74
98
|
if (!id || (!user && !assistant)) return [];
|
|
75
99
|
const content = (user || assistant).cloneNode(true);
|
|
76
|
-
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());
|
|
77
101
|
const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]'));
|
|
78
102
|
const text = content.innerText?.trim() || content.textContent?.trim() || '';
|
|
79
103
|
if (!user && !finished) return [];
|
|
@@ -102,7 +126,13 @@ class WorkbuddyCdpClient {
|
|
|
102
126
|
if (this.connected())
|
|
103
127
|
return;
|
|
104
128
|
this.close();
|
|
105
|
-
|
|
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
|
+
}
|
|
106
136
|
if (!response.ok)
|
|
107
137
|
throw new Error(`WorkBuddy CDP /json HTTP ${response.status}`);
|
|
108
138
|
const targets = await response.json();
|
|
@@ -115,7 +145,13 @@ class WorkbuddyCdpClient {
|
|
|
115
145
|
socket.onclose = () => { if (this.socket === socket)
|
|
116
146
|
this.close(); };
|
|
117
147
|
try {
|
|
118
|
-
await new Promise((resolve, reject) => {
|
|
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
|
+
});
|
|
119
155
|
await this.evaluate(exports.chineseLocaleScript);
|
|
120
156
|
}
|
|
121
157
|
catch (error) {
|
|
@@ -123,9 +159,16 @@ class WorkbuddyCdpClient {
|
|
|
123
159
|
throw error;
|
|
124
160
|
}
|
|
125
161
|
}
|
|
126
|
-
close() {
|
|
127
|
-
|
|
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
|
+
}
|
|
128
170
|
async createTask() {
|
|
171
|
+
this.recentlyCreatedConversationId = undefined;
|
|
129
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"]')) }; })()`;
|
|
130
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 }; })()`);
|
|
131
174
|
if (before.loginRequired)
|
|
@@ -133,7 +176,11 @@ class WorkbuddyCdpClient {
|
|
|
133
176
|
try {
|
|
134
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 新建任务入口未找到或不可用");
|
|
135
178
|
// WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
|
|
136
|
-
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};
|
|
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
|
+
}
|
|
137
184
|
}
|
|
138
185
|
catch (error) {
|
|
139
186
|
const summarize = (state) => {
|
|
@@ -195,17 +242,23 @@ class WorkbuddyCdpClient {
|
|
|
195
242
|
return { clicked, skipped };
|
|
196
243
|
})()`);
|
|
197
244
|
}
|
|
198
|
-
async direct(text, timeoutMs =
|
|
245
|
+
async direct(text, timeoutMs = exports.WORKBUDDY_MAX_WAIT_MS, expert, newTask = false) {
|
|
199
246
|
return (await this.complete({ task: text, timeoutMs }, expert, newTask)).text;
|
|
200
247
|
}
|
|
201
|
-
async complete(request, expert, newTask = false) {
|
|
248
|
+
async complete(request, expert, newTask = false, expectedConversationId) {
|
|
202
249
|
let stage = "准备 WorkBuddy 桌面任务";
|
|
203
250
|
try {
|
|
204
251
|
(0, attachments_1.validateAttachments)(request.attachments);
|
|
205
252
|
const text = request.task;
|
|
206
|
-
const timeoutMs = request.timeoutMs ??
|
|
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 桌面任务与预期会话不符,未输入消息或确认选项");
|
|
207
260
|
if (!this.pendingInteraction && request.sessionId && !newTask && !expert) {
|
|
208
|
-
const interaction = await this.readInteraction();
|
|
261
|
+
const interaction = await this.readInteraction(expectedConversationId);
|
|
209
262
|
if (interaction)
|
|
210
263
|
this.pendingInteraction = { ...interaction, sessionId: request.sessionId };
|
|
211
264
|
}
|
|
@@ -213,18 +266,30 @@ class WorkbuddyCdpClient {
|
|
|
213
266
|
if (this.pendingInteraction.sessionId && this.pendingInteraction.sessionId !== request.sessionId)
|
|
214
267
|
throw new Error("WorkBuddy 正在等待另一个会话的选择");
|
|
215
268
|
stage = "继续 WorkBuddy 待确认的任务";
|
|
216
|
-
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
|
+
}
|
|
217
277
|
}
|
|
218
|
-
if (expert)
|
|
219
|
-
await this.selectExpert(expert);
|
|
220
278
|
else if (newTask)
|
|
221
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
|
+
}
|
|
222
287
|
stage = "填写 WorkBuddy 输入框";
|
|
223
288
|
await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "WorkBuddy 输入框未就绪");
|
|
224
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') })))`)));
|
|
225
290
|
const beforeIds = new Set(before.map((item) => item.id).filter((id) => Boolean(id)));
|
|
226
|
-
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; })()`))
|
|
227
|
-
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 输入框未找到或桌面会话已切换,未输入消息");
|
|
228
293
|
await this.cdp("Input.dispatchKeyEvent", { type: "keyDown", key: "Backspace", code: "Backspace" });
|
|
229
294
|
await this.cdp("Input.dispatchKeyEvent", { type: "keyUp", key: "Backspace", code: "Backspace" });
|
|
230
295
|
await this.cdp("Input.insertText", { text });
|
|
@@ -248,10 +313,52 @@ class WorkbuddyCdpClient {
|
|
|
248
313
|
}
|
|
249
314
|
stage = "向 WorkBuddy 提交任务(尚未确认发送)";
|
|
250
315
|
await this.waitFor(`(() => { const button = document.querySelector('button[aria-label="发送"]'); return Boolean(button && !button.disabled); })()`, "WorkBuddy 发送按钮未就绪");
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
}
|
|
253
360
|
stage = "等待 WorkBuddy 回复(任务已提交,桌面未被停止)";
|
|
254
|
-
const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage, request.signal);
|
|
361
|
+
const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage, request.signal, expectedConversationId ?? undefined, submissionError);
|
|
255
362
|
if (this.pendingInteraction && temporaryPaths.length)
|
|
256
363
|
this.pendingInteractionCleanup = temporaryPaths;
|
|
257
364
|
return response;
|
|
@@ -281,7 +388,7 @@ class WorkbuddyCdpClient {
|
|
|
281
388
|
throw new agent_1.AgentOperationError("WORKBUDDY_TASK_FAILED", stage, error);
|
|
282
389
|
}
|
|
283
390
|
}
|
|
284
|
-
async continueInteraction(answer, timeoutMs, onMessage) {
|
|
391
|
+
async continueInteraction(answer, timeoutMs, onMessage, signal, expectedConversationId) {
|
|
285
392
|
const interaction = this.pendingInteraction;
|
|
286
393
|
const index = interactionChoice(answer, interaction.options);
|
|
287
394
|
if (index === undefined)
|
|
@@ -289,12 +396,21 @@ class WorkbuddyCdpClient {
|
|
|
289
396
|
const temporaryPaths = this.pendingInteractionCleanup;
|
|
290
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'))) `)));
|
|
291
398
|
const beforeIds = new Set((Array.isArray(before) ? before : []).filter((id) => typeof id === "string"));
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
}
|
|
294
410
|
this.pendingInteraction = undefined;
|
|
295
411
|
let failure;
|
|
296
412
|
try {
|
|
297
|
-
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);
|
|
298
414
|
if (!this.pendingInteraction)
|
|
299
415
|
this.pendingInteractionCleanup = undefined;
|
|
300
416
|
return response;
|
|
@@ -317,62 +433,151 @@ class WorkbuddyCdpClient {
|
|
|
317
433
|
}
|
|
318
434
|
}
|
|
319
435
|
}
|
|
320
|
-
async waitForReply(beforeIds, timeoutMs, changedId, initialText, sessionId, onMessage, signal) {
|
|
321
|
-
|
|
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
|
+
};
|
|
322
501
|
const previousText = new Map();
|
|
323
502
|
const stablePolls = new Map();
|
|
324
503
|
const forwarded = new Set();
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const latest = candidates.at(-1) ?? current.at(-1);
|
|
345
|
-
this.pendingInteraction = { ...interaction, messageId: interaction.messageId ?? latest?.id ?? undefined, initialText: interaction.initialText ?? latest?.text ?? "", sessionId };
|
|
346
|
-
return { text: [...(onMessage ? [] : body), formatInteraction(this.pendingInteraction)].join("\n\n") };
|
|
347
|
-
}
|
|
348
|
-
const latest = candidates.at(-1);
|
|
349
|
-
const completed = candidates.filter((item) => item.finished || Boolean(item.stopped && (stablePolls.get(item.id) ?? 0) >= 2));
|
|
350
|
-
const reply = candidates.map((item) => item.text?.trim()).filter((text) => text && !forwarded.has(text)).join("\n\n");
|
|
351
|
-
const latestComplete = latest && (latest.finished || Boolean(latest.stopped && (stablePolls.get(latest.id) ?? 0) >= 2));
|
|
352
|
-
if (latestComplete && completed.length) {
|
|
353
|
-
const attachments = await this.readArtifacts(new Set(completed.map((item) => item.id)))
|
|
354
|
-
.catch(error => { throw new agent_1.AgentOperationError("WORKBUDDY_ARTIFACT_READ_FAILED", "读取 WorkBuddy 已完成任务的产物(尚未上传 AUN FS)", error); });
|
|
355
|
-
if (attachments.length)
|
|
356
|
-
return { text: reply ?? "", attachments };
|
|
357
|
-
if (reply || forwarded.size)
|
|
358
|
-
return { text: reply };
|
|
359
|
-
if (signal?.aborted && latest.finished)
|
|
360
|
-
throw new agent_1.AgentOperationError("WORKBUDDY_REQUEST_CANCELLED", "等待 WorkBuddy 取消后的任务结束", new Error("请求已取消,已确认本轮桌面生成结束,未返回正文或附件"));
|
|
361
|
-
}
|
|
362
|
-
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));
|
|
363
523
|
for (const item of candidates) {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
}
|
|
368
563
|
}
|
|
369
564
|
}
|
|
370
565
|
}
|
|
566
|
+
throw timeoutError();
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
clearInterval(progressTimer);
|
|
570
|
+
await progressTask;
|
|
571
|
+
this.replyDeadline = undefined;
|
|
572
|
+
if (progressError)
|
|
573
|
+
throw progressError;
|
|
371
574
|
}
|
|
372
|
-
throw new agent_1.AgentOperationError("WORKBUDDY_REPLY_TIMEOUT", "等待 WorkBuddy 回复", new Error(`已等待 ${Math.round(timeoutMs / 1000)} 秒,未确认任务完成;桌面任务未被停止,请勿重复提交`));
|
|
373
575
|
}
|
|
374
|
-
async readInteraction() {
|
|
375
|
-
const
|
|
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();
|
|
376
581
|
if (!raw || raw === "null" || raw === "undefined")
|
|
377
582
|
return undefined;
|
|
378
583
|
const value = JSON.parse(String(raw));
|
|
@@ -466,8 +671,10 @@ class WorkbuddyCdpClient {
|
|
|
466
671
|
if (failed) throw new Error('WorkBuddy 临时附件清理失败');
|
|
467
672
|
})()`);
|
|
468
673
|
}
|
|
469
|
-
async readArtifacts(messageIds) {
|
|
674
|
+
async readArtifacts(messageIds, expectedConversationId) {
|
|
675
|
+
const conversationMatches = expectedConversationId === undefined ? "true" : conversationMatchesScript(expectedConversationId);
|
|
470
676
|
const result = await this.evaluate(`(async () => {
|
|
677
|
+
if (!(${conversationMatches})) return null;
|
|
471
678
|
const ids = new Set(${JSON.stringify([...messageIds])});
|
|
472
679
|
const urls = [];
|
|
473
680
|
for (const message of document.querySelectorAll('.cr-agent[data-message-id]')) {
|
|
@@ -518,6 +725,8 @@ class WorkbuddyCdpClient {
|
|
|
518
725
|
}
|
|
519
726
|
return output;
|
|
520
727
|
})()`);
|
|
728
|
+
if (result === null)
|
|
729
|
+
throw conversationChangedError();
|
|
521
730
|
const attachments = [];
|
|
522
731
|
for (const item of result) {
|
|
523
732
|
let data;
|
|
@@ -556,9 +765,11 @@ class WorkbuddyCdpClient {
|
|
|
556
765
|
attachments.push({ kind: imageType ? "image" : "file", data, filename: (0, attachments_1.attachmentFilename)(item.filename), contentType });
|
|
557
766
|
(0, attachments_1.validateAttachments)(attachments);
|
|
558
767
|
}
|
|
768
|
+
if (expectedConversationId !== undefined && !await this.evaluate(conversationMatches))
|
|
769
|
+
throw conversationChangedError();
|
|
559
770
|
return attachments;
|
|
560
771
|
}
|
|
561
|
-
async selectExpert(name) {
|
|
772
|
+
async selectExpert(name, expectedConversationId) {
|
|
562
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; })()`))
|
|
563
774
|
throw new Error("WorkBuddy 专家入口未找到");
|
|
564
775
|
await this.waitFor(`Boolean(document.querySelector('input[placeholder="搜索专家"]'))`, "WorkBuddy 专家搜索框未就绪");
|
|
@@ -569,19 +780,50 @@ class WorkbuddyCdpClient {
|
|
|
569
780
|
throw new Error("WorkBuddy 专家搜索框未找到");
|
|
570
781
|
await this.cdp("Input.insertText", { text: query });
|
|
571
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 专家搜索超时");
|
|
572
|
-
const result = await this.
|
|
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 };
|
|
573
788
|
const cards = [...document.querySelectorAll('article[role="button"]')].filter(card => card.querySelector('button[data-track-id="expert_summon_click"]'));
|
|
574
789
|
const title = card => card.querySelector('[class*="_role_"]')?.textContent?.trim() || card.querySelector('img')?.alt || card.innerText.split('\\n').filter(Boolean)[1];
|
|
575
790
|
const exact = cards.filter(card => title(card) === ${JSON.stringify(name)});
|
|
576
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}`)});
|
|
577
792
|
const related = cards.filter(card => title(card)?.includes(${JSON.stringify(name)}) || ${JSON.stringify(name)}.includes(title(card) || ''));
|
|
578
793
|
const matches = exact.length ? exact : related.length === 1 ? related : alias.length === 1 ? alias : related;
|
|
794
|
+
if (!matches.length) return false;
|
|
579
795
|
if (matches.length !== 1) return { names: matches.map(title) };
|
|
580
796
|
const button = matches[0].querySelector('button[data-track-id="expert_summon_click"]');
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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;
|
|
585
827
|
if (result.names?.length)
|
|
586
828
|
throw new Error(`WorkBuddy 专家匹配不唯一,请指定完整名称:${result.names.join("、")}`);
|
|
587
829
|
}
|
|
@@ -590,8 +832,9 @@ class WorkbuddyCdpClient {
|
|
|
590
832
|
async waitFor(expression, message) {
|
|
591
833
|
const deadline = Date.now() + this.timeoutMs;
|
|
592
834
|
while (Date.now() < deadline) {
|
|
593
|
-
|
|
594
|
-
|
|
835
|
+
const value = await this.evaluate(expression);
|
|
836
|
+
if (value)
|
|
837
|
+
return value;
|
|
595
838
|
await sleep(200);
|
|
596
839
|
}
|
|
597
840
|
throw new Error(message);
|
|
@@ -604,10 +847,28 @@ class WorkbuddyCdpClient {
|
|
|
604
847
|
} if (message.id === undefined)
|
|
605
848
|
return; const pending = this.pending.get(message.id); if (!pending)
|
|
606
849
|
return; this.pending.delete(message.id); pending.resolve(message); }
|
|
607
|
-
|
|
608
|
-
|
|
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
|
+
}
|
|
609
869
|
async evaluate(expression) {
|
|
610
870
|
const slot = `__agentBridgeEvaluation_${(0, node_crypto_1.randomUUID)().replaceAll("-", "")}`;
|
|
871
|
+
let failure;
|
|
611
872
|
try {
|
|
612
873
|
const wrapped = `(() => { const state = {}; globalThis[${JSON.stringify(slot)}] = state; state.promise = Promise.resolve((${expression})); return state.promise; })()`;
|
|
613
874
|
const response = await this.cdp("Runtime.evaluate", { expression: wrapped, returnByValue: true, awaitPromise: true });
|
|
@@ -619,9 +880,14 @@ class WorkbuddyCdpClient {
|
|
|
619
880
|
}
|
|
620
881
|
return response.result?.result?.value;
|
|
621
882
|
}
|
|
883
|
+
catch (error) {
|
|
884
|
+
failure = error;
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
622
887
|
finally {
|
|
623
888
|
try {
|
|
624
|
-
|
|
889
|
+
if (!(failure instanceof WorkbuddyCdpTransportError))
|
|
890
|
+
await this.cdp("Runtime.evaluate", { expression: `delete globalThis[${JSON.stringify(slot)}]`, returnByValue: true });
|
|
625
891
|
}
|
|
626
892
|
catch { /* 连接中断时保留原始错误,页面重载会释放槽位。 */ }
|
|
627
893
|
}
|