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.
- package/README.md +17 -10
- package/dist/src-ts/agent.js +35 -0
- package/dist/src-ts/channel-sessions.js +3 -2
- package/dist/src-ts/cli.js +67 -37
- package/dist/src-ts/control-server.js +12 -30
- 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 +31 -13
- package/dist/src-ts/http-agent.js +36 -0
- package/dist/src-ts/plugin-catalog.js +11 -2
- package/dist/src-ts/plugin-manager.js +351 -50
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +73 -5
- package/dist/src-ts/plugins/abweb/public/index.html +15 -0
- package/dist/src-ts/plugins/abweb/public/style.css +4 -0
- package/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/dist/src-ts/plugins/aun/index.js +155 -21
- package/dist/src-ts/plugins/aun/messages.js +4 -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/cdp-client.js +26 -12
- package/dist/src-ts/plugins/chatgpt/index.js +80 -19
- package/dist/src-ts/plugins/claude-cli/index.js +19 -2
- package/dist/src-ts/plugins/codex-cli/index.js +19 -2
- package/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/dist/src-ts/plugins/doubao/cdp-client.js +234 -45
- package/dist/src-ts/plugins/doubao/index.js +152 -8
- 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 +172 -21
- 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/local-cli/agent.js +74 -9
- package/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
- package/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
- package/dist/src-ts/plugins/personal-assistant/index.js +263 -0
- package/dist/src-ts/plugins/personal-assistant/model-client.js +181 -0
- package/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
- package/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
- package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
- package/dist/src-ts/plugins/personal-assistant/tools.js +167 -0
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +90 -18
- package/dist/src-ts/plugins/qwenwork/index.js +139 -24
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +353 -82
- package/dist/src-ts/plugins/workbuddy/index.js +124 -41
- 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 +132 -14
- package/dist/src-ts/version-manager.js +6 -3
- package/dist/tests-ts/abweb-cli.test.js +10 -2
- package/dist/tests-ts/abweb-model-config.test.js +73 -0
- package/dist/tests-ts/agent.test.js +27 -0
- 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 +20 -0
- package/dist/tests-ts/aun-plugin.test.js +292 -19
- package/dist/tests-ts/bundled-package.test.js +12 -2
- package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
- package/dist/tests-ts/channel-sessions.test.js +27 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +118 -0
- package/dist/tests-ts/control-server.test.js +56 -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 +773 -12
- package/dist/tests-ts/dsh-agent.test.js +157 -8
- 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/installer-assets.test.js +1 -1
- package/dist/tests-ts/local-cli.test.js +146 -1
- package/dist/tests-ts/model-provider-cancellation.test.js +152 -0
- package/dist/tests-ts/native-session-management.test.js +8 -7
- package/dist/tests-ts/personal-assistant-coordination.test.js +92 -0
- package/dist/tests-ts/personal-assistant-extensions.test.js +35 -0
- package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
- package/dist/tests-ts/personal-assistant-owner-context.test.js +107 -0
- package/dist/tests-ts/personal-assistant-remote-ability.test.js +123 -0
- package/dist/tests-ts/personal-assistant.test.js +663 -0
- package/dist/tests-ts/platform-integration.test.js +183 -1
- package/dist/tests-ts/plugin-catalog.test.js +12 -2
- package/dist/tests-ts/plugin-manager.test.js +356 -4
- package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
- package/dist/tests-ts/publish-packages.test.js +42 -0
- package/dist/tests-ts/qwenwork-plugin.test.js +135 -1
- 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 +159 -4
- 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 +499 -4
- 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/agent.js +35 -0
- package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +73 -5
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +57 -5
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +15 -0
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +4 -0
- 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/agent.js +35 -0
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +155 -21
- package/plugins/aun/dist/src-ts/plugins/aun/messages.js +4 -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/agent.js +35 -0
- package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +80 -19
- package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +1 -1
- package/plugins/claude-cli/dist/src-ts/agent.js +35 -0
- package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +19 -2
- package/plugins/claude-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
- package/plugins/claude-cli/package.json +1 -1
- package/plugins/claude-cli/plugin.json +3 -2
- package/plugins/codex-cli/dist/src-ts/agent.js +35 -0
- package/plugins/codex-cli/dist/src-ts/plugins/codex-cli/index.js +19 -2
- package/plugins/codex-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
- package/plugins/codex-cli/package.json +1 -1
- package/plugins/codex-cli/plugin.json +3 -2
- package/plugins/doubao/config.schema.json +2 -1
- package/plugins/doubao/dist/src-ts/agent.js +35 -0
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +234 -45
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +152 -8
- 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 +3 -2
- package/plugins/dsh/config.schema.json +5 -2
- package/plugins/dsh/dist/src-ts/agent.js +35 -0
- package/plugins/dsh/dist/src-ts/dsh-agent.js +31 -13
- 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 +172 -21
- 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 +3 -2
- package/plugins/feishu/dist/src-ts/agent.js +35 -0
- 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 +23 -0
- package/plugins/personal-assistant/dist/src-ts/agent.js +69 -0
- package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
- package/plugins/personal-assistant/dist/src-ts/plugin.js +13 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +263 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +181 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
- 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 +167 -0
- package/plugins/personal-assistant/package.json +39 -0
- package/plugins/personal-assistant/plugin.json +15 -0
- package/plugins/qwenwork/dist/src-ts/agent.js +35 -0
- 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 +90 -18
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +139 -24
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +3 -2
- package/plugins/workbuddy/config.schema.json +1 -1
- package/plugins/workbuddy/dist/src-ts/agent.js +35 -0
- package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +353 -82
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +124 -41
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +3 -2
- 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;
|
|
@@ -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(
|
|
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
|
-
|
|
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) => {
|
|
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() {
|
|
124
|
-
|
|
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};
|
|
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 =
|
|
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 ??
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
589
|
-
|
|
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
|
-
|
|
603
|
-
|
|
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
|
-
|
|
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
|
}
|