mu-agent-bridge 1.0.34 → 1.0.35

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.
Files changed (65) hide show
  1. package/README.md +2 -2
  2. package/dist/src-ts/channel-sessions.js +3 -3
  3. package/dist/src-ts/dsh-agent.js +141 -46
  4. package/dist/src-ts/headless-account.patch.yml +8 -1
  5. package/dist/src-ts/plugin-manager.js +22 -3
  6. package/dist/src-ts/plugins/aun/attachments.js +44 -13
  7. package/dist/src-ts/plugins/aun/index.js +5 -6
  8. package/dist/src-ts/plugins/chatgpt/cdp-client.js +270 -9
  9. package/dist/src-ts/plugins/chatgpt/index.js +6 -4
  10. package/dist/src-ts/plugins/desktop-launch.js +19 -0
  11. package/dist/src-ts/plugins/doubao/attachments.js +42 -0
  12. package/dist/src-ts/plugins/doubao/cdp-client.js +302 -45
  13. package/dist/src-ts/plugins/doubao/index.js +37 -24
  14. package/dist/src-ts/plugins/dsh/index.js +3 -1
  15. package/dist/src-ts/plugins/qwenwork/cdp-client.js +263 -10
  16. package/dist/src-ts/plugins/qwenwork/index.js +31 -3
  17. package/dist/src-ts/plugins/workbuddy/cdp-client.js +9 -2
  18. package/dist/src-ts/plugins/workbuddy/index.js +3 -3
  19. package/dist/tests-ts/aun-attachments.test.js +41 -3
  20. package/dist/tests-ts/aun-plugin.test.js +28 -5
  21. package/dist/tests-ts/bundled-package.test.js +3 -1
  22. package/dist/tests-ts/channel-sessions.test.js +22 -0
  23. package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
  24. package/dist/tests-ts/desktop-launch.test.js +41 -0
  25. package/dist/tests-ts/doubao-plugin.test.js +428 -2
  26. package/dist/tests-ts/dsh-agent.test.js +199 -0
  27. package/dist/tests-ts/plugin-manager.test.js +100 -1
  28. package/dist/tests-ts/qwenwork-images.test.js +166 -0
  29. package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
  30. package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
  31. package/package.json +1 -1
  32. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
  33. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
  34. package/plugins/aun/package.json +1 -1
  35. package/plugins/aun/plugin.json +1 -1
  36. package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
  37. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +270 -9
  38. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +6 -4
  39. package/plugins/chatgpt/package.json +1 -1
  40. package/plugins/chatgpt/plugin.json +1 -1
  41. package/plugins/doubao/config.schema.json +3 -0
  42. package/plugins/doubao/dist/src-ts/attachments.js +62 -0
  43. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
  44. package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
  45. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +302 -45
  46. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +37 -24
  47. package/plugins/doubao/package.json +1 -1
  48. package/plugins/doubao/plugin.json +1 -1
  49. package/plugins/dsh/dist/src-ts/attachments.js +62 -0
  50. package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
  51. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
  52. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +3 -1
  53. package/plugins/dsh/package.json +5 -2
  54. package/plugins/dsh/plugin.json +1 -1
  55. package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
  56. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
  57. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +263 -10
  58. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +31 -3
  59. package/plugins/qwenwork/package.json +1 -1
  60. package/plugins/qwenwork/plugin.json +1 -1
  61. package/plugins/workbuddy/config.schema.json +1 -1
  62. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +9 -2
  63. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +3 -3
  64. package/plugins/workbuddy/package.json +1 -1
  65. package/plugins/workbuddy/plugin.json +1 -1
@@ -1,9 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChatgptCdpClient = exports.userBubbleSelector = exports.turnSelector = exports.editorSelector = void 0;
4
+ exports.chatgptAttachmentPath = chatgptAttachmentPath;
4
5
  const node_child_process_1 = require("node:child_process");
6
+ const node_crypto_1 = require("node:crypto");
7
+ const attachments_1 = require("../../attachments");
5
8
  const session_store_1 = require("./session-store");
6
9
  const cdp_draft_1 = require("../cdp-draft");
10
+ function chatgptAttachmentPath(href, outputDirectory) {
11
+ let path;
12
+ try {
13
+ if (decodeURIComponent(href).split(/[\\/]/).some(part => part === "." || part === ".."))
14
+ return;
15
+ if (href.startsWith("file://")) {
16
+ const url = new URL(href);
17
+ if (url.hostname || url.search || url.hash || url.username || url.password)
18
+ return;
19
+ path = decodeURIComponent(url.pathname);
20
+ }
21
+ else if (href.startsWith("sandbox:/"))
22
+ path = decodeURIComponent(href.slice("sandbox:".length));
23
+ else if (href.startsWith("/") || /^[a-z]:\//i.test(href))
24
+ path = decodeURIComponent(href);
25
+ else
26
+ return;
27
+ }
28
+ catch {
29
+ return;
30
+ }
31
+ if (/[%?#\u0000-\u001f\u007f]/.test(path) || path.startsWith("//") || path.includes("\\"))
32
+ return;
33
+ if (path.split("/").some(part => part === "." || part === ".."))
34
+ return;
35
+ const normalized = /^\/[a-z]:\//i.test(path) ? path.slice(1) : path;
36
+ const unixRoot = `/tmp/${outputDirectory}/`;
37
+ const windowsRoot = normalized.match(/^[a-z]:\/Users\/[^/]+\/AppData\/Local\/Temp\//i)?.[0];
38
+ const prefix = normalized.startsWith(unixRoot) ? unixRoot : windowsRoot && normalized.startsWith(`${windowsRoot}${outputDirectory}/`) ? `${windowsRoot}${outputDirectory}/` : undefined;
39
+ if (!prefix || normalized.slice(prefix.length).split("/").some(part => !part || part.startsWith(".")))
40
+ return;
41
+ return normalized;
42
+ }
7
43
  exports.editorSelector = '[data-codex-composer="true"][contenteditable="true"]';
8
44
  exports.turnSelector = '[data-chatgpt-conversation-turn="true"]';
9
45
  exports.userBubbleSelector = "[data-user-message-bubble]";
@@ -16,6 +52,7 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
52
  const sidebarSelector = "[data-app-action-sidebar-thread-id]";
17
53
  const newSessionSelector = 'button,[role="button"]';
18
54
  class ChatgptCdpClient {
55
+ attachmentInput = true;
19
56
  socket;
20
57
  tunnel;
21
58
  sequence = 0;
@@ -190,15 +227,55 @@ class ChatgptCdpClient {
190
227
  this.tunnel = undefined;
191
228
  this.rejectPending(new Error("ChatGPT CDP 已关闭"));
192
229
  }
193
- async direct(text, timeoutMs = 240_000) {
230
+ async direct(text, timeoutMs = 240_000, attachments) {
231
+ (0, attachments_1.validateAttachments)(attachments);
232
+ const slot = `__agentBridgeChatgpt_${(0, node_crypto_1.randomUUID)().replaceAll("-", "")}`;
233
+ const outputDirectory = `agent-bridge-chatgpt-${(0, node_crypto_1.randomUUID)()}`;
234
+ const startedAt = Date.now();
235
+ const instruction = `附件回传约定:如果本轮需要返回文件、图片或视频,请把要返回的文件副本写入本轮新建的临时目录(Linux/macOS:/tmp/${outputDirectory};Windows:C:/Users/<当前用户>/AppData/Local/Temp/${outputDirectory}),并在最终回复给出文件链接。仅需文字时无需创建目录。`;
236
+ let failure;
237
+ try {
238
+ return await this.directTurn(`${text} ${instruction}`, timeoutMs, attachments, slot, outputDirectory, startedAt);
239
+ }
240
+ catch (error) {
241
+ failure = error;
242
+ throw error;
243
+ }
244
+ finally {
245
+ if (attachments?.length) {
246
+ try {
247
+ await this.evaluate(`(async () => {
248
+ const state = globalThis[${JSON.stringify(slot)}];
249
+ if (!state) return;
250
+ state.observer?.disconnect();
251
+ for (const button of state.buttons ?? []) if (button.isConnected) button.click();
252
+ try { await Promise.all(state.paths.map(path => state.services.workspaceFiles.releaseTemporaryFile({ path }))); }
253
+ finally { delete globalThis[${JSON.stringify(slot)}]; }
254
+ })()`);
255
+ }
256
+ catch (cleanupError) {
257
+ if (failure === undefined)
258
+ throw cleanupError;
259
+ throw new AggregateError([failure, cleanupError], `${failure instanceof Error ? failure.message : String(failure)};临时附件清理失败`, { cause: failure });
260
+ }
261
+ }
262
+ }
263
+ }
264
+ async directTurn(text, timeoutMs, attachments, slot, outputDirectory, startedAt) {
194
265
  await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "ChatGPT 桌面端输入框未就绪");
195
266
  await this.recoverDraft();
196
267
  const beforeTurnsJson = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})].map(turn => ({ id: turn.getAttribute(${JSON.stringify(turnIdAttribute)}), users: turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length })))`);
197
268
  const beforeTurns = JSON.parse(String(beforeTurnsJson || "[]"));
198
269
  const beforeAssistantCount = Number(await this.evaluate(`document.querySelectorAll(${JSON.stringify(assistantMessageSelector)}).length`));
270
+ const baseline = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})].map(node => {
271
+ const message = node.closest('[data-response-annotation-target][data-response-annotation-conversation]');
272
+ return message ? [message.getAttribute('data-response-annotation-conversation'), message.getAttribute('data-response-annotation-target')] : null;
273
+ }).filter(Boolean))`);
274
+ const beforeMessages = JSON.parse(String(baseline || "[]"));
275
+ const origin = await this.activeSession();
199
276
  const ready = await this.evaluate(`(() => {
200
277
  const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
201
- if (!editor || editor.innerText.trim()) return false;
278
+ if (!editor || editor.innerText.trim() || document.querySelector('button[aria-label^="Remove "]')) return false;
202
279
  const findButton = () => {
203
280
  for (let parent = editor; parent && parent !== document.body; parent = parent.parentElement) {
204
281
  const buttons = [...parent.querySelectorAll(${JSON.stringify(sendButtonSelector)})];
@@ -213,6 +290,8 @@ class ChatgptCdpClient {
213
290
  })()`);
214
291
  if (!ready)
215
292
  throw new Error("ChatGPT 输入框含有未发送内容,或桌面端发送按钮未就绪");
293
+ if (attachments?.length)
294
+ await this.injectAttachments(attachments, slot);
216
295
  await this.cdp("Input.insertText", { text });
217
296
  const inputMatches = await this.evaluate(`document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText.trim() === ${JSON.stringify(text.trim())}`);
218
297
  if (!inputMatches)
@@ -239,22 +318,36 @@ class ChatgptCdpClient {
239
318
  while (Date.now() < deadline) {
240
319
  await sleep(800);
241
320
  const raw = await this.evaluate(`(() => {
321
+ const previous = new Set(${JSON.stringify(Array.isArray(beforeMessages) ? beforeMessages : [])}.map(item => JSON.stringify(item)));
322
+ let hasArtifacts = false;
323
+ const artifacts = [...document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})].flatMap(node => {
324
+ const message = node.closest('[data-response-annotation-target][data-response-annotation-conversation]');
325
+ if (!message) return [];
326
+ const conversationId = message.getAttribute('data-response-annotation-conversation');
327
+ const messageId = message.getAttribute('data-response-annotation-target');
328
+ if (!conversationId || !messageId || previous.has(JSON.stringify([conversationId, messageId]))) return [];
329
+ hasArtifacts ||= Boolean(node.querySelector('a[href], img[src], video[src], source[src], [data-file-reference][data-prompt-link-href]'));
330
+ return [{ conversationId, messageId }];
331
+ });
242
332
  const before = ${JSON.stringify(beforeTurns)};
243
333
  const turns = [...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})];
334
+ const userText = node => (node?.querySelector('[data-markdown-text-tone="user-message"]') ?? node)?.innerText.trim().replace(/\\s+/g, ' ');
335
+ const expectedUserText = ${JSON.stringify(text.trim().replace(/\s+/g, " "))};
244
336
  const turn = turns.findLast((item, index) => {
245
337
  const id = item.getAttribute(${JSON.stringify(turnIdAttribute)});
246
338
  const oldIndex = before.findIndex(entry => entry.id === id);
247
339
  const old = oldIndex < 0 ? undefined : before[oldIndex];
248
340
  const users = item.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)});
249
341
  const fresh = oldIndex < 0 || index >= before.length || users.length > (old?.users ?? 0);
250
- return fresh && users.item(users.length - 1)?.innerText.trim() === ${JSON.stringify(text.trim())};
342
+ return fresh && userText(users.item(users.length - 1)) === expectedUserText;
251
343
  });
252
344
  if (!turn) {
253
345
  const assistants = [...document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})];
254
346
  const fallback = assistants.length > ${beforeAssistantCount}
255
347
  ? assistants.at(-1)?.innerText.trim() ?? ""
256
348
  : "";
257
- return JSON.stringify({ reply: fallback, active: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) });
349
+ const submitted = [...document.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)})].some(node => userText(node) === expectedUserText);
350
+ return JSON.stringify({ reply: fallback, artifacts, hasArtifacts, submitted, active: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) });
258
351
  }
259
352
  const user = turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).item(turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length - 1);
260
353
  const unit = user?.closest(${JSON.stringify(contentUnitSelector)});
@@ -264,14 +357,20 @@ class ChatgptCdpClient {
264
357
  const legacyReplies = [...turn.querySelectorAll('[data-message-author-role="assistant"]')].map(item => item.innerText.trim()).filter(Boolean);
265
358
  const reply = replies.at(-1) ?? legacyReplies.at(-1) ?? "";
266
359
  const active = Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)}));
267
- return JSON.stringify({ reply, active });
360
+ return JSON.stringify({ reply, active, artifacts, hasArtifacts, submitted: true });
268
361
  })()`);
269
362
  const state = JSON.parse(String(raw));
270
363
  const reply = state.reply.trim();
271
- stable = reply && reply === previous ? stable + 1 : 0;
272
- previous = reply;
273
- if (reply && stable >= 2 && !state.active)
274
- return reply;
364
+ const signature = JSON.stringify([reply, state.artifacts, state.hasArtifacts]);
365
+ stable = (reply || state.hasArtifacts) && signature === previous ? stable + 1 : 0;
366
+ previous = signature;
367
+ if ((reply || state.hasArtifacts) && stable >= 2 && !state.active) {
368
+ const output = state.hasArtifacts && state.artifacts?.length ? await this.readArtifacts(state.artifacts, origin, outputDirectory, startedAt, state.submitted) : [];
369
+ if (output.length)
370
+ return { text: reply, attachments: output };
371
+ if (reply)
372
+ return reply;
373
+ }
275
374
  }
276
375
  throw new Error("ChatGPT UI 回复超时");
277
376
  }
@@ -315,6 +414,168 @@ class ChatgptCdpClient {
315
414
  recoverDraft() {
316
415
  return (0, cdp_draft_1.recoverCdpDraft)(this.evaluate.bind(this), exports.editorSelector, stopButtonSelector, this.dataDir);
317
416
  }
417
+ async injectAttachments(items, slot) {
418
+ const files = items.map(item => ({ filename: (0, attachments_1.attachmentFilename)(item.filename), contentType: item.contentType, data: Buffer.from(item.data).toString("base64") }));
419
+ await this.evaluate(String.raw `(async () => {
420
+ const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
421
+ if (!editor || document.querySelector('button[aria-label^="Remove "]')) throw new Error('ChatGPT 输入框已有附件或未就绪,未发送消息');
422
+ const entry = [...document.scripts].find(script => /\/assets\/index-[^/]+\.js$/.test(script.src));
423
+ if (!entry) throw new Error('ChatGPT 桌面原生接口入口未找到');
424
+ const source = await (await fetch(entry.src)).text();
425
+ const rpc = source.match(/["'](\.\/rpc-[\w-]+\.js)["']/);
426
+ if (!rpc) throw new Error('ChatGPT 桌面原生附件接口未找到');
427
+ const { appServices: services } = await import(new URL(rpc[1], entry.src).href);
428
+ const state = { services, paths: [], buttons: new Set(), observer: null }; globalThis[${JSON.stringify(slot)}] = state;
429
+ const transfer = new DataTransfer();
430
+ for (const item of ${JSON.stringify(files)}) {
431
+ const binary = atob(item.data); const bytes = new Uint8Array(binary.length);
432
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
433
+ const result = await services.workspaceFiles.createTemporaryFile({ bytes, fileName: item.filename });
434
+ if (!result?.path) throw new Error('ChatGPT 临时附件创建失败');
435
+ state.paths.push(result.path);
436
+ const file = new File([bytes], item.filename, { type: item.contentType });
437
+ Object.defineProperty(file, 'path', { value: result.path }); transfer.items.add(file);
438
+ }
439
+ const names = ${JSON.stringify(files.map(file => `Remove ${file.filename}`))};
440
+ const capture = () => {
441
+ for (const button of document.querySelectorAll('button[aria-label^="Remove "]')) {
442
+ if (names.includes(button.getAttribute('aria-label'))) state.buttons.add(button);
443
+ }
444
+ };
445
+ state.observer = new MutationObserver(capture);
446
+ state.observer.observe(document.body, { childList: true, subtree: true });
447
+ editor.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: transfer }));
448
+ capture();
449
+ })()`);
450
+ await this.waitFor(`(() => {
451
+ const names = ${JSON.stringify(files.map(file => `Remove ${file.filename}`))};
452
+ const current = [...document.querySelectorAll('button[aria-label^="Remove "]')].map(node => node.getAttribute('aria-label'));
453
+ const ready = current.length === names.length && names.every(name => current.filter(value => value === name).length === names.filter(value => value === name).length);
454
+ if (ready) globalThis[${JSON.stringify(slot)}]?.observer?.disconnect();
455
+ return ready;
456
+ })()`, "ChatGPT 附件上传未确认");
457
+ }
458
+ async readArtifacts(messages, origin, outputDirectory, startedAt, submitted = false) {
459
+ const active = await this.activeSession();
460
+ if (!origin || !active || active.host !== origin.host)
461
+ throw new Error("ChatGPT 附件所属会话无法确认");
462
+ if (active.id !== origin.id)
463
+ throw new Error("ChatGPT 会话已变化,未读取输出附件");
464
+ let conversationId = active.id.replace(/^local:/, "");
465
+ if (conversationId.startsWith("client-new-thread:")) {
466
+ const ids = [...new Set(messages.map(message => message.conversationId))];
467
+ if (active.id !== origin.id || !submitted || ids.length !== 1 || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(ids[0]))
468
+ throw new Error("ChatGPT 新会话身份尚未确认,未读取输出附件");
469
+ conversationId = ids[0];
470
+ }
471
+ const scoped = messages.filter(message => typeof message.conversationId === "string" && typeof message.messageId === "string"
472
+ && message.conversationId === conversationId);
473
+ const sessionGuard = `const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"], [data-app-action-sidebar-thread-active="true"]');
474
+ if (selected?.getAttribute('data-app-action-sidebar-thread-id') !== ${JSON.stringify(active.id)}
475
+ || selected?.getAttribute('data-app-action-sidebar-thread-host-id') !== ${JSON.stringify(active.host)}) throw new Error('ChatGPT 会话已变化,未读取输出附件');`;
476
+ const references = await this.evaluate(`(() => {
477
+ ${sessionGuard}
478
+ const messages = ${JSON.stringify(scoped)};
479
+ const references = [];
480
+ for (const node of document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})) {
481
+ const message = node.closest('[data-response-annotation-target][data-response-annotation-conversation]');
482
+ if (!message || !messages.some(item => item.conversationId === message.getAttribute('data-response-annotation-conversation') && item.messageId === message.getAttribute('data-response-annotation-target'))) continue;
483
+ for (const link of node.querySelectorAll('a[href], img[src], video[src], source[src], [data-file-reference][data-prompt-link-href]')) {
484
+ const href = link.getAttribute('href') ?? link.getAttribute('src') ?? link.getAttribute('data-prompt-link-href');
485
+ if (href) references.push(href);
486
+ }
487
+ }
488
+ return references;
489
+ })()`);
490
+ const paths = [...new Set(references.map(href => chatgptAttachmentPath(href, outputDirectory)).filter((path) => Boolean(path)))];
491
+ if (paths.length > attachments_1.MAX_ATTACHMENTS)
492
+ throw new Error("单条消息最多包含 8 个附件");
493
+ const output = [];
494
+ let total = 0;
495
+ for (const path of paths) {
496
+ const fileSlot = `__agentBridgeChatgptOutput_${(0, node_crypto_1.randomUUID)().replaceAll("-", "")}`;
497
+ let file;
498
+ let readFailure;
499
+ try {
500
+ const metadata = await this.evaluate(`(async () => {
501
+ ${sessionGuard}
502
+ globalThis[${JSON.stringify(fileSlot)}] = '';
503
+ const call = (name, params) => new Promise((resolve, reject) => {
504
+ const requestId = crypto.randomUUID();
505
+ const finish = () => { clearTimeout(timer); window.removeEventListener('message', receive); };
506
+ const receive = event => {
507
+ const response = event.data;
508
+ if ((event.source !== window && !(event.source === null && event.origin === '')) || response?.type !== 'fetch-response' || response.requestId !== requestId) return;
509
+ finish();
510
+ if (response.responseType !== 'success' || response.status !== 200) { reject(new Error('ChatGPT 输出附件读取失败')); return; }
511
+ try { resolve(JSON.parse(response.bodyJsonString)); } catch { reject(new Error('ChatGPT 输出附件响应无效')); }
512
+ };
513
+ const timer = setTimeout(() => { finish(); reject(new Error('ChatGPT 输出附件读取超时')); }, ${this.timeoutMs});
514
+ window.addEventListener('message', receive);
515
+ try { window.electronBridge.sendMessageFromView({ type: 'fetch', requestId, method: 'POST', url: 'vscode://codex/' + name, body: JSON.stringify(params) }); }
516
+ catch (error) { finish(); reject(error); }
517
+ });
518
+ const params = { hostId: ${JSON.stringify(active.host)}, path: ${JSON.stringify(path)} };
519
+ const metadata = await call('read-file-metadata', params);
520
+ if (!metadata.isFile || !Number.isSafeInteger(metadata.sizeBytes) || metadata.sizeBytes <= 0) throw new Error('ChatGPT 输出附件元数据无效');
521
+ if (!Number.isFinite(metadata.createdAtMs) || metadata.createdAtMs < ${startedAt - 5000}) throw new Error('ChatGPT 输出附件不是本轮创建的文件');
522
+ if (metadata.sizeBytes > ${attachments_1.MAX_ATTACHMENT_BYTES}) throw new Error('单个附件不能超过 32 MiB');
523
+ if (metadata.sizeBytes > ${attachments_1.MAX_TOTAL_ATTACHMENT_BYTES - total}) throw new Error('单条消息附件总大小不能超过 64 MiB');
524
+ { ${sessionGuard} }
525
+ const entry = [...document.scripts].find(script => /\\/assets\\/index-[^/]+\\.js$/.test(script.src));
526
+ if (!entry) throw new Error('ChatGPT 桌面原生接口入口未找到');
527
+ const source = await (await fetch(entry.src)).text();
528
+ const rpc = source.match(/["'](\\.\\/rpc-[\\w-]+\\.js)["']/);
529
+ if (!rpc) throw new Error('ChatGPT 桌面原生附件接口未找到');
530
+ const { appServices: services } = await import(new URL(rpc[1], entry.src).href);
531
+ { ${sessionGuard} }
532
+ const file = await services.workspaceFiles.read({ ...params, representation: 'blob' });
533
+ { ${sessionGuard} }
534
+ if (globalThis[${JSON.stringify(fileSlot)}] !== '') throw new Error('ChatGPT 输出附件读取已取消');
535
+ if (typeof file.blob !== 'string' || file.blob.length !== Math.ceil(metadata.sizeBytes / 3) * 4) throw new Error('ChatGPT 输出附件内容无效');
536
+ globalThis[${JSON.stringify(fileSlot)}] = file.blob;
537
+ return { length: file.blob.length, size: metadata.sizeBytes };
538
+ })()`);
539
+ if (!Number.isSafeInteger(metadata.length) || metadata.length <= 0 || metadata.length > Math.ceil(attachments_1.MAX_ATTACHMENT_BYTES / 3) * 4)
540
+ throw new Error("ChatGPT 输出附件内容无效");
541
+ const chunks = [];
542
+ for (let offset = 0; offset < metadata.length; offset += 256 * 1024) {
543
+ const chunk = await this.evaluate(`(() => { ${sessionGuard} return globalThis[${JSON.stringify(fileSlot)}]?.slice(${offset}, ${offset + 256 * 1024}); })()`);
544
+ if (typeof chunk !== "string" || chunk.length !== Math.min(256 * 1024, metadata.length - offset))
545
+ throw new Error("ChatGPT 输出附件分块读取失败");
546
+ chunks.push(chunk);
547
+ }
548
+ file = { ...metadata, contentsBase64: chunks.join("") };
549
+ }
550
+ catch (error) {
551
+ readFailure = error;
552
+ throw error;
553
+ }
554
+ finally {
555
+ try {
556
+ await this.evaluate(`delete globalThis[${JSON.stringify(fileSlot)}]`);
557
+ }
558
+ catch (cleanupError) {
559
+ if (readFailure === undefined)
560
+ throw cleanupError;
561
+ throw new AggregateError([readFailure, cleanupError], `${readFailure instanceof Error ? readFailure.message : String(readFailure)};输出附件缓存清理失败`, { cause: readFailure });
562
+ }
563
+ }
564
+ if (typeof file.contentsBase64 !== "string" || file.contentsBase64.length > Math.ceil(attachments_1.MAX_ATTACHMENT_BYTES / 3) * 4)
565
+ throw new Error("ChatGPT 输出附件内容无效");
566
+ const data = Buffer.from(file.contentsBase64, "base64");
567
+ if (data.toString("base64") !== file.contentsBase64 || data.byteLength !== file.size)
568
+ throw new Error("ChatGPT 输出附件大小或编码不一致");
569
+ const filename = (0, attachments_1.attachmentFilename)(path);
570
+ const imageType = (0, attachments_1.detectImageContentType)(data);
571
+ const types = { txt: "text/plain", md: "text/markdown", pdf: "application/pdf", json: "application/json", zip: "application/zip", mp4: "video/mp4", webm: "video/webm", mov: "video/quicktime", mp3: "audio/mpeg", wav: "audio/wav" };
572
+ const contentType = imageType ?? file.mimeType ?? types[filename.split(".").at(-1)?.toLowerCase() ?? ""] ?? "application/octet-stream";
573
+ output.push({ kind: imageType ? "image" : "file", data, filename, contentType });
574
+ total += data.byteLength;
575
+ (0, attachments_1.validateAttachments)(output);
576
+ }
577
+ return output;
578
+ }
318
579
  async waitFor(expression, message) {
319
580
  const deadline = Date.now() + this.timeoutMs;
320
581
  while (Date.now() < deadline) {
@@ -5,7 +5,7 @@ const cdp_client_1 = require("./cdp-client");
5
5
  const session_store_1 = require("./session-store");
6
6
  exports.chatgptManifest = {
7
7
  id: "chatgpt",
8
- version: "1.0.6",
8
+ version: "1.0.8",
9
9
  displayName: "ChatGPT(Codex)",
10
10
  type: "agent",
11
11
  entry: "dist/index.js",
@@ -23,6 +23,7 @@ class ChatgptPlugin {
23
23
  options;
24
24
  manifest = exports.chatgptManifest;
25
25
  sessionOperations = [];
26
+ get attachmentInput() { return this.client?.attachmentInput === true; }
26
27
  client;
27
28
  failure;
28
29
  remote = false;
@@ -94,7 +95,8 @@ class ChatgptPlugin {
94
95
  throw new Error("ChatGPT 插件未启动");
95
96
  const run = this.queue.then(() => this.run(request));
96
97
  this.queue = run.then(() => undefined, () => undefined);
97
- return { text: await run, sessionId: request.sessionId };
98
+ const result = await run;
99
+ return { ...(typeof result === "string" ? { text: result } : result), sessionId: request.sessionId };
98
100
  }
99
101
  async controlSession(request) {
100
102
  if (request.operation !== "new")
@@ -204,7 +206,7 @@ class ChatgptPlugin {
204
206
  await this.client.connect();
205
207
  selected ??= await this.client.activeSession?.();
206
208
  const message = explicit?.[2]?.trim() || request.task;
207
- const reply = await this.client.direct(message, request.timeoutMs);
209
+ const reply = await this.client.direct(message, request.timeoutMs, request.attachments);
208
210
  const baseline = request.sessionId ? store.pendingBindings[request.sessionId] : undefined;
209
211
  if (baseline) {
210
212
  try {
@@ -226,7 +228,7 @@ class ChatgptPlugin {
226
228
  if (selected) {
227
229
  const current = await this.client.activeSession?.().catch(() => undefined);
228
230
  const entry = current?.id === selected.id && current.host === selected.host ? current : selected;
229
- store.remember(entry, message, reply);
231
+ store.remember(entry, message, typeof reply === "string" ? reply : reply.text);
230
232
  if (request.sessionId)
231
233
  store.bindings[request.sessionId] = entry;
232
234
  this.persist();
@@ -7,10 +7,19 @@ exports.waitFor = waitFor;
7
7
  const promises_1 = require("node:fs/promises");
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = require("node:path");
10
+ const versionDirectory = /^(?:app[-_ ]?)?v?\d+(?:[.-]\d+)+$/i;
10
11
  /** 在 Windows 常见安装目录中查找桌面程序,避免依赖单一盘符。 */
11
12
  async function findWindowsExecutable(fileName, configured) {
12
13
  if (configured?.trim()) {
13
14
  const configuredPath = (0, node_path_1.resolve)(configured);
15
+ if (fileName.toLowerCase() === "doubao.exe") {
16
+ let root = (0, node_path_1.dirname)(configuredPath);
17
+ while (/^app$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
18
+ root = (0, node_path_1.dirname)(root);
19
+ const latest = await findExecutableInTree(root, fileName, /^/, 3);
20
+ if (latest)
21
+ return latest;
22
+ }
14
23
  // 千问升级会保留或删除旧版本目录,从同一安装位置重新定位新版。
15
24
  if (fileName.toLowerCase() === "qwenworkcn.exe" && /^v?\d+(?:[.-]\d+)+$/i.test((0, node_path_1.basename)((0, node_path_1.dirname)(configuredPath)))) {
16
25
  const latest = await findExecutableInTree((0, node_path_1.dirname)((0, node_path_1.dirname)(configuredPath)), fileName, /^/, 1);
@@ -42,6 +51,16 @@ async function findExecutableInTree(directory, fileName, pattern, depth) {
42
51
  async function search(directory, fileName, pattern, depth, insideProduct) {
43
52
  try {
44
53
  const entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
54
+ // 豆包上层文件是启动器,优先查找新版目录或 app 中的实际程序。
55
+ if (insideProduct && depth > 0 && fileName.toLowerCase() === "doubao.exe") {
56
+ const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^app$/i.test(entry.name)));
57
+ versions.sort((left, right) => right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true }));
58
+ for (const entry of versions) {
59
+ const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, true);
60
+ if (found)
61
+ return found;
62
+ }
63
+ }
45
64
  for (const entry of entries)
46
65
  if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase())
47
66
  return (0, node_path_1.join)(directory, entry.name);
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cleanupUploadScript = exports.uploadStateScript = void 0;
4
+ exports.buildUploadInjection = buildUploadInjection;
5
+ // 原生上传状态保存在附件区的 React 属性中,避免复刻平台私有上传签名。
6
+ exports.uploadStateScript = `() => {
7
+ const area = document.querySelector('[data-testid=attachment_area]');
8
+ let fiber = area?.[Object.keys(area).find(key => key.startsWith('__reactFiber'))];
9
+ while (fiber) {
10
+ if (Array.isArray(fiber.memoizedProps?.attachmentStates)) return fiber.memoizedProps.attachmentStates;
11
+ fiber = fiber.return;
12
+ }
13
+ return [];
14
+ }`;
15
+ function buildUploadInjection(items) {
16
+ return `(() => {
17
+ const editor = document.querySelector('[contenteditable=true]');
18
+ if (!editor) return '未找到豆包输入区';
19
+ if (editor.textContent.trim() || document.querySelector('[data-testid=attachment_area]')) return '豆包存在未发送草稿,请先处理草稿';
20
+ const input = document.querySelector('input[type=file][data-testid=upload-file-input]');
21
+ if (!input) return '未找到豆包原生附件上传入口';
22
+ const transfer = new DataTransfer();
23
+ for (const item of ${JSON.stringify(items)}) {
24
+ const bytes = Uint8Array.from(atob(item.base64), char => char.charCodeAt(0));
25
+ transfer.items.add(new File([bytes], item.uploadName, { type: item.type }));
26
+ }
27
+ Object.defineProperty(input, 'files', { value: transfer.files, configurable: true });
28
+ try { input.dispatchEvent(new Event('change', { bubbles: true })); }
29
+ finally { delete input.files; }
30
+ return 'started';
31
+ })()`;
32
+ }
33
+ // 唯一文件名前缀限定清理范围,用户上传的新附件不会被删除。
34
+ exports.cleanupUploadScript = `(names) => {
35
+ const states = (${exports.uploadStateScript})();
36
+ const area = document.querySelector('[data-testid=attachment_area]');
37
+ const buttons = area?.querySelectorAll('[data-testid=attachment-delete-btn], [data-testid=message_nested_content_file_delete]') || [];
38
+ for (let index = states.length - 1; index >= 0; index--) {
39
+ if (names.includes(states[index].fileName)) buttons[index]?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
40
+ }
41
+ return 'cleaned';
42
+ }`;