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
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkbuddyCdpClient = exports.chineseLocaleScript = exports.assistantSelector = exports.editorSelector = exports.WORKBUDDY_MAX_WAIT_MS = void 0;
|
|
4
|
+
exports.formatInteraction = formatInteraction;
|
|
5
|
+
exports.interactionChoice = interactionChoice;
|
|
6
|
+
const agent_1 = require("../../agent");
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_url_1 = require("node:url");
|
|
10
|
+
const attachments_1 = require("../../attachments");
|
|
11
|
+
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
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
|
+
}
|
|
16
|
+
function interactionText(value) { return value.normalize("NFKC").replace(/[\uFEFF\u200B]/g, "").trim(); }
|
|
17
|
+
function formatInteraction(value) { return [value.prompt, ...value.options.map((option, index) => `${index + 1}. ${option}`)].filter(Boolean).join("\n"); }
|
|
18
|
+
function interactionChoice(value, options) {
|
|
19
|
+
const input = interactionText(value).replace(/^[第\s]*(\d+)[、.。::)]?\s*$/, "$1");
|
|
20
|
+
const number = Number(input);
|
|
21
|
+
if (Number.isInteger(number) && number >= 1 && number <= options.length && String(number) === input)
|
|
22
|
+
return number - 1;
|
|
23
|
+
const index = options.findIndex((option) => interactionText(option) === input);
|
|
24
|
+
return index >= 0 ? index : undefined;
|
|
25
|
+
}
|
|
26
|
+
exports.editorSelector = '[data-slate-editor][contenteditable="true"]';
|
|
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
|
+
}
|
|
45
|
+
exports.chineseLocaleScript = `(() => {
|
|
46
|
+
const key = 'CODEBUDDY_IDE_STORAGE_LANG';
|
|
47
|
+
const legacyKey = 'workbuddy-language';
|
|
48
|
+
const locale = 'zh-CN';
|
|
49
|
+
const changed = localStorage.getItem(key) !== locale || localStorage.getItem(legacyKey) !== locale;
|
|
50
|
+
localStorage.setItem(key, locale);
|
|
51
|
+
localStorage.setItem(legacyKey, locale);
|
|
52
|
+
return changed;
|
|
53
|
+
})()`;
|
|
54
|
+
class WorkbuddyCdpClient {
|
|
55
|
+
attachmentInput = true;
|
|
56
|
+
socket;
|
|
57
|
+
sequence = 0;
|
|
58
|
+
pending = new Map();
|
|
59
|
+
pendingInteraction;
|
|
60
|
+
pendingInteractionCleanup;
|
|
61
|
+
recentlyCreatedConversationId;
|
|
62
|
+
replyDeadline;
|
|
63
|
+
port;
|
|
64
|
+
timeoutMs;
|
|
65
|
+
fetchImpl;
|
|
66
|
+
constructor(options = {}) { this.port = options.port ?? Number(process.env.WORKBUDDY_CDP_PORT ?? "9229"); this.timeoutMs = options.timeoutMs ?? 30_000; this.fetchImpl = options.fetchImpl ?? fetch; }
|
|
67
|
+
connected() { return this.socket?.readyState === WebSocket.OPEN; }
|
|
68
|
+
pendingInteractionSessionId() { return this.pendingInteraction?.sessionId; }
|
|
69
|
+
async conversationId() {
|
|
70
|
+
const value = await this.evaluate(`([...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_')))?.getAttribute('data-conversation-id')`);
|
|
71
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
72
|
+
}
|
|
73
|
+
async activateConversation(conversationId, preserveDraft = false) {
|
|
74
|
+
const id = conversationId.trim();
|
|
75
|
+
if (!id)
|
|
76
|
+
throw new Error("WorkBuddy 会话标识不能为空");
|
|
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 当前有用户草稿、待确认操作或正在生成回复,模型请求未切换会话'); }
|
|
79
|
+
const button = [...document.querySelectorAll('[data-conversation-id]')]
|
|
80
|
+
.filter(element => element.getAttribute('data-conversation-id') === ${JSON.stringify(id)})
|
|
81
|
+
.map(item => item.matches('[role="button"],button') ? item : item.querySelector('[role="button"],button') || item.closest('[role="button"],button'))
|
|
82
|
+
.find(item => item && !item.disabled && item.getAttribute('aria-disabled') !== 'true');
|
|
83
|
+
if (!button || button.getAttribute('aria-disabled') === 'true' || button.disabled) return false;
|
|
84
|
+
button.click(); return true;
|
|
85
|
+
})()`);
|
|
86
|
+
if (clicked !== true)
|
|
87
|
+
throw new Error(`WorkBuddy 未找到会话 ${id}`);
|
|
88
|
+
await this.waitFor(conversationMatchesScript(id), "WorkBuddy 原会话切换未完成");
|
|
89
|
+
}
|
|
90
|
+
async readHistorySnapshot() {
|
|
91
|
+
return await this.evaluate(`(() => {
|
|
92
|
+
const selected = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'));
|
|
93
|
+
const conversationId = selected?.getAttribute('data-conversation-id') || undefined;
|
|
94
|
+
const messages = [...document.querySelectorAll('.cr-agent[data-message-id]')].flatMap(element => {
|
|
95
|
+
const id = element.getAttribute('data-message-id');
|
|
96
|
+
const user = element.querySelector('[data-user-content]');
|
|
97
|
+
const assistant = element.querySelector('[data-assistant-content]');
|
|
98
|
+
if (!id || (!user && !assistant)) return [];
|
|
99
|
+
const content = (user || assistant).cloneNode(true);
|
|
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());
|
|
101
|
+
const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]'));
|
|
102
|
+
const text = content.innerText?.trim() || content.textContent?.trim() || '';
|
|
103
|
+
if (!user && !finished) return [];
|
|
104
|
+
return [{ id, role: user ? 'user' : 'assistant', text, complete: true }];
|
|
105
|
+
});
|
|
106
|
+
const first = document.querySelector('.cr-agent[data-message-id]');
|
|
107
|
+
const scroll = first?.parentElement;
|
|
108
|
+
const missingReasons = [];
|
|
109
|
+
const waitingChoice = document.querySelector(${JSON.stringify(interactionOptionSelector)});
|
|
110
|
+
if (waitingChoice) return { conversationId, messages: [], missingReasons, unavailableReason: 'WorkBuddy 正在等待用户选择,暂不读取历史' };
|
|
111
|
+
if (scroll && scroll.scrollHeight > scroll.clientHeight && scroll.scrollTop > 0) missingReasons.push('消息容器当前不在顶部,较早消息可能未加载。');
|
|
112
|
+
else if (scroll && scroll.scrollHeight > scroll.clientHeight && scroll.scrollTop === 0) missingReasons.push('消息容器可能采用虚拟列表,无法证实未渲染消息范围。');
|
|
113
|
+
if (!messages.length) missingReasons.push('当前 DOM 没有带稳定 ID 的用户或助手消息。');
|
|
114
|
+
return { conversationId, messages, missingReasons };
|
|
115
|
+
})()`);
|
|
116
|
+
}
|
|
117
|
+
async historyAvailability() {
|
|
118
|
+
return await this.evaluate(`(() => ({
|
|
119
|
+
conversationId: [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id') || undefined,
|
|
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()); })(),
|
|
123
|
+
}))()`);
|
|
124
|
+
}
|
|
125
|
+
async connect() {
|
|
126
|
+
if (this.connected())
|
|
127
|
+
return;
|
|
128
|
+
this.close();
|
|
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
|
+
}
|
|
136
|
+
if (!response.ok)
|
|
137
|
+
throw new Error(`WorkBuddy CDP /json HTTP ${response.status}`);
|
|
138
|
+
const targets = await response.json();
|
|
139
|
+
const page = targets.find((target) => target.type === "page" && /workbuddy/i.test(`${target.title} ${target.url}`));
|
|
140
|
+
if (!page?.webSocketDebuggerUrl)
|
|
141
|
+
throw new Error(`未找到 WorkBuddy 页面 target(端口 ${this.port})`);
|
|
142
|
+
this.socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
143
|
+
this.socket.onmessage = (event) => this.onMessage(String(event.data));
|
|
144
|
+
const socket = this.socket;
|
|
145
|
+
socket.onclose = () => { if (this.socket === socket)
|
|
146
|
+
this.close(); };
|
|
147
|
+
try {
|
|
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
|
+
});
|
|
155
|
+
await this.evaluate(exports.chineseLocaleScript);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
this.close();
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
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
|
+
}
|
|
170
|
+
async createTask() {
|
|
171
|
+
this.recentlyCreatedConversationId = undefined;
|
|
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"]')) }; })()`;
|
|
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 }; })()`);
|
|
174
|
+
if (before.loginRequired)
|
|
175
|
+
throw new Error("WorkBuddy 尚未登录,请先在桌面端登录后重试");
|
|
176
|
+
try {
|
|
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 新建任务入口未找到或不可用");
|
|
178
|
+
// WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
|
|
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
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const summarize = (state) => {
|
|
187
|
+
let url = "[无效地址]";
|
|
188
|
+
try {
|
|
189
|
+
const parsed = new URL(state.url);
|
|
190
|
+
url = parsed.protocol === "file:" ? "file:///[本地路径已隐藏]" : `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
191
|
+
}
|
|
192
|
+
catch { /* 无效地址不输出原文,避免带入凭证。 */ }
|
|
193
|
+
return { url, selected: state.selected, messages: state.messages, editorPresent: state.editor !== null, editorEmpty: state.editor === "", home: state.home };
|
|
194
|
+
};
|
|
195
|
+
let after = "快照不可用";
|
|
196
|
+
try {
|
|
197
|
+
after = summarize(await this.evaluate(snapshot));
|
|
198
|
+
}
|
|
199
|
+
catch { /* 诊断失败不覆盖原始错误。 */ }
|
|
200
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)};页面状态:${JSON.stringify({ before: summarize(before), after })}`, { cause: error });
|
|
201
|
+
}
|
|
202
|
+
this.pendingInteraction = undefined;
|
|
203
|
+
const temporaryPaths = this.pendingInteractionCleanup;
|
|
204
|
+
this.pendingInteractionCleanup = undefined;
|
|
205
|
+
if (temporaryPaths?.length)
|
|
206
|
+
await this.cleanupTemporaryFiles(temporaryPaths);
|
|
207
|
+
}
|
|
208
|
+
async claimDailyPoints() {
|
|
209
|
+
return await this.evaluate(`(() => {
|
|
210
|
+
const buttons = [...document.querySelectorAll('button,[role="button"]')];
|
|
211
|
+
const clicked = [];
|
|
212
|
+
let skipped = 0;
|
|
213
|
+
for (const button of buttons) {
|
|
214
|
+
const label = [...new Set([button.innerText, button.textContent, button.getAttribute('aria-label'), button.getAttribute('title')].filter(Boolean))].join(' ').normalize('NFKC').replace(/\\s+/g, ' ').trim();
|
|
215
|
+
if (!label || !button.getClientRects().length || button.disabled || button.getAttribute('aria-disabled') === 'true') continue;
|
|
216
|
+
let parent = button.parentElement;
|
|
217
|
+
let context = '';
|
|
218
|
+
for (let depth = 0; parent && depth < 5; depth++, parent = parent.parentElement) {
|
|
219
|
+
const text = (parent.innerText || parent.textContent || '').normalize('NFKC').replace(/\\s+/g, ' ').trim();
|
|
220
|
+
if (/(积分|签到|奖励|任务)/.test(text)) { context = text; break; }
|
|
221
|
+
}
|
|
222
|
+
if (!context) continue;
|
|
223
|
+
if (/购买|加购|充值|升级|开通|兑换|抽奖/.test(label + context)) { skipped++; continue; }
|
|
224
|
+
if (!/(签到|领取(?:积分|奖励)?|收下奖励|去完成)/.test(label)) continue;
|
|
225
|
+
button.click(); clicked.push(label);
|
|
226
|
+
}
|
|
227
|
+
const carousel = document.querySelector('.wb-slot--avatar-top-combo')?.shadowRoot;
|
|
228
|
+
const dots = [...(carousel?.querySelectorAll('.dot') ?? [])];
|
|
229
|
+
const active = dots.find((dot) => dot.getAttribute('data-on') === '1');
|
|
230
|
+
for (const dot of dots) {
|
|
231
|
+
dot.click();
|
|
232
|
+
const card = carousel?.querySelector('#rw')?.innerText ?? '';
|
|
233
|
+
const claim = carousel?.querySelector('#go');
|
|
234
|
+
let props;
|
|
235
|
+
try { props = JSON.parse(claim?.getAttribute('data-track-props') ?? '{}'); } catch { props = {}; }
|
|
236
|
+
if (/Buddy加油站/.test(card) && /每日可领/.test(card) && claim?.innerText?.trim() === '立即领取' && props.index === 0 && props.type === 'checkin' && !claim.disabled && claim.getAttribute('aria-disabled') !== 'true') {
|
|
237
|
+
claim.click(); clicked.push('Buddy加油站:立即领取');
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!clicked.includes('Buddy加油站:立即领取')) active?.click();
|
|
242
|
+
return { clicked, skipped };
|
|
243
|
+
})()`);
|
|
244
|
+
}
|
|
245
|
+
async direct(text, timeoutMs = exports.WORKBUDDY_MAX_WAIT_MS, expert, newTask = false) {
|
|
246
|
+
return (await this.complete({ task: text, timeoutMs }, expert, newTask)).text;
|
|
247
|
+
}
|
|
248
|
+
async complete(request, expert, newTask = false, expectedConversationId) {
|
|
249
|
+
let stage = "准备 WorkBuddy 桌面任务";
|
|
250
|
+
try {
|
|
251
|
+
(0, attachments_1.validateAttachments)(request.attachments);
|
|
252
|
+
const text = request.task;
|
|
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 桌面任务与预期会话不符,未输入消息或确认选项");
|
|
260
|
+
if (!this.pendingInteraction && request.sessionId && !newTask && !expert) {
|
|
261
|
+
const interaction = await this.readInteraction(expectedConversationId);
|
|
262
|
+
if (interaction)
|
|
263
|
+
this.pendingInteraction = { ...interaction, sessionId: request.sessionId };
|
|
264
|
+
}
|
|
265
|
+
if (this.pendingInteraction) {
|
|
266
|
+
if (this.pendingInteraction.sessionId && this.pendingInteraction.sessionId !== request.sessionId)
|
|
267
|
+
throw new Error("WorkBuddy 正在等待另一个会话的选择");
|
|
268
|
+
stage = "继续 WorkBuddy 待确认的任务";
|
|
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
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if (newTask)
|
|
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
|
+
}
|
|
287
|
+
stage = "填写 WorkBuddy 输入框";
|
|
288
|
+
await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "WorkBuddy 输入框未就绪");
|
|
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') })))`)));
|
|
290
|
+
const beforeIds = new Set(before.map((item) => item.id).filter((id) => Boolean(id)));
|
|
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 输入框未找到或桌面会话已切换,未输入消息");
|
|
293
|
+
await this.cdp("Input.dispatchKeyEvent", { type: "keyDown", key: "Backspace", code: "Backspace" });
|
|
294
|
+
await this.cdp("Input.dispatchKeyEvent", { type: "keyUp", key: "Backspace", code: "Backspace" });
|
|
295
|
+
await this.cdp("Input.insertText", { text });
|
|
296
|
+
if (!await this.evaluate(`document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText.trim() === ${JSON.stringify(text.trim())}`))
|
|
297
|
+
throw new Error("WorkBuddy 输入内容校验失败,未发送消息");
|
|
298
|
+
const temporaryPaths = [];
|
|
299
|
+
let failure;
|
|
300
|
+
try {
|
|
301
|
+
if (request.attachments?.length) {
|
|
302
|
+
stage = "向 WorkBuddy 提交入站附件";
|
|
303
|
+
await this.injectAttachments(request.attachments, temporaryPaths);
|
|
304
|
+
if (!await this.evaluate(`(() => {
|
|
305
|
+
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)}); if (!editor) return false;
|
|
306
|
+
const clone = editor.cloneNode(true);
|
|
307
|
+
for (const tag of clone.querySelectorAll('[class*="_fileTag"]')) tag.remove();
|
|
308
|
+
clone.style.position = 'absolute'; clone.style.left = '-100000px'; document.body.appendChild(clone);
|
|
309
|
+
try { return clone.innerText.replace(/[\\uFEFF\\u200B]/g, '').trim() === ${JSON.stringify(text.trim())}; }
|
|
310
|
+
finally { clone.remove(); }
|
|
311
|
+
})()`))
|
|
312
|
+
throw new Error("WorkBuddy 附件上传后的正文校验失败,未发送消息");
|
|
313
|
+
}
|
|
314
|
+
stage = "向 WorkBuddy 提交任务(尚未确认发送)";
|
|
315
|
+
await this.waitFor(`(() => { const button = document.querySelector('button[aria-label="发送"]'); return Boolean(button && !button.disabled); })()`, "WorkBuddy 发送按钮未就绪");
|
|
316
|
+
let submissionError;
|
|
317
|
+
if (expectedConversationId === null) {
|
|
318
|
+
const assignedId = await this.evaluate(`(async () => {
|
|
319
|
+
if (!(${conversationMatches})) return false;
|
|
320
|
+
const button = document.querySelector('button[aria-label="发送"]');
|
|
321
|
+
if (!button || button.disabled) return false;
|
|
322
|
+
const existingIds = new Set([...document.querySelectorAll('[data-conversation-id]')].map(item => item.getAttribute('data-conversation-id')));
|
|
323
|
+
button.click();
|
|
324
|
+
return await new Promise(resolve => {
|
|
325
|
+
let candidate; let timer; let timeout;
|
|
326
|
+
const finish = value => { clearInterval(timer); clearTimeout(timeout); document.removeEventListener('click', changed, true); document.removeEventListener('keydown', changed, true); resolve(value); };
|
|
327
|
+
const changed = () => finish(null);
|
|
328
|
+
document.addEventListener('click', changed, true);
|
|
329
|
+
document.addEventListener('keydown', changed, true);
|
|
330
|
+
const check = () => {
|
|
331
|
+
const id = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id');
|
|
332
|
+
if (!id) { if (candidate) finish(null); return; }
|
|
333
|
+
if (existingIds.has(id) || candidate && candidate !== id) { finish(null); return; }
|
|
334
|
+
candidate = id;
|
|
335
|
+
if (${conversationMatchesFunction}(id)) finish(id);
|
|
336
|
+
};
|
|
337
|
+
timer = setInterval(check, 50);
|
|
338
|
+
timeout = setTimeout(() => finish(null), ${Math.max(1, Math.min(timeoutMs, this.timeoutMs * 0.9))});
|
|
339
|
+
check();
|
|
340
|
+
});
|
|
341
|
+
})()`);
|
|
342
|
+
if (assignedId === false)
|
|
343
|
+
throw new Error("WorkBuddy 发送按钮不可用或桌面会话已切换,未发送消息");
|
|
344
|
+
if (typeof assignedId !== "string" || !assignedId)
|
|
345
|
+
throw conversationChangedError();
|
|
346
|
+
expectedConversationId = assignedId;
|
|
347
|
+
conversationMatches = conversationMatchesScript(assignedId);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
try {
|
|
351
|
+
if (!await this.evaluate(`(() => { if (!(${conversationMatches})) return false; const button = document.querySelector('button[aria-label="发送"]'); if (!button || button.disabled) return false; button.click(); return true; })()`))
|
|
352
|
+
throw new Error("WorkBuddy 发送按钮不可用或桌面会话已切换,未发送消息");
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (!(error instanceof WorkbuddyCdpTransportError))
|
|
356
|
+
throw error;
|
|
357
|
+
submissionError = error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
stage = "等待 WorkBuddy 回复(任务已提交,桌面未被停止)";
|
|
361
|
+
const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage, request.signal, expectedConversationId ?? undefined, submissionError);
|
|
362
|
+
if (this.pendingInteraction && temporaryPaths.length)
|
|
363
|
+
this.pendingInteractionCleanup = temporaryPaths;
|
|
364
|
+
return response;
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
failure = error;
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
if (temporaryPaths.length && this.pendingInteractionCleanup !== temporaryPaths) {
|
|
372
|
+
try {
|
|
373
|
+
await this.cleanupTemporaryFiles(temporaryPaths);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
if (failure !== undefined)
|
|
377
|
+
throw new AggregateError([failure, error], `${failure instanceof Error ? failure.message : String(failure)};临时附件清理失败`, { cause: failure });
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (error instanceof agent_1.AgentOperationError)
|
|
385
|
+
throw error;
|
|
386
|
+
if (error instanceof AggregateError)
|
|
387
|
+
throw Object.assign(error, { code: "WORKBUDDY_TASK_FAILED", stage });
|
|
388
|
+
throw new agent_1.AgentOperationError("WORKBUDDY_TASK_FAILED", stage, error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async continueInteraction(answer, timeoutMs, onMessage, signal, expectedConversationId) {
|
|
392
|
+
const interaction = this.pendingInteraction;
|
|
393
|
+
const index = interactionChoice(answer, interaction.options);
|
|
394
|
+
if (index === undefined)
|
|
395
|
+
return { text: formatInteraction(interaction) };
|
|
396
|
+
const temporaryPaths = this.pendingInteractionCleanup;
|
|
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'))) `)));
|
|
398
|
+
const beforeIds = new Set((Array.isArray(before) ? before : []).filter((id) => typeof id === "string"));
|
|
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
|
+
}
|
|
410
|
+
this.pendingInteraction = undefined;
|
|
411
|
+
let failure;
|
|
412
|
+
try {
|
|
413
|
+
const response = await this.waitForReply(beforeIds, timeoutMs, interaction.messageId, interaction.initialText, interaction.sessionId, onMessage, signal, expectedConversationId ?? undefined, submissionError);
|
|
414
|
+
if (!this.pendingInteraction)
|
|
415
|
+
this.pendingInteractionCleanup = undefined;
|
|
416
|
+
return response;
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
failure = error;
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
if (temporaryPaths && !this.pendingInteraction) {
|
|
424
|
+
this.pendingInteractionCleanup = undefined;
|
|
425
|
+
try {
|
|
426
|
+
await this.cleanupTemporaryFiles(temporaryPaths);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (failure !== undefined)
|
|
430
|
+
throw new AggregateError([failure, error], `${failure instanceof Error ? failure.message : String(failure)};临时附件清理失败`, { cause: failure });
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
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
|
+
};
|
|
501
|
+
const previousText = new Map();
|
|
502
|
+
const stablePolls = new Map();
|
|
503
|
+
const forwarded = new Set();
|
|
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));
|
|
523
|
+
for (const item of candidates) {
|
|
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
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
throw timeoutError();
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
clearInterval(progressTimer);
|
|
570
|
+
await progressTask;
|
|
571
|
+
this.replyDeadline = undefined;
|
|
572
|
+
if (progressError)
|
|
573
|
+
throw progressError;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
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();
|
|
581
|
+
if (!raw || raw === "null" || raw === "undefined")
|
|
582
|
+
return undefined;
|
|
583
|
+
const value = JSON.parse(String(raw));
|
|
584
|
+
if (!Array.isArray(value.options) || value.options.length === 0 || value.options.some((option) => typeof option !== "string" || !option.trim()))
|
|
585
|
+
return undefined;
|
|
586
|
+
return { prompt: typeof value.prompt === "string" ? value.prompt : "请选择:", options: value.options, ...(typeof value.messageId === "string" ? { messageId: value.messageId } : {}), ...(typeof value.initialText === "string" ? { initialText: value.initialText } : {}) };
|
|
587
|
+
}
|
|
588
|
+
async injectAttachments(items, temporaryPaths = []) {
|
|
589
|
+
const before = await this.evaluate(`JSON.stringify([...document.querySelector(${JSON.stringify(exports.editorSelector)}).querySelectorAll('[class*="_fileTag"]')].map(element => element.textContent?.trim()).filter(Boolean))`);
|
|
590
|
+
if (JSON.parse(String(before)).length)
|
|
591
|
+
throw new Error("WorkBuddy 输入框已有附件,未发送消息");
|
|
592
|
+
const files = items.map((item) => ({ kind: item.kind, filename: (0, attachments_1.attachmentFilename)(item.filename), contentType: item.contentType, data: Buffer.from(item.data).toString("base64") }));
|
|
593
|
+
const uploadSlot = `__agentBridgeUpload_${(0, node_crypto_1.randomUUID)().replaceAll("-", "")}`;
|
|
594
|
+
const expression = `(async () => {
|
|
595
|
+
const paths = []; globalThis[${JSON.stringify(uploadSlot)}] = { paths };
|
|
596
|
+
try {
|
|
597
|
+
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
|
|
598
|
+
if (!editor || typeof DataTransfer === 'undefined' || typeof File === 'undefined') throw new Error('WorkBuddy 当前页面不支持附件输入');
|
|
599
|
+
const fiberKey = Object.keys(editor).find(key => key.startsWith('__reactFiber'));
|
|
600
|
+
let adapter;
|
|
601
|
+
for (let fiber = editor[fiberKey]; fiber && !adapter; fiber = fiber.return) {
|
|
602
|
+
const candidate = fiber.memoizedProps?.value?.adapter;
|
|
603
|
+
if (candidate && typeof candidate.downloadFileToLocal === 'function' && typeof candidate.cleanupDownloadedTemporaryFile === 'function') adapter = candidate;
|
|
604
|
+
}
|
|
605
|
+
const transfer = new DataTransfer();
|
|
606
|
+
for (const item of ${JSON.stringify(files)}) {
|
|
607
|
+
const binary = atob(item.data); const bytes = new Uint8Array(binary.length);
|
|
608
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
609
|
+
const file = new File([bytes], item.filename, { type: item.contentType });
|
|
610
|
+
if (item.kind === 'file') {
|
|
611
|
+
if (!adapter) throw new Error('WorkBuddy 当前页面不支持附件文件物化');
|
|
612
|
+
const written = await adapter.downloadFileToLocal({ url: '', fileName: item.filename, contentBase64: item.data });
|
|
613
|
+
if (!written?.success || typeof written.filePath !== 'string' || !written.filePath) throw new Error('WorkBuddy 附件文件物化失败');
|
|
614
|
+
paths.push(written.filePath);
|
|
615
|
+
Object.defineProperty(file, 'path', { value: written.filePath });
|
|
616
|
+
}
|
|
617
|
+
transfer.items.add(file);
|
|
618
|
+
}
|
|
619
|
+
editor.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
620
|
+
return { paths };
|
|
621
|
+
} catch (error) { return { paths, error: error instanceof Error ? error.message : String(error) }; }
|
|
622
|
+
})()`;
|
|
623
|
+
let result;
|
|
624
|
+
let pathsRecovered = false;
|
|
625
|
+
try {
|
|
626
|
+
result = await this.evaluate(expression);
|
|
627
|
+
if (!result || !Array.isArray(result.paths))
|
|
628
|
+
throw new Error("WorkBuddy 附件投递结果无效,未重复投递");
|
|
629
|
+
temporaryPaths.push(...result.paths);
|
|
630
|
+
pathsRecovered = true;
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
try {
|
|
634
|
+
temporaryPaths.push(...await this.evaluate(`globalThis[${JSON.stringify(uploadSlot)}]?.paths ?? []`));
|
|
635
|
+
pathsRecovered = true;
|
|
636
|
+
}
|
|
637
|
+
catch { /* 连接中断时保留页面路径记录与原始错误。 */ }
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
finally {
|
|
641
|
+
if (pathsRecovered) {
|
|
642
|
+
try {
|
|
643
|
+
await this.evaluate(`delete globalThis[${JSON.stringify(uploadSlot)}]`);
|
|
644
|
+
}
|
|
645
|
+
catch { /* 页面重载会释放已恢复的路径记录。 */ }
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (result.error)
|
|
649
|
+
throw new Error(result.error);
|
|
650
|
+
const names = files.map((item) => item.filename);
|
|
651
|
+
await this.waitFor(`(() => {
|
|
652
|
+
const names = ${JSON.stringify(names)}; const current = [...document.querySelector(${JSON.stringify(exports.editorSelector)}).querySelectorAll('[class*="_fileTag"]')].map(element => element.textContent?.trim()).filter(Boolean);
|
|
653
|
+
return current.length === names.length && names.every(name => current.filter(value => value === name).length === names.filter(value => value === name).length);
|
|
654
|
+
})()`, "WorkBuddy 附件上传未确认");
|
|
655
|
+
}
|
|
656
|
+
async cleanupTemporaryFiles(paths) {
|
|
657
|
+
await this.evaluate(`(async () => {
|
|
658
|
+
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
|
|
659
|
+
const key = editor && Object.keys(editor).find(key => key.startsWith('__reactFiber'));
|
|
660
|
+
let adapter;
|
|
661
|
+
for (let fiber = editor?.[key]; fiber && !adapter; fiber = fiber.return) {
|
|
662
|
+
const candidate = fiber.memoizedProps?.value?.adapter;
|
|
663
|
+
if (typeof candidate?.cleanupDownloadedTemporaryFile === 'function') adapter = candidate;
|
|
664
|
+
}
|
|
665
|
+
if (!adapter) throw new Error('WorkBuddy 临时附件清理接口不可用');
|
|
666
|
+
let failed = false;
|
|
667
|
+
for (const path of ${JSON.stringify(paths)}) {
|
|
668
|
+
try { const result = await adapter.cleanupDownloadedTemporaryFile(path); if (!result?.success) failed = true; }
|
|
669
|
+
catch { failed = true; }
|
|
670
|
+
}
|
|
671
|
+
if (failed) throw new Error('WorkBuddy 临时附件清理失败');
|
|
672
|
+
})()`);
|
|
673
|
+
}
|
|
674
|
+
async readArtifacts(messageIds, expectedConversationId) {
|
|
675
|
+
const conversationMatches = expectedConversationId === undefined ? "true" : conversationMatchesScript(expectedConversationId);
|
|
676
|
+
const result = await this.evaluate(`(async () => {
|
|
677
|
+
if (!(${conversationMatches})) return null;
|
|
678
|
+
const ids = new Set(${JSON.stringify([...messageIds])});
|
|
679
|
+
const urls = [];
|
|
680
|
+
for (const message of document.querySelectorAll('.cr-agent[data-message-id]')) {
|
|
681
|
+
if (!ids.has(message.getAttribute('data-message-id'))) continue;
|
|
682
|
+
const panel = message.querySelector('.artifact-slot-panel');
|
|
683
|
+
const fiberKey = panel && Object.keys(panel).find(key => key.startsWith('__reactFiber'));
|
|
684
|
+
let artifactProps; let requestId;
|
|
685
|
+
// shortcut: WorkBuddy 未公开产物接口,DOM 改版时应替换这段只读的面板属性适配。
|
|
686
|
+
for (let fiber = panel?.[fiberKey]; fiber; fiber = fiber.return) {
|
|
687
|
+
const props = fiber.memoizedProps;
|
|
688
|
+
if (Array.isArray(props?.allArtifacts) && !artifactProps) artifactProps = props;
|
|
689
|
+
if (typeof props?.artifactRequestId === 'string') { requestId = props.artifactRequestId; break; }
|
|
690
|
+
if (!requestId && typeof props?.requestId === 'string') requestId = props.requestId;
|
|
691
|
+
}
|
|
692
|
+
if (artifactProps && requestId && artifactProps.taskId) {
|
|
693
|
+
const current = artifactProps.allArtifacts.filter(item => item.conversationId === artifactProps.taskId && [item.requestId, item.lastModifiedRequestId, item._meta?.createdByRequestId].includes(requestId));
|
|
694
|
+
// 正式交付文件优先于中间脚本和工作区预览,后者可能已被任务清理。
|
|
695
|
+
const delivered = current.filter(item => item.sourceTool === 'PresentFiles' || item._meta?.sourceTool === 'PresentFiles' || item._meta?.['codebuddy.ai/toolName'] === 'present_files');
|
|
696
|
+
for (const item of delivered.length ? delivered : current) urls.push(item.uri || item.url || '');
|
|
697
|
+
} else {
|
|
698
|
+
for (const card of message.querySelectorAll('.artifact-slot-panel__card[data-dir][data-share]')) urls.push(card.getAttribute('data-share') || '');
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const output = []; const seen = new Set(); let total = 0;
|
|
702
|
+
for (const value of urls) {
|
|
703
|
+
if (typeof value !== 'string' || !value.startsWith('file:///')) continue;
|
|
704
|
+
const url = new URL(value);
|
|
705
|
+
if (url.hostname || url.pathname.startsWith('//') || url.search || url.hash || seen.has(url.href)) continue;
|
|
706
|
+
if (/\\.(?:mp4|webm|mov|m4v|avi|mkv|wmv|flv|mpeg|mpg|3gp)$/i.test(decodeURIComponent(url.pathname))) continue;
|
|
707
|
+
seen.add(url.href); if (seen.size > ${attachments_1.MAX_ATTACHMENTS}) throw new Error('单条消息最多包含 8 个附件');
|
|
708
|
+
let response;
|
|
709
|
+
try { response = await fetch(url.href); }
|
|
710
|
+
catch { output.push({ fileUrl: url.href }); continue; }
|
|
711
|
+
if (!response.ok || !response.body) throw new Error('WorkBuddy 产物读取失败');
|
|
712
|
+
if (response.headers.get('content-type')?.trim().toLowerCase().startsWith('video/')) { await response.body.cancel(); seen.delete(url.href); continue; }
|
|
713
|
+
if (Number(response.headers.get('content-length')) > ${attachments_1.MAX_ATTACHMENT_BYTES}) { await response.body.cancel(); throw new Error('单个附件不能超过 32 MiB'); }
|
|
714
|
+
const reader = response.body.getReader(); let size = 0; let binary = '';
|
|
715
|
+
try {
|
|
716
|
+
while (true) {
|
|
717
|
+
const { done, value: chunk } = await reader.read(); if (done) break;
|
|
718
|
+
size += chunk.byteLength; total += chunk.byteLength;
|
|
719
|
+
if (size > ${attachments_1.MAX_ATTACHMENT_BYTES}) throw new Error('单个附件不能超过 32 MiB');
|
|
720
|
+
if (total > ${attachments_1.MAX_TOTAL_ATTACHMENT_BYTES}) throw new Error('单条消息附件总大小不能超过 64 MiB');
|
|
721
|
+
for (let index = 0; index < chunk.length; index += 0x8000) binary += String.fromCharCode(...chunk.subarray(index, index + 0x8000));
|
|
722
|
+
}
|
|
723
|
+
} finally { await reader.cancel(); }
|
|
724
|
+
output.push({ filename: decodeURIComponent(url.pathname.split('/').pop() || ''), data: btoa(binary), contentType: response.headers.get('content-type')?.split(';')[0] || 'application/octet-stream' });
|
|
725
|
+
}
|
|
726
|
+
return output;
|
|
727
|
+
})()`);
|
|
728
|
+
if (result === null)
|
|
729
|
+
throw conversationChangedError();
|
|
730
|
+
const attachments = [];
|
|
731
|
+
for (const item of result) {
|
|
732
|
+
let data;
|
|
733
|
+
if (item.fileUrl) {
|
|
734
|
+
const url = new URL(item.fileUrl);
|
|
735
|
+
if (url.protocol !== "file:" || url.hostname || url.search || url.hash || /^(?:\/{2})|%5c|%2f/i.test(url.pathname))
|
|
736
|
+
throw new Error("WorkBuddy 产物文件 URL 无效");
|
|
737
|
+
const path = (0, node_url_1.fileURLToPath)(url);
|
|
738
|
+
const file = await (0, promises_1.open)(path, "r").catch(error => { throw new Error(`WorkBuddy 产物读取失败:${path}(${error.code ?? String(error)})`); });
|
|
739
|
+
try {
|
|
740
|
+
const stat = await file.stat();
|
|
741
|
+
if (!stat.isFile())
|
|
742
|
+
throw new Error("WorkBuddy 产物不是普通文件");
|
|
743
|
+
if (stat.size > attachments_1.MAX_ATTACHMENT_BYTES)
|
|
744
|
+
throw new Error("单个附件不能超过 32 MiB");
|
|
745
|
+
const chunks = [];
|
|
746
|
+
let size = 0;
|
|
747
|
+
for await (const chunk of file.createReadStream({ autoClose: false })) {
|
|
748
|
+
size += chunk.length;
|
|
749
|
+
if (size > attachments_1.MAX_ATTACHMENT_BYTES)
|
|
750
|
+
throw new Error("单个附件不能超过 32 MiB");
|
|
751
|
+
chunks.push(chunk);
|
|
752
|
+
}
|
|
753
|
+
data = new Uint8Array(Buffer.concat(chunks));
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
await file.close();
|
|
757
|
+
}
|
|
758
|
+
item.filename = (0, attachments_1.attachmentFilename)(path);
|
|
759
|
+
item.contentType = /\.pdf$/i.test(path) ? "application/pdf" : "application/octet-stream";
|
|
760
|
+
}
|
|
761
|
+
else
|
|
762
|
+
data = new Uint8Array(Buffer.from(item.data, "base64"));
|
|
763
|
+
const imageType = (0, attachments_1.detectImageContentType)(data);
|
|
764
|
+
const contentType = imageType ?? (item.contentType.startsWith("image/") ? "application/octet-stream" : item.contentType);
|
|
765
|
+
attachments.push({ kind: imageType ? "image" : "file", data, filename: (0, attachments_1.attachmentFilename)(item.filename), contentType });
|
|
766
|
+
(0, attachments_1.validateAttachments)(attachments);
|
|
767
|
+
}
|
|
768
|
+
if (expectedConversationId !== undefined && !await this.evaluate(conversationMatches))
|
|
769
|
+
throw conversationChangedError();
|
|
770
|
+
return attachments;
|
|
771
|
+
}
|
|
772
|
+
async selectExpert(name, expectedConversationId) {
|
|
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; })()`))
|
|
774
|
+
throw new Error("WorkBuddy 专家入口未找到");
|
|
775
|
+
await this.waitFor(`Boolean(document.querySelector('input[placeholder="搜索专家"]'))`, "WorkBuddy 专家搜索框未就绪");
|
|
776
|
+
if (!await this.evaluate(`(() => { const element = document.querySelector('input[placeholder="搜索专家"]'); if (!element) return false; element.focus(); return true; })()`))
|
|
777
|
+
throw new Error("WorkBuddy 专家搜索框未找到");
|
|
778
|
+
for (const query of [...new Set([name, name.replace(/专家(?:团)?$/, "")])]) {
|
|
779
|
+
if (!await this.evaluate(`(() => { const element = document.querySelector('input[placeholder="搜索专家"]'); if (!element) return false; const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; setter?.call(element, ''); element.dispatchEvent(new Event('input', { bubbles: true })); element.focus(); return true; })()`))
|
|
780
|
+
throw new Error("WorkBuddy 专家搜索框未找到");
|
|
781
|
+
await this.cdp("Input.insertText", { text: query });
|
|
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 专家搜索超时");
|
|
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 };
|
|
788
|
+
const cards = [...document.querySelectorAll('article[role="button"]')].filter(card => card.querySelector('button[data-track-id="expert_summon_click"]'));
|
|
789
|
+
const title = card => card.querySelector('[class*="_role_"]')?.textContent?.trim() || card.querySelector('img')?.alt || card.innerText.split('\\n').filter(Boolean)[1];
|
|
790
|
+
const exact = cards.filter(card => title(card) === ${JSON.stringify(name)});
|
|
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}`)});
|
|
792
|
+
const related = cards.filter(card => title(card)?.includes(${JSON.stringify(name)}) || ${JSON.stringify(name)}.includes(title(card) || ''));
|
|
793
|
+
const matches = exact.length ? exact : related.length === 1 ? related : alias.length === 1 ? alias : related;
|
|
794
|
+
if (!matches.length) return false;
|
|
795
|
+
if (matches.length !== 1) return { names: matches.map(title) };
|
|
796
|
+
const button = matches[0].querySelector('button[data-track-id="expert_summon_click"]');
|
|
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;
|
|
827
|
+
if (result.names?.length)
|
|
828
|
+
throw new Error(`WorkBuddy 专家匹配不唯一,请指定完整名称:${result.names.join("、")}`);
|
|
829
|
+
}
|
|
830
|
+
throw new Error(`未找到 WorkBuddy 专家:${name}`);
|
|
831
|
+
}
|
|
832
|
+
async waitFor(expression, message) {
|
|
833
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
834
|
+
while (Date.now() < deadline) {
|
|
835
|
+
const value = await this.evaluate(expression);
|
|
836
|
+
if (value)
|
|
837
|
+
return value;
|
|
838
|
+
await sleep(200);
|
|
839
|
+
}
|
|
840
|
+
throw new Error(message);
|
|
841
|
+
}
|
|
842
|
+
onMessage(raw) { let message; try {
|
|
843
|
+
message = JSON.parse(raw);
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
return;
|
|
847
|
+
} if (message.id === undefined)
|
|
848
|
+
return; const pending = this.pending.get(message.id); if (!pending)
|
|
849
|
+
return; this.pending.delete(message.id); pending.resolve(message); }
|
|
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
|
+
}
|
|
869
|
+
async evaluate(expression) {
|
|
870
|
+
const slot = `__agentBridgeEvaluation_${(0, node_crypto_1.randomUUID)().replaceAll("-", "")}`;
|
|
871
|
+
let failure;
|
|
872
|
+
try {
|
|
873
|
+
const wrapped = `(() => { const state = {}; globalThis[${JSON.stringify(slot)}] = state; state.promise = Promise.resolve((${expression})); return state.promise; })()`;
|
|
874
|
+
const response = await this.cdp("Runtime.evaluate", { expression: wrapped, returnByValue: true, awaitPromise: true });
|
|
875
|
+
if (response.error)
|
|
876
|
+
throw new Error(`WorkBuddy CDP Runtime.evaluate 失败:${response.error.message ?? response.error.code}`);
|
|
877
|
+
if (response.result?.exceptionDetails) {
|
|
878
|
+
const details = response.result.exceptionDetails;
|
|
879
|
+
throw new Error(`WorkBuddy 页面 JS 异常:${details.exception?.description ?? details.text ?? JSON.stringify(details)}`);
|
|
880
|
+
}
|
|
881
|
+
return response.result?.result?.value;
|
|
882
|
+
}
|
|
883
|
+
catch (error) {
|
|
884
|
+
failure = error;
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
finally {
|
|
888
|
+
try {
|
|
889
|
+
if (!(failure instanceof WorkbuddyCdpTransportError))
|
|
890
|
+
await this.cdp("Runtime.evaluate", { expression: `delete globalThis[${JSON.stringify(slot)}]`, returnByValue: true });
|
|
891
|
+
}
|
|
892
|
+
catch { /* 连接中断时保留原始错误,页面重载会释放槽位。 */ }
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
exports.WorkbuddyCdpClient = WorkbuddyCdpClient;
|