mu-agent-bridge 1.0.33 → 1.0.34
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 +2 -0
- package/dist/src-ts/plugin-manager.js +16 -5
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +130 -23
- package/dist/src-ts/plugins/workbuddy/index.js +41 -7
- package/dist/tests-ts/workbuddy-plugin.test.js +240 -9
- package/package.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +130 -23
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +41 -7
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +3 -3
package/README.md
CHANGED
|
@@ -142,3 +142,5 @@ Windows 路径时,可设置 `DSH_INSTALL_DIR`。
|
|
|
142
142
|
## AUN 身份管理
|
|
143
143
|
|
|
144
144
|
创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md);给 Codex、Claude Code 直接执行的 CLI 说明见 [使用说明](使用说明.md)。
|
|
145
|
+
|
|
146
|
+
WorkBuddy 执行中的可见消息可经原通道逐条回传,同一请求去重并排除思考过程;配合 `agent-bridge-workbuddy@1.1.11` 使用。插件升级同时刷新内部依赖,避免继续运行旧版客户端。
|
|
@@ -366,7 +366,15 @@ class PluginManager {
|
|
|
366
366
|
? (request) => this.controlSession(target, request)
|
|
367
367
|
: undefined;
|
|
368
368
|
const task = !message.text.trim() && message.attachments?.length ? "请处理收到的附件" : message.text;
|
|
369
|
-
const
|
|
369
|
+
const forwarded = new Set();
|
|
370
|
+
const onMessage = async (text) => {
|
|
371
|
+
if (!text.trim() || forwarded.has(text))
|
|
372
|
+
return;
|
|
373
|
+
await message.reply(text);
|
|
374
|
+
forwarded.add(text);
|
|
375
|
+
this.recordMessage(target, "reply", text, message.peerId);
|
|
376
|
+
};
|
|
377
|
+
const response = await this.sessions.run(message, target, (sessionId, newSession) => this.handle(target, { task, sessionId, onMessage, ...(newSession ? { newSession: true } : {}), ...(message.attachments ? { attachments: message.attachments } : {}) }), control);
|
|
370
378
|
const attachments = response.attachments?.length ? response.attachments : (0, attachments_1.asAttachments)(response.images);
|
|
371
379
|
(0, attachments_1.validateAttachments)(attachments);
|
|
372
380
|
if (attachments.length) {
|
|
@@ -375,10 +383,10 @@ class PluginManager {
|
|
|
375
383
|
throw new agent_1.AgentError("当前通道不支持附件回传");
|
|
376
384
|
}
|
|
377
385
|
await message.replyAttachments(attachments, response.text);
|
|
386
|
+
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
378
387
|
}
|
|
379
388
|
else
|
|
380
|
-
await
|
|
381
|
-
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
389
|
+
await onMessage(response.text);
|
|
382
390
|
}
|
|
383
391
|
catch (error) {
|
|
384
392
|
this.recordMessage(target, "error", error instanceof Error ? error.message : String(error), message.peerId);
|
|
@@ -626,10 +634,13 @@ class PluginManager {
|
|
|
626
634
|
const packagedFile = (0, node_path_1.join)(record.path, record.manifest.entry);
|
|
627
635
|
const developmentFile = (0, node_path_1.resolve)(__dirname, "plugins", record.id, (0, node_path_1.basename)(record.manifest.entry));
|
|
628
636
|
const file = (0, node_fs_1.existsSync)(packagedFile) ? packagedFile : developmentFile;
|
|
629
|
-
//
|
|
637
|
+
// 同时刷新插件内部依赖,避免升级后仍运行旧版 CDP 客户端。
|
|
630
638
|
const req = (0, node_module_1.createRequire)(file);
|
|
631
639
|
const resolved = req.resolve(file);
|
|
632
|
-
|
|
640
|
+
const moduleRoot = (0, node_fs_1.existsSync)(packagedFile) ? (0, node_path_1.resolve)(record.path) : (0, node_path_1.dirname)(file);
|
|
641
|
+
for (const cached of Object.keys(req.cache))
|
|
642
|
+
if (cached === resolved || cached.startsWith(`${moduleRoot}${node_path_1.sep}`))
|
|
643
|
+
delete req.cache[cached];
|
|
633
644
|
const mod = req(resolved);
|
|
634
645
|
let value = mod.default ?? mod.plugin ?? mod;
|
|
635
646
|
// Entries may export a ready instance, a factory, or a class. Supporting all
|
|
@@ -4,6 +4,17 @@ exports.WorkbuddyCdpClient = exports.assistantSelector = exports.editorSelector
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const attachments_1 = require("../../attachments");
|
|
6
6
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
7
|
+
const interactionOptionSelector = '.conversation-input-area [role="dialog"] button,.pending-sandbox-panel button,button[class*="conversation-"][class*="__option"]';
|
|
8
|
+
function interactionText(value) { return value.normalize("NFKC").replace(/[\uFEFF\u200B]/g, "").trim(); }
|
|
9
|
+
function formatInteraction(value) { return [value.prompt, ...value.options.map((option, index) => `${index + 1}. ${option}`)].filter(Boolean).join("\n"); }
|
|
10
|
+
function interactionChoice(value, options) {
|
|
11
|
+
const input = interactionText(value).replace(/^[第\s]*(\d+)[、.。::)]?\s*$/, "$1");
|
|
12
|
+
const number = Number(input);
|
|
13
|
+
if (Number.isInteger(number) && number >= 1 && number <= options.length && String(number) === input)
|
|
14
|
+
return number - 1;
|
|
15
|
+
const index = options.findIndex((option) => interactionText(option) === input);
|
|
16
|
+
return index >= 0 ? index : undefined;
|
|
17
|
+
}
|
|
7
18
|
exports.editorSelector = '[data-slate-editor][contenteditable="true"]';
|
|
8
19
|
exports.assistantSelector = '.cr-agent[data-message-id] [data-assistant-content]';
|
|
9
20
|
class WorkbuddyCdpClient {
|
|
@@ -11,11 +22,18 @@ class WorkbuddyCdpClient {
|
|
|
11
22
|
socket;
|
|
12
23
|
sequence = 0;
|
|
13
24
|
pending = new Map();
|
|
25
|
+
pendingInteraction;
|
|
26
|
+
pendingInteractionCleanup;
|
|
14
27
|
port;
|
|
15
28
|
timeoutMs;
|
|
16
29
|
fetchImpl;
|
|
17
30
|
constructor(options = {}) { this.port = options.port ?? Number(process.env.WORKBUDDY_CDP_PORT ?? "9229"); this.timeoutMs = options.timeoutMs ?? 30_000; this.fetchImpl = options.fetchImpl ?? fetch; }
|
|
18
31
|
connected() { return Boolean(this.socket); }
|
|
32
|
+
pendingInteractionSessionId() { return this.pendingInteraction?.sessionId; }
|
|
33
|
+
async conversationId() {
|
|
34
|
+
const value = await this.evaluate(`([...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_')))?.getAttribute('data-conversation-id')`);
|
|
35
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
36
|
+
}
|
|
19
37
|
async connect() {
|
|
20
38
|
if (this.socket)
|
|
21
39
|
return;
|
|
@@ -39,18 +57,12 @@ class WorkbuddyCdpClient {
|
|
|
39
57
|
close() { this.socket?.close(); this.socket = undefined; for (const pending of this.pending.values())
|
|
40
58
|
pending.reject(new Error("WorkBuddy CDP 已关闭")); this.pending.clear(); }
|
|
41
59
|
async createTask() {
|
|
42
|
-
const snapshot = `(() => {
|
|
43
|
-
const selected = [...document.querySelectorAll('[data-conversation-id]')].find((item) => String(item.className).includes('_selected_'));
|
|
44
|
-
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)})?.cloneNode(true);
|
|
45
|
-
for (const placeholder of editor?.querySelectorAll('[data-slate-placeholder]') ?? []) placeholder.remove();
|
|
46
|
-
return { url: location.href.split(/[?#]/, 1)[0], conversationId: selected?.getAttribute('data-conversation-id') ?? null, messages: document.querySelectorAll('.cr-agent[data-message-id]').length, editor: editor?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim() ?? null };
|
|
47
|
-
})()`;
|
|
60
|
+
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"]')) }; })()`;
|
|
48
61
|
const before = await this.evaluate(snapshot);
|
|
49
62
|
if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"]')].find((item) => item.textContent?.trim() === '新建任务'); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`))
|
|
50
63
|
throw new Error("WorkBuddy 新建任务入口未找到或不可用");
|
|
51
|
-
//
|
|
52
|
-
const
|
|
53
|
-
await this.waitFor(`(() => { const state = ${snapshot}; return state.editor === '' && state.messages === 0 && (state.conversationId !== ${JSON.stringify(before.conversationId)} || state.url !== ${JSON.stringify(before.url)} || (${wasLanding} && state.conversationId === null)); })()`, "WorkBuddy 新任务状态校验失败");
|
|
64
|
+
// WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
|
|
65
|
+
await this.waitFor(`(() => { const state = ${snapshot}; const blank = state.editor !== null && state.editor.trim() === '' && state.messages === 0; return blank && ((state.conversationId && state.conversationId !== ${JSON.stringify(before.conversationId)}) || state.url !== ${JSON.stringify(before.url)} || (state.home && !state.selected)); })()`, "WorkBuddy 新任务状态校验失败");
|
|
54
66
|
}
|
|
55
67
|
async direct(text, timeoutMs = 240_000, expert, newTask = false) {
|
|
56
68
|
return (await this.complete({ task: text, timeoutMs }, expert, newTask)).text;
|
|
@@ -59,6 +71,16 @@ class WorkbuddyCdpClient {
|
|
|
59
71
|
(0, attachments_1.validateAttachments)(request.attachments);
|
|
60
72
|
const text = request.task;
|
|
61
73
|
const timeoutMs = request.timeoutMs ?? 240_000;
|
|
74
|
+
if (!this.pendingInteraction && request.sessionId && !newTask && !expert) {
|
|
75
|
+
const interaction = await this.readInteraction();
|
|
76
|
+
if (interaction)
|
|
77
|
+
this.pendingInteraction = { ...interaction, sessionId: request.sessionId };
|
|
78
|
+
}
|
|
79
|
+
if (this.pendingInteraction) {
|
|
80
|
+
if (this.pendingInteraction.sessionId && this.pendingInteraction.sessionId !== request.sessionId)
|
|
81
|
+
throw new Error("WorkBuddy 正在等待另一个会话的选择");
|
|
82
|
+
return this.continueInteraction(text, timeoutMs, request.onMessage);
|
|
83
|
+
}
|
|
62
84
|
if (expert)
|
|
63
85
|
await this.selectExpert(expert);
|
|
64
86
|
else if (newTask)
|
|
@@ -91,28 +113,53 @@ class WorkbuddyCdpClient {
|
|
|
91
113
|
await this.waitFor(`(() => { const button = document.querySelector('button[aria-label="发送"]'); return Boolean(button && !button.disabled); })()`, "WorkBuddy 发送按钮未就绪");
|
|
92
114
|
if (!await this.evaluate(`(() => { const button = document.querySelector('button[aria-label="发送"]'); if (!button || button.disabled) return false; button.click(); return true; })()`))
|
|
93
115
|
throw new Error("WorkBuddy 发送按钮不可用");
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage);
|
|
117
|
+
if (this.pendingInteraction && temporaryPaths.length)
|
|
118
|
+
this.pendingInteractionCleanup = temporaryPaths;
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
failure = error;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (temporaryPaths.length && this.pendingInteractionCleanup !== temporaryPaths) {
|
|
127
|
+
try {
|
|
128
|
+
await this.cleanupTemporaryFiles(temporaryPaths);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (failure !== undefined)
|
|
132
|
+
throw new AggregateError([failure, error], `${failure instanceof Error ? failure.message : String(failure)};临时附件清理失败`, { cause: failure });
|
|
133
|
+
throw error;
|
|
106
134
|
}
|
|
107
135
|
}
|
|
108
|
-
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async continueInteraction(answer, timeoutMs, onMessage) {
|
|
139
|
+
const interaction = this.pendingInteraction;
|
|
140
|
+
const index = interactionChoice(answer, interaction.options);
|
|
141
|
+
if (index === undefined)
|
|
142
|
+
return { text: formatInteraction(interaction) };
|
|
143
|
+
const temporaryPaths = this.pendingInteractionCleanup;
|
|
144
|
+
const before = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => element.getAttribute('data-message-id'))) `)));
|
|
145
|
+
const beforeIds = new Set((Array.isArray(before) ? before : []).filter((id) => typeof id === "string"));
|
|
146
|
+
if (!await this.evaluate(`(() => { const options = [...document.querySelectorAll(${JSON.stringify(interactionOptionSelector)})]; const option = options[${index}]; if (!option || option.disabled || option.getAttribute('aria-disabled') === 'true') return false; option.click(); return true; })()`))
|
|
147
|
+
throw new Error("WorkBuddy 选择项不可用");
|
|
148
|
+
this.pendingInteraction = undefined;
|
|
149
|
+
let failure;
|
|
150
|
+
try {
|
|
151
|
+
const response = await this.waitForReply(beforeIds, timeoutMs, interaction.messageId, interaction.initialText, interaction.sessionId, onMessage);
|
|
152
|
+
if (!this.pendingInteraction)
|
|
153
|
+
this.pendingInteractionCleanup = undefined;
|
|
154
|
+
return response;
|
|
109
155
|
}
|
|
110
156
|
catch (error) {
|
|
111
157
|
failure = error;
|
|
112
158
|
throw error;
|
|
113
159
|
}
|
|
114
160
|
finally {
|
|
115
|
-
if (temporaryPaths.
|
|
161
|
+
if (temporaryPaths && !this.pendingInteraction) {
|
|
162
|
+
this.pendingInteractionCleanup = undefined;
|
|
116
163
|
try {
|
|
117
164
|
await this.cleanupTemporaryFiles(temporaryPaths);
|
|
118
165
|
}
|
|
@@ -124,6 +171,66 @@ class WorkbuddyCdpClient {
|
|
|
124
171
|
}
|
|
125
172
|
}
|
|
126
173
|
}
|
|
174
|
+
async waitForReply(beforeIds, timeoutMs, changedId, initialText, sessionId, onMessage) {
|
|
175
|
+
const deadline = Date.now() + timeoutMs;
|
|
176
|
+
const previousText = new Map();
|
|
177
|
+
const stablePolls = new Map();
|
|
178
|
+
const forwarded = new Set();
|
|
179
|
+
while (Date.now() < deadline) {
|
|
180
|
+
await sleep(1_000);
|
|
181
|
+
const current = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => { const content = element.querySelector('[data-assistant-content]'); let text = content?.innerText?.trim() || ''; for (const thought of content?.querySelectorAll?.('.cr-reasoning') || []) text = text.replace(thought.innerText || '', ''); text = text.trim(); const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]')); const stopped = Boolean(text) && !document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]'); return { id: element.getAttribute('data-message-id'), text, finished: finished, stopped: stopped }; }))`)));
|
|
182
|
+
const interaction = await this.readInteraction();
|
|
183
|
+
const candidates = current.filter((item) => item.id && (!beforeIds.has(item.id) || item.id === changedId && item.text !== initialText));
|
|
184
|
+
for (const item of candidates) {
|
|
185
|
+
const id = item.id;
|
|
186
|
+
const text = item.text?.trim() ?? "";
|
|
187
|
+
const count = text && text === previousText.get(id) ? (stablePolls.get(id) ?? 0) + 1 : text ? 1 : 0;
|
|
188
|
+
previousText.set(id, text);
|
|
189
|
+
stablePolls.set(id, count);
|
|
190
|
+
}
|
|
191
|
+
if (interaction) {
|
|
192
|
+
const body = candidates.map((item) => item.text?.trim()).filter((text) => Boolean(text && !forwarded.has(text)));
|
|
193
|
+
if (onMessage)
|
|
194
|
+
for (const text of body) {
|
|
195
|
+
await onMessage(text);
|
|
196
|
+
forwarded.add(text);
|
|
197
|
+
}
|
|
198
|
+
const latest = candidates.at(-1) ?? current.at(-1);
|
|
199
|
+
this.pendingInteraction = { ...interaction, messageId: interaction.messageId ?? latest?.id ?? undefined, initialText: interaction.initialText ?? latest?.text ?? "", sessionId };
|
|
200
|
+
return { text: [...(onMessage ? [] : body), formatInteraction(this.pendingInteraction)].join("\n\n") };
|
|
201
|
+
}
|
|
202
|
+
const latest = candidates.at(-1);
|
|
203
|
+
const completed = candidates.filter((item) => item.finished || Boolean(item.stopped && (stablePolls.get(item.id) ?? 0) >= 2));
|
|
204
|
+
const reply = candidates.map((item) => item.text?.trim()).filter((text) => text && !forwarded.has(text)).join("\n\n");
|
|
205
|
+
const latestComplete = latest && (latest.finished || Boolean(latest.stopped && (stablePolls.get(latest.id) ?? 0) >= 2));
|
|
206
|
+
if (latestComplete && completed.length) {
|
|
207
|
+
const attachments = await this.readArtifacts(new Set(completed.map((item) => item.id)));
|
|
208
|
+
if (attachments.length)
|
|
209
|
+
return { text: reply ?? "", attachments };
|
|
210
|
+
if (reply || forwarded.size)
|
|
211
|
+
return { text: reply };
|
|
212
|
+
}
|
|
213
|
+
if (onMessage) {
|
|
214
|
+
for (const item of candidates) {
|
|
215
|
+
const text = item.text?.trim();
|
|
216
|
+
if (text && (stablePolls.get(item.id) ?? 0) >= 2 && !forwarded.has(text)) {
|
|
217
|
+
await onMessage(text);
|
|
218
|
+
forwarded.add(text);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw new Error("WorkBuddy UI 回复超时");
|
|
224
|
+
}
|
|
225
|
+
async readInteraction() {
|
|
226
|
+
const raw = await this.evaluate(`JSON.stringify((() => { const nodes = [...document.querySelectorAll(${JSON.stringify(interactionOptionSelector)})]; if (!nodes.length) return null; const root = nodes[0].closest('[role="dialog"],.pending-sandbox-panel,.conversation-high-credit-approval') || nodes[0].parentElement; const rawOptions = nodes.map(node => (node.innerText || node.textContent)?.trim() || '').filter(Boolean); let prompt = root?.innerText?.trim() || ''; for (const option of rawOptions) prompt = prompt.replace(option, ''); return { prompt: prompt.replace(/\\n{2,}/g, '\\n').trim(), options: rawOptions.map(option => option.replace(/^\\d+\\s*[.、::)]?\\s*/, '').trim()), messageId: nodes[0].closest('.cr-agent[data-message-id]')?.getAttribute('data-message-id') || undefined, initialText: nodes[0].closest('.cr-agent[data-message-id]')?.querySelector('[data-assistant-content]')?.innerText?.trim() || '' }; })())`);
|
|
227
|
+
if (!raw || raw === "null" || raw === "undefined")
|
|
228
|
+
return undefined;
|
|
229
|
+
const value = JSON.parse(String(raw));
|
|
230
|
+
if (!Array.isArray(value.options) || value.options.length === 0 || value.options.some((option) => typeof option !== "string" || !option.trim()))
|
|
231
|
+
return undefined;
|
|
232
|
+
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 } : {}) };
|
|
233
|
+
}
|
|
127
234
|
async injectAttachments(items, temporaryPaths = []) {
|
|
128
235
|
const before = await this.evaluate(`JSON.stringify([...document.querySelector(${JSON.stringify(exports.editorSelector)}).querySelectorAll('[class*="_fileTag"]')].map(element => element.textContent?.trim()).filter(Boolean))`);
|
|
129
236
|
if (JSON.parse(String(before)).length)
|
|
@@ -13,7 +13,7 @@ const plugin_1 = require("../../plugin");
|
|
|
13
13
|
const cdp_client_1 = require("./cdp-client");
|
|
14
14
|
const cli_client_1 = require("./cli-client");
|
|
15
15
|
exports.workbuddyManifest = {
|
|
16
|
-
id: "workbuddy", version: "1.1.
|
|
16
|
+
id: "workbuddy", version: "1.1.11", displayName: "WorkBuddy", type: "agent",
|
|
17
17
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
18
18
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
19
19
|
};
|
|
@@ -41,6 +41,7 @@ class WorkbuddyPlugin {
|
|
|
41
41
|
queue = Promise.resolve();
|
|
42
42
|
pending = 0;
|
|
43
43
|
activeSessionId;
|
|
44
|
+
activeDesktopConversationId;
|
|
44
45
|
desktopSessionValid = true;
|
|
45
46
|
knownSessions = new Set();
|
|
46
47
|
sessionDataDir;
|
|
@@ -63,14 +64,18 @@ class WorkbuddyPlugin {
|
|
|
63
64
|
this.cliPath = (0, cli_client_1.findCli)(typeof config.cliPath === "string" ? config.cliPath : undefined);
|
|
64
65
|
this.failure = undefined;
|
|
65
66
|
this.activeSessionId = undefined;
|
|
67
|
+
this.activeDesktopConversationId = undefined;
|
|
66
68
|
this.activeExpert = undefined;
|
|
67
69
|
this.expertSession = false;
|
|
68
70
|
this.desktopSessionValid = true;
|
|
69
71
|
this.knownSessions.clear();
|
|
70
72
|
this.sessionDataDir = context.dataDir;
|
|
73
|
+
let savedBinding;
|
|
71
74
|
if (this.sessionDataDir) {
|
|
72
|
-
|
|
75
|
+
const saved = await this.loadKnownSessions(this.sessionDataDir);
|
|
76
|
+
for (const sessionId of saved.sessionIds)
|
|
73
77
|
this.knownSessions.add(sessionId);
|
|
78
|
+
savedBinding = saved.active;
|
|
74
79
|
}
|
|
75
80
|
this.sessionOperations = [];
|
|
76
81
|
if (this.transport === "cli") {
|
|
@@ -91,6 +96,11 @@ class WorkbuddyPlugin {
|
|
|
91
96
|
throw error;
|
|
92
97
|
}
|
|
93
98
|
this.sessionOperations = (this.transport === "cdp" || !this.cliPath) && this.client?.createTask ? ["new"] : [];
|
|
99
|
+
if (savedBinding && this.sessionOperations.length && await this.client?.conversationId?.() === savedBinding.conversationId) {
|
|
100
|
+
this.activeSessionId = savedBinding.sessionId;
|
|
101
|
+
this.activeDesktopConversationId = savedBinding.conversationId;
|
|
102
|
+
this.activeExpert = savedBinding.expert;
|
|
103
|
+
}
|
|
94
104
|
}
|
|
95
105
|
async deactivate(_reason) {
|
|
96
106
|
await this.queue;
|
|
@@ -111,7 +121,17 @@ class WorkbuddyPlugin {
|
|
|
111
121
|
if (!request.task.trim())
|
|
112
122
|
throw new Error("消息内容不能为空");
|
|
113
123
|
(0, attachments_1.validateAttachments)(request.attachments);
|
|
114
|
-
const run = this.enqueue(() =>
|
|
124
|
+
const run = this.enqueue(async () => {
|
|
125
|
+
try {
|
|
126
|
+
return await this.complete(request);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (this.activeSessionId && this.desktopSessionValid && this.client?.conversationId) {
|
|
130
|
+
this.activeDesktopConversationId = await this.client.conversationId();
|
|
131
|
+
await this.persistKnownSessions();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
115
135
|
const response = await run;
|
|
116
136
|
return { ...response, sessionId: request.sessionId ?? response.sessionId };
|
|
117
137
|
}
|
|
@@ -141,6 +161,9 @@ class WorkbuddyPlugin {
|
|
|
141
161
|
const hasAttachments = Array.isArray(request.attachments) && request.attachments.length > 0;
|
|
142
162
|
if (hasAttachments && !this.attachmentInput)
|
|
143
163
|
throw new Error("WorkBuddy 当前传输不支持附件输入");
|
|
164
|
+
const pendingInteractionSessionId = this.client?.pendingInteractionSessionId?.();
|
|
165
|
+
if (!usesCli && pendingInteractionSessionId && pendingInteractionSessionId !== request.sessionId)
|
|
166
|
+
throw new Error("WorkBuddy 正在等待另一个会话的选择");
|
|
144
167
|
if (usesCli && request.sessionId)
|
|
145
168
|
throw new Error("CodeBuddy CLI 会话隔离未验证,不能处理带 sessionId 的请求");
|
|
146
169
|
if (!usesCli && !request.sessionId && this.activeSessionId)
|
|
@@ -196,8 +219,13 @@ class WorkbuddyPlugin {
|
|
|
196
219
|
throw new Error("WorkBuddy CDP 未连接");
|
|
197
220
|
if (!this.client.createTask)
|
|
198
221
|
throw new Error("WorkBuddy 客户端不支持新建会话");
|
|
199
|
-
if (this.activeSessionId === sessionId && this.desktopSessionValid)
|
|
222
|
+
if (this.activeSessionId === sessionId && this.desktopSessionValid) {
|
|
223
|
+
if (this.activeDesktopConversationId && await this.client.conversationId?.() !== this.activeDesktopConversationId) {
|
|
224
|
+
this.desktopSessionValid = false;
|
|
225
|
+
throw new Error("WorkBuddy 桌面任务已切换,请先创建新会话");
|
|
226
|
+
}
|
|
200
227
|
return;
|
|
228
|
+
}
|
|
201
229
|
if (this.knownSessions.has(sessionId))
|
|
202
230
|
throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
|
|
203
231
|
await this.createSession(sessionId);
|
|
@@ -205,6 +233,8 @@ class WorkbuddyPlugin {
|
|
|
205
233
|
async createSession(sessionId, name) {
|
|
206
234
|
// 点击失败时桌面可能已经切换,保留逻辑绑定但禁止向未知任务续发。
|
|
207
235
|
this.desktopSessionValid = false;
|
|
236
|
+
this.activeSessionId = undefined;
|
|
237
|
+
this.activeDesktopConversationId = undefined;
|
|
208
238
|
this.knownSessions.add(sessionId);
|
|
209
239
|
await this.persistKnownSessions();
|
|
210
240
|
await this.client.createTask(name);
|
|
@@ -221,11 +251,14 @@ class WorkbuddyPlugin {
|
|
|
221
251
|
const sessionIds = parsed.sessionIds;
|
|
222
252
|
if (!Array.isArray(sessionIds) || sessionIds.some((sessionId) => typeof sessionId !== "string" || !sessionId.trim()))
|
|
223
253
|
throw new Error("sessionIds 无效");
|
|
224
|
-
|
|
254
|
+
const active = parsed.active;
|
|
255
|
+
if (active !== undefined && (!active || typeof active.sessionId !== "string" || !sessionIds.includes(active.sessionId) || typeof active.conversationId !== "string" || !active.conversationId.trim() || (active.expert !== undefined && active.expert !== null && typeof active.expert !== "string")))
|
|
256
|
+
throw new Error("活动会话绑定无效");
|
|
257
|
+
return { sessionIds: new Set(sessionIds), ...(active ? { active: active } : {}) };
|
|
225
258
|
}
|
|
226
259
|
catch (error) {
|
|
227
260
|
if (error.code === "ENOENT")
|
|
228
|
-
return new Set();
|
|
261
|
+
return { sessionIds: new Set() };
|
|
229
262
|
if (error instanceof SyntaxError || error instanceof Error)
|
|
230
263
|
throw new Error(`WorkBuddy 已知会话记录无效:${error.message}`);
|
|
231
264
|
throw new Error("WorkBuddy 已知会话记录无效");
|
|
@@ -237,7 +270,8 @@ class WorkbuddyPlugin {
|
|
|
237
270
|
const write = this.sessionWrites.catch(() => { }).then(async () => {
|
|
238
271
|
await (0, promises_1.mkdir)(this.sessionDataDir, { recursive: true });
|
|
239
272
|
const file = (0, node_path_1.join)(this.sessionDataDir, "known-sessions.json");
|
|
240
|
-
|
|
273
|
+
const active = this.desktopSessionValid && this.activeSessionId && this.activeDesktopConversationId ? { sessionId: this.activeSessionId, conversationId: this.activeDesktopConversationId, expert: this.activeExpert } : undefined;
|
|
274
|
+
await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify({ sessionIds: [...this.knownSessions].sort(), active })}\n`, { mode: 0o600 });
|
|
241
275
|
await (0, promises_1.rename)(`${file}.tmp`, file);
|
|
242
276
|
});
|
|
243
277
|
this.sessionWrites = write;
|
|
@@ -190,6 +190,194 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
190
190
|
strict_1.default.equal(clicks, 1);
|
|
191
191
|
strict_1.default.ok(readinessChecks >= 3);
|
|
192
192
|
});
|
|
193
|
+
(0, node_test_1.default)("workbuddy CDP 页面已停止但没有完成标记时仍回传可见文本", async () => {
|
|
194
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
195
|
+
let polls = 0;
|
|
196
|
+
const internal = client;
|
|
197
|
+
internal.cdp = async () => undefined;
|
|
198
|
+
internal.readArtifacts = async () => [];
|
|
199
|
+
internal.evaluate = async (expression) => {
|
|
200
|
+
if (expression.includes("finished:")) {
|
|
201
|
+
polls++;
|
|
202
|
+
return JSON.stringify([{ id: "new", text: "视频已停止", finished: false, stopped: true }]);
|
|
203
|
+
}
|
|
204
|
+
if (expression.includes("conversation-input-area"))
|
|
205
|
+
return "[]";
|
|
206
|
+
if (expression.includes('button[aria-label="发送"]'))
|
|
207
|
+
return true;
|
|
208
|
+
if (expression.includes("JSON.stringify"))
|
|
209
|
+
return "[]";
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
212
|
+
strict_1.default.deepEqual(await client.complete({ task: "继续", timeoutMs: 2_200 }), { text: "视频已停止" });
|
|
213
|
+
strict_1.default.ok(polls >= 2);
|
|
214
|
+
});
|
|
215
|
+
(0, node_test_1.default)("workbuddy CDP 选择后仍在处理时不能把工具状态当成最终回复", async () => {
|
|
216
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
217
|
+
let polls = 0;
|
|
218
|
+
const internal = client;
|
|
219
|
+
internal.readArtifacts = async () => [];
|
|
220
|
+
const message = {
|
|
221
|
+
getAttribute: (name) => name === "data-message-id" ? "new" : name === "data-message-processing" && polls < 3 ? "true" : null,
|
|
222
|
+
querySelector: (selector) => selector === "[data-assistant-content]" ? { innerText: polls < 3 ? "执行中\nSensitiveProtection" : "图片已收到:1024×768" } : polls >= 3 ? {} : null,
|
|
223
|
+
};
|
|
224
|
+
internal.evaluate = async (expression) => {
|
|
225
|
+
if (expression.includes("finished:"))
|
|
226
|
+
polls++;
|
|
227
|
+
return (0, node_vm_1.runInNewContext)(expression, { document: {
|
|
228
|
+
querySelectorAll: (selector) => selector === ".cr-agent[data-message-id]" ? [message] : [],
|
|
229
|
+
querySelector: (selector) => selector.includes('button[aria-label="停止"]') && polls < 3 ? {} : null,
|
|
230
|
+
} });
|
|
231
|
+
};
|
|
232
|
+
strict_1.default.deepEqual(await internal.waitForReply(new Set(), 4_000), { text: "图片已收到:1024×768" });
|
|
233
|
+
strict_1.default.equal(polls, 3);
|
|
234
|
+
});
|
|
235
|
+
(0, node_test_1.default)("workbuddy CDP 选择框先回传选项,收到序号后点击并继续等待", async () => {
|
|
236
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
237
|
+
let polls = 0;
|
|
238
|
+
let clicks = 0;
|
|
239
|
+
const internal = client;
|
|
240
|
+
internal.cdp = async () => undefined;
|
|
241
|
+
internal.readArtifacts = async () => [];
|
|
242
|
+
internal.evaluate = async (expression) => {
|
|
243
|
+
if (expression.includes("finished:")) {
|
|
244
|
+
polls++;
|
|
245
|
+
return JSON.stringify(polls === 1
|
|
246
|
+
? [{ id: "new", text: "", finished: false, stopped: false }]
|
|
247
|
+
: [{ id: "new", text: "视频已开始", finished: true, stopped: true }]);
|
|
248
|
+
}
|
|
249
|
+
if (expression.includes("option.click")) {
|
|
250
|
+
clicks++;
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
if (expression.includes("conversation-input-area")) {
|
|
254
|
+
strict_1.default.match(expression, /conversation-input-area/);
|
|
255
|
+
return polls === 1 ? JSON.stringify({ prompt: "确认开始生成视频?", options: ["确认", "本次会话始终允许", "拒绝"], messageId: "new", initialText: "" }) : "[]";
|
|
256
|
+
}
|
|
257
|
+
if (expression.includes('button[aria-label="发送"]'))
|
|
258
|
+
return true;
|
|
259
|
+
if (expression.includes("JSON.stringify"))
|
|
260
|
+
return "[]";
|
|
261
|
+
return true;
|
|
262
|
+
};
|
|
263
|
+
strict_1.default.equal((await client.complete({ task: "生成视频", timeoutMs: 2_200 })).text, "确认开始生成视频?\n1. 确认\n2. 本次会话始终允许\n3. 拒绝");
|
|
264
|
+
strict_1.default.deepEqual(await client.complete({ task: "1", timeoutMs: 2_200 }), { text: "视频已开始" });
|
|
265
|
+
strict_1.default.equal(clicks, 1);
|
|
266
|
+
});
|
|
267
|
+
(0, node_test_1.default)("workbuddy CDP 回传敏感凭证选择框并按同一组选项继续", async () => {
|
|
268
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
269
|
+
let clicked = -1;
|
|
270
|
+
const labels = ["允许访问", "允许加密访问(推荐)", "禁止访问"];
|
|
271
|
+
const root = { innerText: `是否允许模型访问敏感凭证?\n${labels.map((text, index) => `${index + 1}\n${text}`).join("\n")}` };
|
|
272
|
+
const nodes = labels.map((text, index) => ({
|
|
273
|
+
innerText: `${index + 1}\n${text}`, textContent: `${index + 1}${text}`,
|
|
274
|
+
parentElement: { innerText: "只有选项" },
|
|
275
|
+
closest: (selector) => selector.includes("pending-sandbox-panel") ? root : null,
|
|
276
|
+
getAttribute: () => null, click: () => { clicked = index; },
|
|
277
|
+
}));
|
|
278
|
+
const internal = client;
|
|
279
|
+
internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: { querySelectorAll: (selector) => selector.includes(".pending-sandbox-panel button") ? nodes : [] } });
|
|
280
|
+
const interaction = await internal.readInteraction();
|
|
281
|
+
strict_1.default.deepEqual(interaction, { prompt: "是否允许模型访问敏感凭证?", options: labels, initialText: "" });
|
|
282
|
+
internal.pendingInteraction = interaction;
|
|
283
|
+
internal.waitForReply = async () => ({ text: "已继续" });
|
|
284
|
+
strict_1.default.deepEqual(await client.complete({ task: "2" }), { text: "已继续" });
|
|
285
|
+
strict_1.default.equal(clicked, 1);
|
|
286
|
+
});
|
|
287
|
+
(0, node_test_1.default)("workbuddy CDP 高额确认框只读取按钮选项", async () => {
|
|
288
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
289
|
+
const labels = ["确认", "本次会话始终允许", "拒绝"];
|
|
290
|
+
const root = { innerText: `确认开始生成视频?\n积分消耗较高\n${labels.map((text, index) => `${index + 1}\n${text}`).join("\n")}` };
|
|
291
|
+
const nodes = labels.map((text, index) => ({
|
|
292
|
+
tagName: "BUTTON", className: "conversation-high-credit-approval__option", innerText: `${index + 1}\n${text}`, textContent: `${index + 1}${text}`,
|
|
293
|
+
closest: (selector) => selector.includes("conversation-high-credit-approval") ? root : null,
|
|
294
|
+
getAttribute: (name) => name === "data-message-id" ? "new" : null,
|
|
295
|
+
querySelector: () => ({ innerText: "" }), click: () => undefined,
|
|
296
|
+
}));
|
|
297
|
+
const descendants = nodes.flatMap((node, index) => [node,
|
|
298
|
+
{ ...node, tagName: "SPAN", className: "conversation-high-credit-approval__option-num", innerText: String(index + 1) },
|
|
299
|
+
{ ...node, tagName: "SPAN", className: "conversation-high-credit-approval__option-text", innerText: labels[index] },
|
|
300
|
+
]);
|
|
301
|
+
const internal = client;
|
|
302
|
+
internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: {
|
|
303
|
+
querySelectorAll: (selector) => {
|
|
304
|
+
return descendants.filter(node => selector.split(",").some(part => {
|
|
305
|
+
if (part.startsWith("."))
|
|
306
|
+
return part.slice(1) === node.className;
|
|
307
|
+
if (part.startsWith("button") && node.tagName !== "BUTTON")
|
|
308
|
+
return false;
|
|
309
|
+
const classes = [...part.matchAll(/\[class\*=['"]([^'"]+)['"]\]/g)].map(match => match[1]);
|
|
310
|
+
return classes.length > 0 && classes.every(value => node.className.includes(value));
|
|
311
|
+
}));
|
|
312
|
+
},
|
|
313
|
+
} });
|
|
314
|
+
strict_1.default.deepEqual(await internal.readInteraction(), { prompt: "确认开始生成视频?\n积分消耗较高", options: labels, initialText: "" });
|
|
315
|
+
});
|
|
316
|
+
(0, node_test_1.default)("workbuddy CDP 执行中的可见消息也回传,不等待最终答案", async () => {
|
|
317
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
318
|
+
const forwarded = [];
|
|
319
|
+
let polls = 0;
|
|
320
|
+
const internal = client;
|
|
321
|
+
internal.readArtifacts = async () => [];
|
|
322
|
+
internal.evaluate = async (expression) => {
|
|
323
|
+
if (expression.includes("finished:")) {
|
|
324
|
+
polls++;
|
|
325
|
+
return JSON.stringify([{ id: "new", text: polls < 3 ? "请补充视频时长" : "视频已生成", finished: polls >= 3, stopped: false }]);
|
|
326
|
+
}
|
|
327
|
+
return "null";
|
|
328
|
+
};
|
|
329
|
+
strict_1.default.deepEqual(await internal.waitForReply(new Set(), 4_000, undefined, undefined, "peer", async (text) => { forwarded.push(text); }), { text: "视频已生成" });
|
|
330
|
+
strict_1.default.deepEqual(forwarded, ["请补充视频时长"]);
|
|
331
|
+
});
|
|
332
|
+
(0, node_test_1.default)("workbuddy CDP 多条可见消息逐条回传,不只取最新一条", async () => {
|
|
333
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
334
|
+
const forwarded = [];
|
|
335
|
+
let polls = 0;
|
|
336
|
+
const internal = client;
|
|
337
|
+
internal.readArtifacts = async () => [];
|
|
338
|
+
internal.evaluate = async (expression) => {
|
|
339
|
+
if (expression.includes("finished:")) {
|
|
340
|
+
polls++;
|
|
341
|
+
return JSON.stringify([
|
|
342
|
+
{ id: "first", text: "第一条结果", finished: true, stopped: true },
|
|
343
|
+
{ id: "second", text: polls < 3 ? "第二条处理中" : "第二条结果", finished: polls >= 3, stopped: polls >= 3 },
|
|
344
|
+
]);
|
|
345
|
+
}
|
|
346
|
+
return "null";
|
|
347
|
+
};
|
|
348
|
+
strict_1.default.deepEqual(await internal.waitForReply(new Set(), 4_000, undefined, undefined, "peer", async (text) => { forwarded.push(text); }), { text: "第二条结果" });
|
|
349
|
+
strict_1.default.deepEqual(forwarded, ["第一条结果", "第二条处理中"]);
|
|
350
|
+
});
|
|
351
|
+
(0, node_test_1.default)("workbuddy CDP 只过滤思考区域,保留全部可见正文", async () => {
|
|
352
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
353
|
+
const content = { innerText: "深度思考\n内部推理\n请确认是否继续\n生成结果已保存", querySelectorAll: () => [{ innerText: "深度思考\n内部推理\n" }] };
|
|
354
|
+
const message = { getAttribute: () => "new", querySelector: (selector) => selector === "[data-assistant-content]" ? content : {} };
|
|
355
|
+
const internal = client;
|
|
356
|
+
internal.readArtifacts = async () => [];
|
|
357
|
+
internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: {
|
|
358
|
+
querySelectorAll: (selector) => selector === ".cr-agent[data-message-id]" ? [message] : [],
|
|
359
|
+
querySelector: () => null,
|
|
360
|
+
} });
|
|
361
|
+
strict_1.default.deepEqual(await internal.waitForReply(new Set(), 2_000), { text: "请确认是否继续\n生成结果已保存" });
|
|
362
|
+
});
|
|
363
|
+
(0, node_test_1.default)("workbuddy CDP 选择框不截断同轮可见正文", async () => {
|
|
364
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
365
|
+
const forwarded = [];
|
|
366
|
+
const internal = client;
|
|
367
|
+
internal.evaluate = async (expression) => expression.includes("finished:")
|
|
368
|
+
? JSON.stringify([{ id: "new", text: "已准备好,请选择项目", finished: false, stopped: false }])
|
|
369
|
+
: JSON.stringify({ prompt: "选择项目", options: ["项目甲", "项目乙"] });
|
|
370
|
+
strict_1.default.deepEqual(await internal.waitForReply(new Set(), 2_000, undefined, undefined, "peer", async (text) => { forwarded.push(text); }), { text: "选择项目\n1. 项目甲\n2. 项目乙" });
|
|
371
|
+
strict_1.default.deepEqual(forwarded, ["已准备好,请选择项目"]);
|
|
372
|
+
});
|
|
373
|
+
(0, node_test_1.default)("workbuddy CDP 重载后从当前会话恢复选择框,不重复发送任务", async () => {
|
|
374
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
375
|
+
const internal = client;
|
|
376
|
+
internal.cdp = async () => { throw new Error("不应重新输入任务"); };
|
|
377
|
+
internal.evaluate = async (expression) => expression.includes("conversation-input-area") ? JSON.stringify({ prompt: "确认开始生成视频?", options: ["确认", "拒绝"] }) : "[]";
|
|
378
|
+
strict_1.default.deepEqual(await client.complete({ task: "查看待选项", sessionId: "session-a" }), { text: "确认开始生成视频?\n1. 确认\n2. 拒绝" });
|
|
379
|
+
strict_1.default.equal(client.pendingInteractionSessionId(), "session-a");
|
|
380
|
+
});
|
|
193
381
|
(0, node_test_1.default)("workbuddy CDP 附件改变正文时拒绝发送", async () => {
|
|
194
382
|
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
195
383
|
let sent = false;
|
|
@@ -495,21 +683,34 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
495
683
|
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
496
684
|
}
|
|
497
685
|
});
|
|
498
|
-
(0, node_test_1.default)("workbuddy CDP
|
|
499
|
-
for (const [clearsMessages, changesIdentity
|
|
500
|
-
|
|
686
|
+
(0, node_test_1.default)("workbuddy CDP 新建核验新身份或空白首页,拒绝旧任务和残留草稿", async () => {
|
|
687
|
+
for (const [initialMessages, clearsMessages, changesIdentity, home, draft, succeeds] of [
|
|
688
|
+
[1, true, true, false, "", true],
|
|
689
|
+
[1, false, true, false, "", false],
|
|
690
|
+
[1, true, false, false, "", false],
|
|
691
|
+
[1, true, false, true, "", true],
|
|
692
|
+
[0, true, false, true, "", true],
|
|
693
|
+
[1, true, false, true, "未发送草稿", false],
|
|
694
|
+
]) {
|
|
695
|
+
let messages = initialMessages;
|
|
501
696
|
let conversationId = "old-conversation";
|
|
502
|
-
|
|
697
|
+
let clicked = false;
|
|
698
|
+
const button = { textContent: "新建任务", disabled: false, getAttribute: () => null, click: () => { clicked = true; if (clearsMessages)
|
|
503
699
|
messages = 0; if (changesIdentity)
|
|
504
700
|
conversationId = "new-conversation"; } };
|
|
701
|
+
const editor = {
|
|
702
|
+
innerText: home ? `\uFEFF\n今天帮你做些什么?${draft}` : draft,
|
|
703
|
+
querySelector: () => home && clicked ? { textContent: "今天帮你做些什么?@ 添加上下文,/调用技能与指令" } : null,
|
|
704
|
+
cloneNode: () => ({ textContent: draft, querySelectorAll: () => [{ remove() { } }] }),
|
|
705
|
+
};
|
|
505
706
|
const document = {
|
|
506
|
-
querySelectorAll: (selector) => selector === '.cr-agent[data-message-id]' ? Array(messages).fill({}) : selector === '[data-conversation-id]' ? [{ className: "_selected_old", getAttribute: () => conversationId }] : [button],
|
|
507
|
-
querySelector: () =>
|
|
707
|
+
querySelectorAll: (selector) => selector === '.cr-agent[data-message-id]' ? Array(messages).fill({}) : selector === '[data-conversation-id]' ? home && clicked ? [] : [{ className: "_selected_old", getAttribute: () => conversationId }] : [button],
|
|
708
|
+
querySelector: () => editor,
|
|
508
709
|
};
|
|
509
710
|
const client = new cdp_client_1.WorkbuddyCdpClient({ timeoutMs: 1 });
|
|
510
711
|
const evaluator = client;
|
|
511
712
|
evaluator.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document, location: { href: "app://workbuddy/task/old" } });
|
|
512
|
-
if (
|
|
713
|
+
if (succeeds)
|
|
513
714
|
await client.createTask();
|
|
514
715
|
else
|
|
515
716
|
await strict_1.default.rejects(() => client.createTask(), /新任务状态校验失败/);
|
|
@@ -521,7 +722,7 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
521
722
|
let messages = initialId ? 1 : 0;
|
|
522
723
|
let placeholderRemoved = false;
|
|
523
724
|
const clone = { get textContent() { return placeholderRemoved ? "\uFEFF\u200B\n" : "\uFEFF\u200B\n今天帮你做些什么?"; }, querySelectorAll: () => [{ remove: () => { placeholderRemoved = true; } }] };
|
|
524
|
-
const editor = { innerText: "\uFEFF\u200B\n今天帮你做些什么?", cloneNode: () => { placeholderRemoved = false; return clone; } };
|
|
725
|
+
const editor = { innerText: "\uFEFF\u200B\n今天帮你做些什么?", querySelector: () => conversationId === null ? {} : null, cloneNode: () => { placeholderRemoved = false; return clone; } };
|
|
525
726
|
const button = { textContent: "新建任务", disabled: false, getAttribute: () => null, click: () => { conversationId = null; messages = 0; } };
|
|
526
727
|
const document = {
|
|
527
728
|
querySelectorAll: (selector) => selector === '.cr-agent[data-message-id]' ? Array(messages).fill({}) : selector === '[data-conversation-id]' ? conversationId ? [{ className: "_selected_old", getAttribute: () => conversationId }] : [] : [button],
|
|
@@ -538,7 +739,7 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
538
739
|
const button = { textContent: "新建任务", disabled: false, getAttribute: () => null, click: () => { messages = 0; } };
|
|
539
740
|
const document = {
|
|
540
741
|
querySelectorAll: (selector) => selector === '.cr-agent[data-message-id]' ? Array(messages).fill({}) : selector === '[data-conversation-id]' ? [] : [button],
|
|
541
|
-
querySelector: () => ({ innerText: "", cloneNode: () => ({ textContent: "", querySelectorAll: () => [] }) }),
|
|
742
|
+
querySelector: () => ({ innerText: "", querySelector: () => null, cloneNode: () => ({ textContent: "", querySelectorAll: () => [] }) }),
|
|
542
743
|
};
|
|
543
744
|
const client = new cdp_client_1.WorkbuddyCdpClient({ timeoutMs: 1 });
|
|
544
745
|
const internal = client;
|
|
@@ -643,6 +844,36 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
643
844
|
await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
|
|
644
845
|
}
|
|
645
846
|
});
|
|
847
|
+
(0, node_test_1.default)("workbuddy 重载只恢复仍指向同一桌面任务的绑定", async () => {
|
|
848
|
+
const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-active-session-"));
|
|
849
|
+
let creates = 0;
|
|
850
|
+
let desktopId = "desktop-a";
|
|
851
|
+
const client = {
|
|
852
|
+
connect: async () => undefined, close() { }, connected: () => true,
|
|
853
|
+
conversationId: async () => desktopId,
|
|
854
|
+
createTask: async () => { creates++; }, direct: async (task) => task,
|
|
855
|
+
};
|
|
856
|
+
const context = { dataDir, config: { transport: "cdp" }, logger: { info() { }, warn() { }, error() { } } };
|
|
857
|
+
try {
|
|
858
|
+
const first = new workbuddy_1.WorkbuddyPlugin({ client });
|
|
859
|
+
await first.activate(context);
|
|
860
|
+
await first.handle({ task: "首次", sessionId: "session-a" });
|
|
861
|
+
await first.deactivate("reload");
|
|
862
|
+
const second = new workbuddy_1.WorkbuddyPlugin({ client });
|
|
863
|
+
await second.activate(context);
|
|
864
|
+
strict_1.default.equal((await second.handle({ task: "继续", sessionId: "session-a" })).text, "继续");
|
|
865
|
+
strict_1.default.equal(creates, 1);
|
|
866
|
+
await second.deactivate("reload");
|
|
867
|
+
desktopId = "desktop-b";
|
|
868
|
+
const third = new workbuddy_1.WorkbuddyPlugin({ client });
|
|
869
|
+
await third.activate(context);
|
|
870
|
+
await strict_1.default.rejects(() => third.handle({ task: "不应串会话", sessionId: "session-a" }), /不支持恢复旧 WorkBuddy 会话/);
|
|
871
|
+
await third.deactivate("stop");
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
|
|
875
|
+
}
|
|
876
|
+
});
|
|
646
877
|
(0, node_test_1.default)("workbuddy 新建失败的会话 ID 重启后仍禁止重试", async () => {
|
|
647
878
|
const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-failed-session-"));
|
|
648
879
|
let creates = 0;
|
package/package.json
CHANGED
|
@@ -4,6 +4,17 @@ exports.WorkbuddyCdpClient = exports.assistantSelector = exports.editorSelector
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const attachments_1 = require("../../attachments");
|
|
6
6
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
7
|
+
const interactionOptionSelector = '.conversation-input-area [role="dialog"] button,.pending-sandbox-panel button,button[class*="conversation-"][class*="__option"]';
|
|
8
|
+
function interactionText(value) { return value.normalize("NFKC").replace(/[\uFEFF\u200B]/g, "").trim(); }
|
|
9
|
+
function formatInteraction(value) { return [value.prompt, ...value.options.map((option, index) => `${index + 1}. ${option}`)].filter(Boolean).join("\n"); }
|
|
10
|
+
function interactionChoice(value, options) {
|
|
11
|
+
const input = interactionText(value).replace(/^[第\s]*(\d+)[、.。::)]?\s*$/, "$1");
|
|
12
|
+
const number = Number(input);
|
|
13
|
+
if (Number.isInteger(number) && number >= 1 && number <= options.length && String(number) === input)
|
|
14
|
+
return number - 1;
|
|
15
|
+
const index = options.findIndex((option) => interactionText(option) === input);
|
|
16
|
+
return index >= 0 ? index : undefined;
|
|
17
|
+
}
|
|
7
18
|
exports.editorSelector = '[data-slate-editor][contenteditable="true"]';
|
|
8
19
|
exports.assistantSelector = '.cr-agent[data-message-id] [data-assistant-content]';
|
|
9
20
|
class WorkbuddyCdpClient {
|
|
@@ -11,11 +22,18 @@ class WorkbuddyCdpClient {
|
|
|
11
22
|
socket;
|
|
12
23
|
sequence = 0;
|
|
13
24
|
pending = new Map();
|
|
25
|
+
pendingInteraction;
|
|
26
|
+
pendingInteractionCleanup;
|
|
14
27
|
port;
|
|
15
28
|
timeoutMs;
|
|
16
29
|
fetchImpl;
|
|
17
30
|
constructor(options = {}) { this.port = options.port ?? Number(process.env.WORKBUDDY_CDP_PORT ?? "9229"); this.timeoutMs = options.timeoutMs ?? 30_000; this.fetchImpl = options.fetchImpl ?? fetch; }
|
|
18
31
|
connected() { return Boolean(this.socket); }
|
|
32
|
+
pendingInteractionSessionId() { return this.pendingInteraction?.sessionId; }
|
|
33
|
+
async conversationId() {
|
|
34
|
+
const value = await this.evaluate(`([...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_')))?.getAttribute('data-conversation-id')`);
|
|
35
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
36
|
+
}
|
|
19
37
|
async connect() {
|
|
20
38
|
if (this.socket)
|
|
21
39
|
return;
|
|
@@ -39,18 +57,12 @@ class WorkbuddyCdpClient {
|
|
|
39
57
|
close() { this.socket?.close(); this.socket = undefined; for (const pending of this.pending.values())
|
|
40
58
|
pending.reject(new Error("WorkBuddy CDP 已关闭")); this.pending.clear(); }
|
|
41
59
|
async createTask() {
|
|
42
|
-
const snapshot = `(() => {
|
|
43
|
-
const selected = [...document.querySelectorAll('[data-conversation-id]')].find((item) => String(item.className).includes('_selected_'));
|
|
44
|
-
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)})?.cloneNode(true);
|
|
45
|
-
for (const placeholder of editor?.querySelectorAll('[data-slate-placeholder]') ?? []) placeholder.remove();
|
|
46
|
-
return { url: location.href.split(/[?#]/, 1)[0], conversationId: selected?.getAttribute('data-conversation-id') ?? null, messages: document.querySelectorAll('.cr-agent[data-message-id]').length, editor: editor?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim() ?? null };
|
|
47
|
-
})()`;
|
|
60
|
+
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"]')) }; })()`;
|
|
48
61
|
const before = await this.evaluate(snapshot);
|
|
49
62
|
if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"]')].find((item) => item.textContent?.trim() === '新建任务'); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`))
|
|
50
63
|
throw new Error("WorkBuddy 新建任务入口未找到或不可用");
|
|
51
|
-
//
|
|
52
|
-
const
|
|
53
|
-
await this.waitFor(`(() => { const state = ${snapshot}; return state.editor === '' && state.messages === 0 && (state.conversationId !== ${JSON.stringify(before.conversationId)} || state.url !== ${JSON.stringify(before.url)} || (${wasLanding} && state.conversationId === null)); })()`, "WorkBuddy 新任务状态校验失败");
|
|
64
|
+
// WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
|
|
65
|
+
await this.waitFor(`(() => { const state = ${snapshot}; const blank = state.editor !== null && state.editor.trim() === '' && state.messages === 0; return blank && ((state.conversationId && state.conversationId !== ${JSON.stringify(before.conversationId)}) || state.url !== ${JSON.stringify(before.url)} || (state.home && !state.selected)); })()`, "WorkBuddy 新任务状态校验失败");
|
|
54
66
|
}
|
|
55
67
|
async direct(text, timeoutMs = 240_000, expert, newTask = false) {
|
|
56
68
|
return (await this.complete({ task: text, timeoutMs }, expert, newTask)).text;
|
|
@@ -59,6 +71,16 @@ class WorkbuddyCdpClient {
|
|
|
59
71
|
(0, attachments_1.validateAttachments)(request.attachments);
|
|
60
72
|
const text = request.task;
|
|
61
73
|
const timeoutMs = request.timeoutMs ?? 240_000;
|
|
74
|
+
if (!this.pendingInteraction && request.sessionId && !newTask && !expert) {
|
|
75
|
+
const interaction = await this.readInteraction();
|
|
76
|
+
if (interaction)
|
|
77
|
+
this.pendingInteraction = { ...interaction, sessionId: request.sessionId };
|
|
78
|
+
}
|
|
79
|
+
if (this.pendingInteraction) {
|
|
80
|
+
if (this.pendingInteraction.sessionId && this.pendingInteraction.sessionId !== request.sessionId)
|
|
81
|
+
throw new Error("WorkBuddy 正在等待另一个会话的选择");
|
|
82
|
+
return this.continueInteraction(text, timeoutMs, request.onMessage);
|
|
83
|
+
}
|
|
62
84
|
if (expert)
|
|
63
85
|
await this.selectExpert(expert);
|
|
64
86
|
else if (newTask)
|
|
@@ -91,28 +113,53 @@ class WorkbuddyCdpClient {
|
|
|
91
113
|
await this.waitFor(`(() => { const button = document.querySelector('button[aria-label="发送"]'); return Boolean(button && !button.disabled); })()`, "WorkBuddy 发送按钮未就绪");
|
|
92
114
|
if (!await this.evaluate(`(() => { const button = document.querySelector('button[aria-label="发送"]'); if (!button || button.disabled) return false; button.click(); return true; })()`))
|
|
93
115
|
throw new Error("WorkBuddy 发送按钮不可用");
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage);
|
|
117
|
+
if (this.pendingInteraction && temporaryPaths.length)
|
|
118
|
+
this.pendingInteractionCleanup = temporaryPaths;
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
failure = error;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (temporaryPaths.length && this.pendingInteractionCleanup !== temporaryPaths) {
|
|
127
|
+
try {
|
|
128
|
+
await this.cleanupTemporaryFiles(temporaryPaths);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (failure !== undefined)
|
|
132
|
+
throw new AggregateError([failure, error], `${failure instanceof Error ? failure.message : String(failure)};临时附件清理失败`, { cause: failure });
|
|
133
|
+
throw error;
|
|
106
134
|
}
|
|
107
135
|
}
|
|
108
|
-
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async continueInteraction(answer, timeoutMs, onMessage) {
|
|
139
|
+
const interaction = this.pendingInteraction;
|
|
140
|
+
const index = interactionChoice(answer, interaction.options);
|
|
141
|
+
if (index === undefined)
|
|
142
|
+
return { text: formatInteraction(interaction) };
|
|
143
|
+
const temporaryPaths = this.pendingInteractionCleanup;
|
|
144
|
+
const before = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => element.getAttribute('data-message-id'))) `)));
|
|
145
|
+
const beforeIds = new Set((Array.isArray(before) ? before : []).filter((id) => typeof id === "string"));
|
|
146
|
+
if (!await this.evaluate(`(() => { const options = [...document.querySelectorAll(${JSON.stringify(interactionOptionSelector)})]; const option = options[${index}]; if (!option || option.disabled || option.getAttribute('aria-disabled') === 'true') return false; option.click(); return true; })()`))
|
|
147
|
+
throw new Error("WorkBuddy 选择项不可用");
|
|
148
|
+
this.pendingInteraction = undefined;
|
|
149
|
+
let failure;
|
|
150
|
+
try {
|
|
151
|
+
const response = await this.waitForReply(beforeIds, timeoutMs, interaction.messageId, interaction.initialText, interaction.sessionId, onMessage);
|
|
152
|
+
if (!this.pendingInteraction)
|
|
153
|
+
this.pendingInteractionCleanup = undefined;
|
|
154
|
+
return response;
|
|
109
155
|
}
|
|
110
156
|
catch (error) {
|
|
111
157
|
failure = error;
|
|
112
158
|
throw error;
|
|
113
159
|
}
|
|
114
160
|
finally {
|
|
115
|
-
if (temporaryPaths.
|
|
161
|
+
if (temporaryPaths && !this.pendingInteraction) {
|
|
162
|
+
this.pendingInteractionCleanup = undefined;
|
|
116
163
|
try {
|
|
117
164
|
await this.cleanupTemporaryFiles(temporaryPaths);
|
|
118
165
|
}
|
|
@@ -124,6 +171,66 @@ class WorkbuddyCdpClient {
|
|
|
124
171
|
}
|
|
125
172
|
}
|
|
126
173
|
}
|
|
174
|
+
async waitForReply(beforeIds, timeoutMs, changedId, initialText, sessionId, onMessage) {
|
|
175
|
+
const deadline = Date.now() + timeoutMs;
|
|
176
|
+
const previousText = new Map();
|
|
177
|
+
const stablePolls = new Map();
|
|
178
|
+
const forwarded = new Set();
|
|
179
|
+
while (Date.now() < deadline) {
|
|
180
|
+
await sleep(1_000);
|
|
181
|
+
const current = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => { const content = element.querySelector('[data-assistant-content]'); let text = content?.innerText?.trim() || ''; for (const thought of content?.querySelectorAll?.('.cr-reasoning') || []) text = text.replace(thought.innerText || '', ''); text = text.trim(); const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]')); const stopped = Boolean(text) && !document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]'); return { id: element.getAttribute('data-message-id'), text, finished: finished, stopped: stopped }; }))`)));
|
|
182
|
+
const interaction = await this.readInteraction();
|
|
183
|
+
const candidates = current.filter((item) => item.id && (!beforeIds.has(item.id) || item.id === changedId && item.text !== initialText));
|
|
184
|
+
for (const item of candidates) {
|
|
185
|
+
const id = item.id;
|
|
186
|
+
const text = item.text?.trim() ?? "";
|
|
187
|
+
const count = text && text === previousText.get(id) ? (stablePolls.get(id) ?? 0) + 1 : text ? 1 : 0;
|
|
188
|
+
previousText.set(id, text);
|
|
189
|
+
stablePolls.set(id, count);
|
|
190
|
+
}
|
|
191
|
+
if (interaction) {
|
|
192
|
+
const body = candidates.map((item) => item.text?.trim()).filter((text) => Boolean(text && !forwarded.has(text)));
|
|
193
|
+
if (onMessage)
|
|
194
|
+
for (const text of body) {
|
|
195
|
+
await onMessage(text);
|
|
196
|
+
forwarded.add(text);
|
|
197
|
+
}
|
|
198
|
+
const latest = candidates.at(-1) ?? current.at(-1);
|
|
199
|
+
this.pendingInteraction = { ...interaction, messageId: interaction.messageId ?? latest?.id ?? undefined, initialText: interaction.initialText ?? latest?.text ?? "", sessionId };
|
|
200
|
+
return { text: [...(onMessage ? [] : body), formatInteraction(this.pendingInteraction)].join("\n\n") };
|
|
201
|
+
}
|
|
202
|
+
const latest = candidates.at(-1);
|
|
203
|
+
const completed = candidates.filter((item) => item.finished || Boolean(item.stopped && (stablePolls.get(item.id) ?? 0) >= 2));
|
|
204
|
+
const reply = candidates.map((item) => item.text?.trim()).filter((text) => text && !forwarded.has(text)).join("\n\n");
|
|
205
|
+
const latestComplete = latest && (latest.finished || Boolean(latest.stopped && (stablePolls.get(latest.id) ?? 0) >= 2));
|
|
206
|
+
if (latestComplete && completed.length) {
|
|
207
|
+
const attachments = await this.readArtifacts(new Set(completed.map((item) => item.id)));
|
|
208
|
+
if (attachments.length)
|
|
209
|
+
return { text: reply ?? "", attachments };
|
|
210
|
+
if (reply || forwarded.size)
|
|
211
|
+
return { text: reply };
|
|
212
|
+
}
|
|
213
|
+
if (onMessage) {
|
|
214
|
+
for (const item of candidates) {
|
|
215
|
+
const text = item.text?.trim();
|
|
216
|
+
if (text && (stablePolls.get(item.id) ?? 0) >= 2 && !forwarded.has(text)) {
|
|
217
|
+
await onMessage(text);
|
|
218
|
+
forwarded.add(text);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw new Error("WorkBuddy UI 回复超时");
|
|
224
|
+
}
|
|
225
|
+
async readInteraction() {
|
|
226
|
+
const raw = await this.evaluate(`JSON.stringify((() => { const nodes = [...document.querySelectorAll(${JSON.stringify(interactionOptionSelector)})]; if (!nodes.length) return null; const root = nodes[0].closest('[role="dialog"],.pending-sandbox-panel,.conversation-high-credit-approval') || nodes[0].parentElement; const rawOptions = nodes.map(node => (node.innerText || node.textContent)?.trim() || '').filter(Boolean); let prompt = root?.innerText?.trim() || ''; for (const option of rawOptions) prompt = prompt.replace(option, ''); return { prompt: prompt.replace(/\\n{2,}/g, '\\n').trim(), options: rawOptions.map(option => option.replace(/^\\d+\\s*[.、::)]?\\s*/, '').trim()), messageId: nodes[0].closest('.cr-agent[data-message-id]')?.getAttribute('data-message-id') || undefined, initialText: nodes[0].closest('.cr-agent[data-message-id]')?.querySelector('[data-assistant-content]')?.innerText?.trim() || '' }; })())`);
|
|
227
|
+
if (!raw || raw === "null" || raw === "undefined")
|
|
228
|
+
return undefined;
|
|
229
|
+
const value = JSON.parse(String(raw));
|
|
230
|
+
if (!Array.isArray(value.options) || value.options.length === 0 || value.options.some((option) => typeof option !== "string" || !option.trim()))
|
|
231
|
+
return undefined;
|
|
232
|
+
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 } : {}) };
|
|
233
|
+
}
|
|
127
234
|
async injectAttachments(items, temporaryPaths = []) {
|
|
128
235
|
const before = await this.evaluate(`JSON.stringify([...document.querySelector(${JSON.stringify(exports.editorSelector)}).querySelectorAll('[class*="_fileTag"]')].map(element => element.textContent?.trim()).filter(Boolean))`);
|
|
129
236
|
if (JSON.parse(String(before)).length)
|
|
@@ -13,7 +13,7 @@ const plugin_1 = require("../../plugin");
|
|
|
13
13
|
const cdp_client_1 = require("./cdp-client");
|
|
14
14
|
const cli_client_1 = require("./cli-client");
|
|
15
15
|
exports.workbuddyManifest = {
|
|
16
|
-
id: "workbuddy", version: "1.1.
|
|
16
|
+
id: "workbuddy", version: "1.1.11", displayName: "WorkBuddy", type: "agent",
|
|
17
17
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
18
18
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
19
19
|
};
|
|
@@ -41,6 +41,7 @@ class WorkbuddyPlugin {
|
|
|
41
41
|
queue = Promise.resolve();
|
|
42
42
|
pending = 0;
|
|
43
43
|
activeSessionId;
|
|
44
|
+
activeDesktopConversationId;
|
|
44
45
|
desktopSessionValid = true;
|
|
45
46
|
knownSessions = new Set();
|
|
46
47
|
sessionDataDir;
|
|
@@ -63,14 +64,18 @@ class WorkbuddyPlugin {
|
|
|
63
64
|
this.cliPath = (0, cli_client_1.findCli)(typeof config.cliPath === "string" ? config.cliPath : undefined);
|
|
64
65
|
this.failure = undefined;
|
|
65
66
|
this.activeSessionId = undefined;
|
|
67
|
+
this.activeDesktopConversationId = undefined;
|
|
66
68
|
this.activeExpert = undefined;
|
|
67
69
|
this.expertSession = false;
|
|
68
70
|
this.desktopSessionValid = true;
|
|
69
71
|
this.knownSessions.clear();
|
|
70
72
|
this.sessionDataDir = context.dataDir;
|
|
73
|
+
let savedBinding;
|
|
71
74
|
if (this.sessionDataDir) {
|
|
72
|
-
|
|
75
|
+
const saved = await this.loadKnownSessions(this.sessionDataDir);
|
|
76
|
+
for (const sessionId of saved.sessionIds)
|
|
73
77
|
this.knownSessions.add(sessionId);
|
|
78
|
+
savedBinding = saved.active;
|
|
74
79
|
}
|
|
75
80
|
this.sessionOperations = [];
|
|
76
81
|
if (this.transport === "cli") {
|
|
@@ -91,6 +96,11 @@ class WorkbuddyPlugin {
|
|
|
91
96
|
throw error;
|
|
92
97
|
}
|
|
93
98
|
this.sessionOperations = (this.transport === "cdp" || !this.cliPath) && this.client?.createTask ? ["new"] : [];
|
|
99
|
+
if (savedBinding && this.sessionOperations.length && await this.client?.conversationId?.() === savedBinding.conversationId) {
|
|
100
|
+
this.activeSessionId = savedBinding.sessionId;
|
|
101
|
+
this.activeDesktopConversationId = savedBinding.conversationId;
|
|
102
|
+
this.activeExpert = savedBinding.expert;
|
|
103
|
+
}
|
|
94
104
|
}
|
|
95
105
|
async deactivate(_reason) {
|
|
96
106
|
await this.queue;
|
|
@@ -111,7 +121,17 @@ class WorkbuddyPlugin {
|
|
|
111
121
|
if (!request.task.trim())
|
|
112
122
|
throw new Error("消息内容不能为空");
|
|
113
123
|
(0, attachments_1.validateAttachments)(request.attachments);
|
|
114
|
-
const run = this.enqueue(() =>
|
|
124
|
+
const run = this.enqueue(async () => {
|
|
125
|
+
try {
|
|
126
|
+
return await this.complete(request);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (this.activeSessionId && this.desktopSessionValid && this.client?.conversationId) {
|
|
130
|
+
this.activeDesktopConversationId = await this.client.conversationId();
|
|
131
|
+
await this.persistKnownSessions();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
115
135
|
const response = await run;
|
|
116
136
|
return { ...response, sessionId: request.sessionId ?? response.sessionId };
|
|
117
137
|
}
|
|
@@ -141,6 +161,9 @@ class WorkbuddyPlugin {
|
|
|
141
161
|
const hasAttachments = Array.isArray(request.attachments) && request.attachments.length > 0;
|
|
142
162
|
if (hasAttachments && !this.attachmentInput)
|
|
143
163
|
throw new Error("WorkBuddy 当前传输不支持附件输入");
|
|
164
|
+
const pendingInteractionSessionId = this.client?.pendingInteractionSessionId?.();
|
|
165
|
+
if (!usesCli && pendingInteractionSessionId && pendingInteractionSessionId !== request.sessionId)
|
|
166
|
+
throw new Error("WorkBuddy 正在等待另一个会话的选择");
|
|
144
167
|
if (usesCli && request.sessionId)
|
|
145
168
|
throw new Error("CodeBuddy CLI 会话隔离未验证,不能处理带 sessionId 的请求");
|
|
146
169
|
if (!usesCli && !request.sessionId && this.activeSessionId)
|
|
@@ -196,8 +219,13 @@ class WorkbuddyPlugin {
|
|
|
196
219
|
throw new Error("WorkBuddy CDP 未连接");
|
|
197
220
|
if (!this.client.createTask)
|
|
198
221
|
throw new Error("WorkBuddy 客户端不支持新建会话");
|
|
199
|
-
if (this.activeSessionId === sessionId && this.desktopSessionValid)
|
|
222
|
+
if (this.activeSessionId === sessionId && this.desktopSessionValid) {
|
|
223
|
+
if (this.activeDesktopConversationId && await this.client.conversationId?.() !== this.activeDesktopConversationId) {
|
|
224
|
+
this.desktopSessionValid = false;
|
|
225
|
+
throw new Error("WorkBuddy 桌面任务已切换,请先创建新会话");
|
|
226
|
+
}
|
|
200
227
|
return;
|
|
228
|
+
}
|
|
201
229
|
if (this.knownSessions.has(sessionId))
|
|
202
230
|
throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
|
|
203
231
|
await this.createSession(sessionId);
|
|
@@ -205,6 +233,8 @@ class WorkbuddyPlugin {
|
|
|
205
233
|
async createSession(sessionId, name) {
|
|
206
234
|
// 点击失败时桌面可能已经切换,保留逻辑绑定但禁止向未知任务续发。
|
|
207
235
|
this.desktopSessionValid = false;
|
|
236
|
+
this.activeSessionId = undefined;
|
|
237
|
+
this.activeDesktopConversationId = undefined;
|
|
208
238
|
this.knownSessions.add(sessionId);
|
|
209
239
|
await this.persistKnownSessions();
|
|
210
240
|
await this.client.createTask(name);
|
|
@@ -221,11 +251,14 @@ class WorkbuddyPlugin {
|
|
|
221
251
|
const sessionIds = parsed.sessionIds;
|
|
222
252
|
if (!Array.isArray(sessionIds) || sessionIds.some((sessionId) => typeof sessionId !== "string" || !sessionId.trim()))
|
|
223
253
|
throw new Error("sessionIds 无效");
|
|
224
|
-
|
|
254
|
+
const active = parsed.active;
|
|
255
|
+
if (active !== undefined && (!active || typeof active.sessionId !== "string" || !sessionIds.includes(active.sessionId) || typeof active.conversationId !== "string" || !active.conversationId.trim() || (active.expert !== undefined && active.expert !== null && typeof active.expert !== "string")))
|
|
256
|
+
throw new Error("活动会话绑定无效");
|
|
257
|
+
return { sessionIds: new Set(sessionIds), ...(active ? { active: active } : {}) };
|
|
225
258
|
}
|
|
226
259
|
catch (error) {
|
|
227
260
|
if (error.code === "ENOENT")
|
|
228
|
-
return new Set();
|
|
261
|
+
return { sessionIds: new Set() };
|
|
229
262
|
if (error instanceof SyntaxError || error instanceof Error)
|
|
230
263
|
throw new Error(`WorkBuddy 已知会话记录无效:${error.message}`);
|
|
231
264
|
throw new Error("WorkBuddy 已知会话记录无效");
|
|
@@ -237,7 +270,8 @@ class WorkbuddyPlugin {
|
|
|
237
270
|
const write = this.sessionWrites.catch(() => { }).then(async () => {
|
|
238
271
|
await (0, promises_1.mkdir)(this.sessionDataDir, { recursive: true });
|
|
239
272
|
const file = (0, node_path_1.join)(this.sessionDataDir, "known-sessions.json");
|
|
240
|
-
|
|
273
|
+
const active = this.desktopSessionValid && this.activeSessionId && this.activeDesktopConversationId ? { sessionId: this.activeSessionId, conversationId: this.activeDesktopConversationId, expert: this.activeExpert } : undefined;
|
|
274
|
+
await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify({ sessionIds: [...this.knownSessions].sort(), active })}\n`, { mode: 0o600 });
|
|
241
275
|
await (0, promises_1.rename)(`${file}.tmp`, file);
|
|
242
276
|
});
|
|
243
277
|
this.sessionWrites = write;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "workbuddy",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"type": "agent",
|
|
5
|
-
"displayName": "WorkBuddy
|
|
5
|
+
"displayName": "WorkBuddy",
|
|
6
6
|
"entry": "dist/src-ts/plugins/workbuddy/index.js",
|
|
7
7
|
"apiVersion": "1",
|
|
8
8
|
"process": "in-process",
|
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
"health"
|
|
12
12
|
],
|
|
13
13
|
"configSchema": "config.schema.json",
|
|
14
|
-
"clientDownloadUrl": "https://
|
|
14
|
+
"clientDownloadUrl": "https://www.workbuddy.ai/"
|
|
15
15
|
}
|