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 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 response = await this.sessions.run(message, target, (sessionId) => this.handle(target, { task, sessionId, ...(message.attachments ? { attachments: message.attachments } : {}) }), control);
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 message.reply(response.text);
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
- // CommonJS is the platform's compiled format; evict the module cache for reload.
637
+ // 同时刷新插件内部依赖,避免升级后仍运行旧版 CDP 客户端。
630
638
  const req = (0, node_module_1.createRequire)(file);
631
639
  const resolved = req.resolve(file);
632
- delete req.cache[resolved];
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
- // 落地页在首条消息前没有任务 ID;旧任务 ID 消失或启动时已经空白均可确认落地状态。
52
- const wasLanding = before.conversationId === null && before.messages === 0 && before.editor === "";
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 deadline = Date.now() + timeoutMs;
95
- while (Date.now() < deadline) {
96
- await sleep(1_000);
97
- const current = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => ({ id: element.getAttribute('data-message-id'), text: element.querySelector('[data-assistant-content]')?.innerText, finished: Boolean(element.querySelector('[data-testid="conversation-finished-footer"]')) })))`)));
98
- const completed = current.filter((item) => item.id && !beforeIds.has(item.id) && item.finished);
99
- const reply = completed.filter((item) => item.text?.trim()).at(-1)?.text?.trim();
100
- if (completed.length) {
101
- const attachments = await this.readArtifacts(new Set(completed.map((item) => item.id)));
102
- if (attachments.length)
103
- return { text: reply ?? "", attachments };
104
- if (reply)
105
- return { text: reply };
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
- throw new Error("WorkBuddy UI 回复超时");
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.length) {
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.6", displayName: "WorkBuddy", type: "agent",
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
- for (const sessionId of await this.loadKnownSessions(this.sessionDataDir))
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(() => this.complete(request));
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
- return new Set(sessionIds);
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
- await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify({ sessionIds: [...this.knownSessions].sort() })}\n`, { mode: 0o600 });
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 新建必须同时核验新任务身份和空白界面", async () => {
499
- for (const [clearsMessages, changesIdentity] of [[true, true], [false, true], [true, false]]) {
500
- let messages = 1;
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
- const button = { textContent: "新建任务", disabled: false, getAttribute: () => null, click: () => { if (clearsMessages)
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: () => ({ innerText: "", cloneNode: () => ({ textContent: "", querySelectorAll: () => [] }) }),
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 (clearsMessages && changesIdentity)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -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
- // 落地页在首条消息前没有任务 ID;旧任务 ID 消失或启动时已经空白均可确认落地状态。
52
- const wasLanding = before.conversationId === null && before.messages === 0 && before.editor === "";
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 deadline = Date.now() + timeoutMs;
95
- while (Date.now() < deadline) {
96
- await sleep(1_000);
97
- const current = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => ({ id: element.getAttribute('data-message-id'), text: element.querySelector('[data-assistant-content]')?.innerText, finished: Boolean(element.querySelector('[data-testid="conversation-finished-footer"]')) })))`)));
98
- const completed = current.filter((item) => item.id && !beforeIds.has(item.id) && item.finished);
99
- const reply = completed.filter((item) => item.text?.trim()).at(-1)?.text?.trim();
100
- if (completed.length) {
101
- const attachments = await this.readArtifacts(new Set(completed.map((item) => item.id)));
102
- if (attachments.length)
103
- return { text: reply ?? "", attachments };
104
- if (reply)
105
- return { text: reply };
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
- throw new Error("WorkBuddy UI 回复超时");
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.length) {
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.6", displayName: "WorkBuddy", type: "agent",
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
- for (const sessionId of await this.loadKnownSessions(this.sessionDataDir))
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(() => this.complete(request));
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
- return new Set(sessionIds);
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
- await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify({ sessionIds: [...this.knownSessions].sort() })}\n`, { mode: 0o600 });
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,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-workbuddy",
3
- "version": "1.1.6",
3
+ "version": "1.1.11",
4
4
  "description": "agent-bridge 的 CodeBuddy CLI 插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "workbuddy",
3
- "version": "1.1.6",
3
+ "version": "1.1.11",
4
4
  "type": "agent",
5
- "displayName": "WorkBuddy(CodeBuddy CLI)",
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://copilot.tencent.com/"
14
+ "clientDownloadUrl": "https://www.workbuddy.ai/"
15
15
  }