mu-agent-bridge 1.0.34 → 1.0.39

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 (89) hide show
  1. package/README.md +4 -2
  2. package/dist/src-ts/channel-sessions.js +90 -24
  3. package/dist/src-ts/control-server.js +48 -7
  4. package/dist/src-ts/dsh-agent.js +141 -46
  5. package/dist/src-ts/headless-account.patch.yml +8 -1
  6. package/dist/src-ts/plugin-manager.js +69 -3
  7. package/dist/src-ts/plugins/aun/attachments.js +44 -13
  8. package/dist/src-ts/plugins/aun/index.js +5 -6
  9. package/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  10. package/dist/src-ts/plugins/chatgpt/index.js +127 -7
  11. package/dist/src-ts/plugins/desktop-launch.js +19 -0
  12. package/dist/src-ts/plugins/doubao/attachments.js +42 -0
  13. package/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  14. package/dist/src-ts/plugins/doubao/index.js +114 -25
  15. package/dist/src-ts/plugins/doubao/session-history.js +123 -0
  16. package/dist/src-ts/plugins/dsh/index.js +18 -2
  17. package/dist/src-ts/plugins/dsh/session-history.js +371 -0
  18. package/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  19. package/dist/src-ts/plugins/qwenwork/index.js +42 -3
  20. package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  21. package/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  22. package/dist/src-ts/plugins/workbuddy/index.js +23 -3
  23. package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  24. package/dist/src-ts/session-archive.js +509 -0
  25. package/dist/src-ts/session-history.js +2 -0
  26. package/dist/src-ts/session-management.js +665 -0
  27. package/dist/src-ts/session-summary.js +171 -0
  28. package/dist/tests-ts/aun-attachments.test.js +41 -3
  29. package/dist/tests-ts/aun-plugin.test.js +28 -5
  30. package/dist/tests-ts/bundled-package.test.js +3 -1
  31. package/dist/tests-ts/channel-sessions.test.js +255 -2
  32. package/dist/tests-ts/chatgpt-history.test.js +197 -0
  33. package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
  34. package/dist/tests-ts/control-server.test.js +39 -0
  35. package/dist/tests-ts/desktop-launch.test.js +41 -0
  36. package/dist/tests-ts/doubao-history.test.js +172 -0
  37. package/dist/tests-ts/doubao-plugin.test.js +549 -2
  38. package/dist/tests-ts/dsh-agent.test.js +199 -0
  39. package/dist/tests-ts/dsh-history.test.js +165 -0
  40. package/dist/tests-ts/native-session-management.test.js +195 -0
  41. package/dist/tests-ts/plugin-manager.test.js +354 -4
  42. package/dist/tests-ts/qwenwork-history.test.js +339 -0
  43. package/dist/tests-ts/qwenwork-images.test.js +166 -0
  44. package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
  45. package/dist/tests-ts/session-archive.test.js +422 -0
  46. package/dist/tests-ts/session-management.test.js +323 -0
  47. package/dist/tests-ts/session-summary.test.js +199 -0
  48. package/dist/tests-ts/workbuddy-history.test.js +256 -0
  49. package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
  50. package/package.json +1 -1
  51. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
  52. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
  53. package/plugins/aun/package.json +1 -1
  54. package/plugins/aun/plugin.json +1 -1
  55. package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
  56. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  57. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +127 -7
  58. package/plugins/chatgpt/package.json +1 -1
  59. package/plugins/chatgpt/plugin.json +14 -1
  60. package/plugins/doubao/config.schema.json +3 -0
  61. package/plugins/doubao/dist/src-ts/attachments.js +62 -0
  62. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
  63. package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
  64. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  65. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +114 -25
  66. package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
  67. package/plugins/doubao/package.json +1 -1
  68. package/plugins/doubao/plugin.json +1 -1
  69. package/plugins/dsh/config.schema.json +2 -1
  70. package/plugins/dsh/dist/src-ts/attachments.js +62 -0
  71. package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
  72. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
  73. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +18 -2
  74. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
  75. package/plugins/dsh/package.json +5 -2
  76. package/plugins/dsh/plugin.json +1 -1
  77. package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
  78. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
  79. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  80. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +42 -3
  81. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  82. package/plugins/qwenwork/package.json +1 -1
  83. package/plugins/qwenwork/plugin.json +1 -1
  84. package/plugins/workbuddy/config.schema.json +1 -1
  85. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  86. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +23 -3
  87. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  88. package/plugins/workbuddy/package.json +1 -1
  89. package/plugins/workbuddy/plugin.json +1 -1
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AunDownloadUrlError = void 0;
3
4
  exports.parseAunAttachment = parseAunAttachment;
4
5
  exports.downloadAunAttachments = downloadAunAttachments;
5
6
  exports.downloadAunAttachment = downloadAunAttachment;
6
7
  exports.uploadAunAttachments = uploadAunAttachments;
7
8
  const node_crypto_1 = require("node:crypto");
9
+ const node_util_1 = require("node:util");
8
10
  const attachments_1 = require("../../attachments");
9
11
  const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
10
12
  const mimePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
@@ -12,6 +14,21 @@ const imageTypes = new Set(["image/png", "image/jpeg", "image/webp"]);
12
14
  function digest(data) { return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex"); }
13
15
  function validKey(key) { return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(key) && !key.split("/").some((part) => part === "." || part === ".."); }
14
16
  function validHash(value) { return /^[a-f0-9]{64}$/i.test(value); }
17
+ function normalizedMime(value) {
18
+ if (value === undefined)
19
+ return;
20
+ if (typeof value !== "string" || /[\u0000-\u001f\u007f]/.test(value))
21
+ throw new Error("AUN 附件 MIME 类型无效");
22
+ if (!value.trim())
23
+ return;
24
+ try {
25
+ const type = new node_util_1.MIMEType(value).essence;
26
+ if (mimePattern.test(type))
27
+ return type;
28
+ }
29
+ catch { /* 统一报告无效类型。 */ }
30
+ throw new Error("AUN 附件 MIME 类型无效");
31
+ }
15
32
  function validateReference(ref) {
16
33
  if (!aidPattern.test(ref.ownerAid) || !/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/.test(ref.bucket) || !validKey(ref.objectKey))
17
34
  throw new Error("AUN 附件引用路径无效");
@@ -43,11 +60,9 @@ function parseAunAttachment(value) {
43
60
  const bucket = item.bucket === undefined ? "default" : typeof item.bucket === "string" ? item.bucket : "";
44
61
  const filename = (0, attachments_1.attachmentFilename)(typeof item.filename === "string" ? item.filename : objectKey);
45
62
  const ref = { ownerAid, bucket, objectKey, filename };
46
- if (item.content_type !== undefined) {
47
- if (typeof item.content_type !== "string")
48
- throw new Error("AUN 附件 MIME 类型无效");
49
- ref.contentType = item.content_type.toLowerCase();
50
- }
63
+ const contentType = normalizedMime(item.content_type);
64
+ if (contentType)
65
+ ref.contentType = contentType;
51
66
  if (item.size_bytes !== undefined) {
52
67
  if (typeof item.size_bytes !== "number")
53
68
  throw new Error("AUN 附件大小无效");
@@ -81,10 +96,8 @@ function checkedMetadata(meta, ref) {
81
96
  if ([meta.owner, meta.owner_aid].some((owner) => owner && owner !== ref.ownerAid) || meta.bucket && meta.bucket !== ref.bucket
82
97
  || [meta.path, meta.object_key].some((path) => path && path !== `/${ref.objectKey}` && path !== ref.objectKey))
83
98
  throw new Error("AUN 附件元数据引用不一致");
84
- const rawType = metadataValue(meta, "contentType", "content_type");
85
- if (rawType && (typeof rawType !== "string" || !mimePattern.test(rawType)))
86
- throw new Error("AUN 附件元数据 MIME 类型无效");
87
- const contentType = typeof rawType === "string" && rawType ? rawType.toLowerCase() : ref.contentType ?? "application/octet-stream";
99
+ const rawType = normalizedMime(metadataValue(meta, "contentType", "content_type"));
100
+ const contentType = rawType ?? ref.contentType ?? "application/octet-stream";
88
101
  if (ref.contentType && rawType && ref.contentType.toLowerCase() !== contentType)
89
102
  throw new Error("AUN 附件 MIME 类型校验失败");
90
103
  const sha256 = typeof meta.sha256 === "string" && meta.sha256 ? meta.sha256.toLowerCase() : undefined;
@@ -162,9 +175,20 @@ async function downloadAunAttachments(client, refs) {
162
175
  (0, attachments_1.validateAttachments)(result);
163
176
  return result;
164
177
  }
178
+ class AunDownloadUrlError extends Error {
179
+ diagnostic;
180
+ constructor(url, reason) {
181
+ super("AUN 下载地址无效");
182
+ this.name = "AunDownloadUrlError";
183
+ this.diagnostic = { protocol: url.protocol, hostname: url.hostname, port: url.port, reason };
184
+ }
185
+ }
186
+ exports.AunDownloadUrlError = AunDownloadUrlError;
165
187
  function checkedDownloadUrl(url) {
166
- if (url.protocol !== "https:" || url.username || url.password || url.port || url.hash || !url.hostname.includes(".") || /^(?:localhost|127\.|0\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(url.hostname) || url.hostname.includes(":"))
167
- throw new Error("AUN 下载地址无效");
188
+ const storagePort = url.port === "9470" && /^storage\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i.test(url.hostname);
189
+ const reason = url.protocol !== "https:" ? "协议" : url.username || url.password ? "凭据" : url.port && !storagePort ? "端口" : url.hash ? "片段" : !url.hostname.includes(".") ? "主机名" : /^(?:localhost|127\.|0\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(url.hostname) ? "本机地址" : url.hostname.includes(":") ? "主机名" : undefined;
190
+ if (reason)
191
+ throw new AunDownloadUrlError(url, reason);
168
192
  return url;
169
193
  }
170
194
  async function readDownloadTicket(value, expectedSize) {
@@ -224,9 +248,16 @@ async function uploadAunAttachments(client, ownerAid, peerAid, items) {
224
248
  const filename = (0, attachments_1.attachmentFilename)(item.filename);
225
249
  const objectKey = `shared/${(0, node_crypto_1.randomUUID)()}/${filename.replace(/[^A-Za-z0-9._-]/g, "_")}`;
226
250
  const sha256 = digest(item.data);
227
- const ref = { ownerAid, bucket: "default", objectKey, filename, contentType: item.contentType.toLowerCase(), sizeBytes: item.data.byteLength, sha256 };
228
- const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType: item.contentType, public: false, overwrite: false });
251
+ const contentType = item.contentType.toLowerCase();
252
+ // 视频播放器不携带 AID 凭据,沿用 EvolCore 的标准可读对象上传方式。
253
+ const video = contentType.startsWith("video/");
254
+ const ref = { ownerAid, bucket: "default", objectKey, filename, contentType, sizeBytes: item.data.byteLength, sha256 };
255
+ const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType, public: video, overwrite: video });
229
256
  checkedMetadata({ ...uploaded, size: uploaded.size ?? uploaded.size_bytes ?? item.data.byteLength }, ref);
257
+ if (video) {
258
+ result.push({ owner_aid: ownerAid, object_key: objectKey, filename, content_type: contentType, size_bytes: item.data.byteLength, sha256 });
259
+ continue;
260
+ }
230
261
  const share = await client.storage.lowlevel.createShareLink({ owner: ownerAid, bucket: "default", objectKey, allowedAids: [peerAid], expireInSeconds: 86400 });
231
262
  if (share.owner_aid && share.owner_aid !== ownerAid || share.bucket && share.bucket !== "default" || share.object_key && share.object_key !== objectKey)
232
263
  throw new Error("AUN 附件分享对象不一致");
@@ -11,7 +11,7 @@ const node_url_1 = require("node:url");
11
11
  const messages_1 = require("./messages");
12
12
  const attachments_1 = require("./attachments");
13
13
  const manifest = {
14
- id: "aun", version: "1.0.23", type: "channel", displayName: "AUN 通道",
14
+ id: "aun", version: "1.0.30", type: "channel", displayName: "AUN 通道",
15
15
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
16
16
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
17
17
  configSchema: "config.schema.json",
@@ -783,7 +783,7 @@ class AunPlugin {
783
783
  }
784
784
  }
785
785
  catch (error) {
786
- this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error) });
786
+ this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error), ...(error instanceof attachments_1.AunDownloadUrlError ? { downloadUrl: error.diagnostic } : {}) });
787
787
  try {
788
788
  await this.sendText(aid, message.peerId, `附件接收失败:${safeError(error)}`, message.threadId);
789
789
  }
@@ -817,15 +817,14 @@ class AunPlugin {
817
817
  }
818
818
  const groups = new Map();
819
819
  for (const [index, item] of items.entries()) {
820
- const kind = item.kind;
821
- const ref = refs[index];
820
+ const kind = item.kind === "file" && item.contentType.toLowerCase().startsWith("video/") ? "video" : item.kind;
822
821
  const group = groups.get(kind) ?? [];
823
- group.push(ref);
822
+ group.push(refs[index]);
824
823
  groups.set(kind, group);
825
824
  }
826
825
  groupsCount = groups.size;
827
826
  for (const [kind, group] of groups) {
828
- const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : "文件已生成。"), attachments: group };
827
+ const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : kind === "video" ? "视频已生成。" : "文件已生成。"), attachments: group };
829
828
  if (message.threadId)
830
829
  payload.thread_id = message.threadId;
831
830
  if (message.messageId)
@@ -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;
@@ -58,6 +95,78 @@ class ChatgptCdpClient {
58
95
  })()`);
59
96
  return (0, session_store_1.validSession)(entry) ? entry : undefined;
60
97
  }
98
+ async historyBusy() {
99
+ return Boolean(await this.evaluate(`Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)}))`));
100
+ }
101
+ async readSessionHistory(session) {
102
+ if (!this.connected())
103
+ return { supported: false, reason: "ChatGPT CDP 当前未连接" };
104
+ const before = await this.activeSession();
105
+ if (!before || before.id !== session.id || before.host !== session.host)
106
+ return { supported: false, reason: "当前活动会话身份与请求不一致" };
107
+ if (await this.historyBusy())
108
+ return { supported: false, reason: "ChatGPT 正在生成回复,暂不读取历史" };
109
+ const raw = await this.evaluate(`(() => {
110
+ const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"], [data-app-action-sidebar-thread-active="true"]');
111
+ if (selected?.getAttribute('data-app-action-sidebar-thread-id') !== ${JSON.stringify(session.id)}
112
+ || selected?.getAttribute('data-app-action-sidebar-thread-host-id') !== ${JSON.stringify(session.host)}) return { supported: false, reason: '读取期间活动会话身份发生变化' };
113
+ if (document.querySelector(${JSON.stringify(stopButtonSelector)})) return { supported: false, reason: 'ChatGPT 正在生成回复,暂不读取历史' };
114
+ const turns = [...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})];
115
+ const output = [];
116
+ const missingReasons = [];
117
+ let chars = 0;
118
+ const collect = (role, text) => {
119
+ chars += text.length;
120
+ if (chars > 60000) throw new Error('ChatGPT 会话正文超过 60,000 字符上限');
121
+ output.push({ role, text });
122
+ };
123
+ for (const turn of turns) {
124
+ const turnId = turn.getAttribute(${JSON.stringify(turnIdAttribute)});
125
+ if (!turnId) {
126
+ missingReasons.push('部分消息缺少可验证的 turn 身份');
127
+ continue;
128
+ }
129
+ const users = [...turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)})];
130
+ for (const item of users) {
131
+ const body = item.querySelector('[data-markdown-text-tone="user-message"]');
132
+ if (!body) {
133
+ missingReasons.push('部分用户消息缺少专用正文标记,已跳过');
134
+ continue;
135
+ }
136
+ const text = body.innerText?.trim();
137
+ if (text) collect('user', text);
138
+ }
139
+ const assistants = [...turn.querySelectorAll(${JSON.stringify(assistantMessageSelector)})];
140
+ for (const item of assistants) {
141
+ const marker = item.closest('[data-response-annotation-target][data-response-annotation-conversation]');
142
+ const excluded = item.closest('[data-message-author-role="tool"], [data-message-author-role="system"], [data-message-channel="analysis"], [data-message-channel="commentary"], [data-reasoning], [data-is-streaming="true"], [data-streaming="true"], [aria-busy="true"]');
143
+ const text = item.innerText?.trim();
144
+ if (marker?.getAttribute('data-response-annotation-conversation') === ${JSON.stringify(session.id.replace(/^local:/, ""))}
145
+ && marker.getAttribute('data-response-annotation-target') && !excluded && text) collect('assistant', text);
146
+ }
147
+ }
148
+ return { supported: true, turns: output, missingReasons };
149
+ })()`);
150
+ const after = await this.activeSession();
151
+ if (!after || after.id !== session.id || after.host !== session.host)
152
+ return { supported: false, reason: "读取期间活动会话身份发生变化" };
153
+ if (await this.historyBusy())
154
+ return { supported: false, reason: "ChatGPT 正在生成回复,暂不读取历史" };
155
+ const result = raw;
156
+ if (result?.supported === false && typeof result.reason === "string")
157
+ return { supported: false, reason: result.reason };
158
+ if (!result || !Array.isArray(result.turns) || !result.turns.every((turn) => {
159
+ if (!turn || typeof turn !== "object")
160
+ return false;
161
+ const value = turn;
162
+ return (value.role === "user" || value.role === "assistant") && typeof value.text === "string";
163
+ })) {
164
+ return { supported: false, reason: "ChatGPT 会话正文格式无法验证" };
165
+ }
166
+ const missingReasons = Array.isArray(result.missingReasons) ? result.missingReasons.filter((item) => typeof item === "string") : [];
167
+ missingReasons.push("仅包含当前页面已加载的可验证正文,页面虚拟化或未加载历史导致完整性无法确认");
168
+ return { supported: true, turns: result.turns, complete: false, missingReasons };
169
+ }
61
170
  async switchSession(session) {
62
171
  await this.recoverDraft();
63
172
  const locate = `[...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].find(item =>
@@ -190,15 +299,55 @@ class ChatgptCdpClient {
190
299
  this.tunnel = undefined;
191
300
  this.rejectPending(new Error("ChatGPT CDP 已关闭"));
192
301
  }
193
- async direct(text, timeoutMs = 240_000) {
302
+ async direct(text, timeoutMs = 240_000, attachments) {
303
+ (0, attachments_1.validateAttachments)(attachments);
304
+ const slot = `__agentBridgeChatgpt_${(0, node_crypto_1.randomUUID)().replaceAll("-", "")}`;
305
+ const outputDirectory = `agent-bridge-chatgpt-${(0, node_crypto_1.randomUUID)()}`;
306
+ const startedAt = Date.now();
307
+ const instruction = `附件回传约定:如果本轮需要返回文件、图片或视频,请把要返回的文件副本写入本轮新建的临时目录(Linux/macOS:/tmp/${outputDirectory};Windows:C:/Users/<当前用户>/AppData/Local/Temp/${outputDirectory}),并在最终回复给出文件链接。仅需文字时无需创建目录。`;
308
+ let failure;
309
+ try {
310
+ return await this.directTurn(`${text} ${instruction}`, timeoutMs, attachments, slot, outputDirectory, startedAt);
311
+ }
312
+ catch (error) {
313
+ failure = error;
314
+ throw error;
315
+ }
316
+ finally {
317
+ if (attachments?.length) {
318
+ try {
319
+ await this.evaluate(`(async () => {
320
+ const state = globalThis[${JSON.stringify(slot)}];
321
+ if (!state) return;
322
+ state.observer?.disconnect();
323
+ for (const button of state.buttons ?? []) if (button.isConnected) button.click();
324
+ try { await Promise.all(state.paths.map(path => state.services.workspaceFiles.releaseTemporaryFile({ path }))); }
325
+ finally { delete globalThis[${JSON.stringify(slot)}]; }
326
+ })()`);
327
+ }
328
+ catch (cleanupError) {
329
+ if (failure === undefined)
330
+ throw cleanupError;
331
+ throw new AggregateError([failure, cleanupError], `${failure instanceof Error ? failure.message : String(failure)};临时附件清理失败`, { cause: failure });
332
+ }
333
+ }
334
+ }
335
+ }
336
+ async directTurn(text, timeoutMs, attachments, slot, outputDirectory, startedAt) {
194
337
  await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "ChatGPT 桌面端输入框未就绪");
195
338
  await this.recoverDraft();
196
339
  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
340
  const beforeTurns = JSON.parse(String(beforeTurnsJson || "[]"));
198
341
  const beforeAssistantCount = Number(await this.evaluate(`document.querySelectorAll(${JSON.stringify(assistantMessageSelector)}).length`));
342
+ const baseline = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})].map(node => {
343
+ const message = node.closest('[data-response-annotation-target][data-response-annotation-conversation]');
344
+ return message ? [message.getAttribute('data-response-annotation-conversation'), message.getAttribute('data-response-annotation-target')] : null;
345
+ }).filter(Boolean))`);
346
+ const beforeMessages = JSON.parse(String(baseline || "[]"));
347
+ const origin = await this.activeSession();
199
348
  const ready = await this.evaluate(`(() => {
200
349
  const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
201
- if (!editor || editor.innerText.trim()) return false;
350
+ if (!editor || editor.innerText.trim() || document.querySelector('button[aria-label^="Remove "]')) return false;
202
351
  const findButton = () => {
203
352
  for (let parent = editor; parent && parent !== document.body; parent = parent.parentElement) {
204
353
  const buttons = [...parent.querySelectorAll(${JSON.stringify(sendButtonSelector)})];
@@ -213,6 +362,8 @@ class ChatgptCdpClient {
213
362
  })()`);
214
363
  if (!ready)
215
364
  throw new Error("ChatGPT 输入框含有未发送内容,或桌面端发送按钮未就绪");
365
+ if (attachments?.length)
366
+ await this.injectAttachments(attachments, slot);
216
367
  await this.cdp("Input.insertText", { text });
217
368
  const inputMatches = await this.evaluate(`document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText.trim() === ${JSON.stringify(text.trim())}`);
218
369
  if (!inputMatches)
@@ -239,22 +390,36 @@ class ChatgptCdpClient {
239
390
  while (Date.now() < deadline) {
240
391
  await sleep(800);
241
392
  const raw = await this.evaluate(`(() => {
393
+ const previous = new Set(${JSON.stringify(Array.isArray(beforeMessages) ? beforeMessages : [])}.map(item => JSON.stringify(item)));
394
+ let hasArtifacts = false;
395
+ const artifacts = [...document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})].flatMap(node => {
396
+ const message = node.closest('[data-response-annotation-target][data-response-annotation-conversation]');
397
+ if (!message) return [];
398
+ const conversationId = message.getAttribute('data-response-annotation-conversation');
399
+ const messageId = message.getAttribute('data-response-annotation-target');
400
+ if (!conversationId || !messageId || previous.has(JSON.stringify([conversationId, messageId]))) return [];
401
+ hasArtifacts ||= Boolean(node.querySelector('a[href], img[src], video[src], source[src], [data-file-reference][data-prompt-link-href]'));
402
+ return [{ conversationId, messageId }];
403
+ });
242
404
  const before = ${JSON.stringify(beforeTurns)};
243
405
  const turns = [...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})];
406
+ const userText = node => (node?.querySelector('[data-markdown-text-tone="user-message"]') ?? node)?.innerText.trim().replace(/\\s+/g, ' ');
407
+ const expectedUserText = ${JSON.stringify(text.trim().replace(/\s+/g, " "))};
244
408
  const turn = turns.findLast((item, index) => {
245
409
  const id = item.getAttribute(${JSON.stringify(turnIdAttribute)});
246
410
  const oldIndex = before.findIndex(entry => entry.id === id);
247
411
  const old = oldIndex < 0 ? undefined : before[oldIndex];
248
412
  const users = item.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)});
249
413
  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())};
414
+ return fresh && userText(users.item(users.length - 1)) === expectedUserText;
251
415
  });
252
416
  if (!turn) {
253
417
  const assistants = [...document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})];
254
418
  const fallback = assistants.length > ${beforeAssistantCount}
255
419
  ? assistants.at(-1)?.innerText.trim() ?? ""
256
420
  : "";
257
- return JSON.stringify({ reply: fallback, active: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) });
421
+ const submitted = [...document.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)})].some(node => userText(node) === expectedUserText);
422
+ return JSON.stringify({ reply: fallback, artifacts, hasArtifacts, submitted, active: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) });
258
423
  }
259
424
  const user = turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).item(turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length - 1);
260
425
  const unit = user?.closest(${JSON.stringify(contentUnitSelector)});
@@ -264,14 +429,20 @@ class ChatgptCdpClient {
264
429
  const legacyReplies = [...turn.querySelectorAll('[data-message-author-role="assistant"]')].map(item => item.innerText.trim()).filter(Boolean);
265
430
  const reply = replies.at(-1) ?? legacyReplies.at(-1) ?? "";
266
431
  const active = Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)}));
267
- return JSON.stringify({ reply, active });
432
+ return JSON.stringify({ reply, active, artifacts, hasArtifacts, submitted: true });
268
433
  })()`);
269
434
  const state = JSON.parse(String(raw));
270
435
  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;
436
+ const signature = JSON.stringify([reply, state.artifacts, state.hasArtifacts]);
437
+ stable = (reply || state.hasArtifacts) && signature === previous ? stable + 1 : 0;
438
+ previous = signature;
439
+ if ((reply || state.hasArtifacts) && stable >= 2 && !state.active) {
440
+ const output = state.hasArtifacts && state.artifacts?.length ? await this.readArtifacts(state.artifacts, origin, outputDirectory, startedAt, state.submitted) : [];
441
+ if (output.length)
442
+ return { text: reply, attachments: output };
443
+ if (reply)
444
+ return reply;
445
+ }
275
446
  }
276
447
  throw new Error("ChatGPT UI 回复超时");
277
448
  }
@@ -315,6 +486,168 @@ class ChatgptCdpClient {
315
486
  recoverDraft() {
316
487
  return (0, cdp_draft_1.recoverCdpDraft)(this.evaluate.bind(this), exports.editorSelector, stopButtonSelector, this.dataDir);
317
488
  }
489
+ async injectAttachments(items, slot) {
490
+ const files = items.map(item => ({ filename: (0, attachments_1.attachmentFilename)(item.filename), contentType: item.contentType, data: Buffer.from(item.data).toString("base64") }));
491
+ await this.evaluate(String.raw `(async () => {
492
+ const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
493
+ if (!editor || document.querySelector('button[aria-label^="Remove "]')) throw new Error('ChatGPT 输入框已有附件或未就绪,未发送消息');
494
+ const entry = [...document.scripts].find(script => /\/assets\/index-[^/]+\.js$/.test(script.src));
495
+ if (!entry) throw new Error('ChatGPT 桌面原生接口入口未找到');
496
+ const source = await (await fetch(entry.src)).text();
497
+ const rpc = source.match(/["'](\.\/rpc-[\w-]+\.js)["']/);
498
+ if (!rpc) throw new Error('ChatGPT 桌面原生附件接口未找到');
499
+ const { appServices: services } = await import(new URL(rpc[1], entry.src).href);
500
+ const state = { services, paths: [], buttons: new Set(), observer: null }; globalThis[${JSON.stringify(slot)}] = state;
501
+ const transfer = new DataTransfer();
502
+ for (const item of ${JSON.stringify(files)}) {
503
+ const binary = atob(item.data); const bytes = new Uint8Array(binary.length);
504
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
505
+ const result = await services.workspaceFiles.createTemporaryFile({ bytes, fileName: item.filename });
506
+ if (!result?.path) throw new Error('ChatGPT 临时附件创建失败');
507
+ state.paths.push(result.path);
508
+ const file = new File([bytes], item.filename, { type: item.contentType });
509
+ Object.defineProperty(file, 'path', { value: result.path }); transfer.items.add(file);
510
+ }
511
+ const names = ${JSON.stringify(files.map(file => `Remove ${file.filename}`))};
512
+ const capture = () => {
513
+ for (const button of document.querySelectorAll('button[aria-label^="Remove "]')) {
514
+ if (names.includes(button.getAttribute('aria-label'))) state.buttons.add(button);
515
+ }
516
+ };
517
+ state.observer = new MutationObserver(capture);
518
+ state.observer.observe(document.body, { childList: true, subtree: true });
519
+ editor.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: transfer }));
520
+ capture();
521
+ })()`);
522
+ await this.waitFor(`(() => {
523
+ const names = ${JSON.stringify(files.map(file => `Remove ${file.filename}`))};
524
+ const current = [...document.querySelectorAll('button[aria-label^="Remove "]')].map(node => node.getAttribute('aria-label'));
525
+ const ready = current.length === names.length && names.every(name => current.filter(value => value === name).length === names.filter(value => value === name).length);
526
+ if (ready) globalThis[${JSON.stringify(slot)}]?.observer?.disconnect();
527
+ return ready;
528
+ })()`, "ChatGPT 附件上传未确认");
529
+ }
530
+ async readArtifacts(messages, origin, outputDirectory, startedAt, submitted = false) {
531
+ const active = await this.activeSession();
532
+ if (!origin || !active || active.host !== origin.host)
533
+ throw new Error("ChatGPT 附件所属会话无法确认");
534
+ if (active.id !== origin.id)
535
+ throw new Error("ChatGPT 会话已变化,未读取输出附件");
536
+ let conversationId = active.id.replace(/^local:/, "");
537
+ if (conversationId.startsWith("client-new-thread:")) {
538
+ const ids = [...new Set(messages.map(message => message.conversationId))];
539
+ 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]))
540
+ throw new Error("ChatGPT 新会话身份尚未确认,未读取输出附件");
541
+ conversationId = ids[0];
542
+ }
543
+ const scoped = messages.filter(message => typeof message.conversationId === "string" && typeof message.messageId === "string"
544
+ && message.conversationId === conversationId);
545
+ const sessionGuard = `const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"], [data-app-action-sidebar-thread-active="true"]');
546
+ if (selected?.getAttribute('data-app-action-sidebar-thread-id') !== ${JSON.stringify(active.id)}
547
+ || selected?.getAttribute('data-app-action-sidebar-thread-host-id') !== ${JSON.stringify(active.host)}) throw new Error('ChatGPT 会话已变化,未读取输出附件');`;
548
+ const references = await this.evaluate(`(() => {
549
+ ${sessionGuard}
550
+ const messages = ${JSON.stringify(scoped)};
551
+ const references = [];
552
+ for (const node of document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})) {
553
+ const message = node.closest('[data-response-annotation-target][data-response-annotation-conversation]');
554
+ if (!message || !messages.some(item => item.conversationId === message.getAttribute('data-response-annotation-conversation') && item.messageId === message.getAttribute('data-response-annotation-target'))) continue;
555
+ for (const link of node.querySelectorAll('a[href], img[src], video[src], source[src], [data-file-reference][data-prompt-link-href]')) {
556
+ const href = link.getAttribute('href') ?? link.getAttribute('src') ?? link.getAttribute('data-prompt-link-href');
557
+ if (href) references.push(href);
558
+ }
559
+ }
560
+ return references;
561
+ })()`);
562
+ const paths = [...new Set(references.map(href => chatgptAttachmentPath(href, outputDirectory)).filter((path) => Boolean(path)))];
563
+ if (paths.length > attachments_1.MAX_ATTACHMENTS)
564
+ throw new Error("单条消息最多包含 8 个附件");
565
+ const output = [];
566
+ let total = 0;
567
+ for (const path of paths) {
568
+ const fileSlot = `__agentBridgeChatgptOutput_${(0, node_crypto_1.randomUUID)().replaceAll("-", "")}`;
569
+ let file;
570
+ let readFailure;
571
+ try {
572
+ const metadata = await this.evaluate(`(async () => {
573
+ ${sessionGuard}
574
+ globalThis[${JSON.stringify(fileSlot)}] = '';
575
+ const call = (name, params) => new Promise((resolve, reject) => {
576
+ const requestId = crypto.randomUUID();
577
+ const finish = () => { clearTimeout(timer); window.removeEventListener('message', receive); };
578
+ const receive = event => {
579
+ const response = event.data;
580
+ if ((event.source !== window && !(event.source === null && event.origin === '')) || response?.type !== 'fetch-response' || response.requestId !== requestId) return;
581
+ finish();
582
+ if (response.responseType !== 'success' || response.status !== 200) { reject(new Error('ChatGPT 输出附件读取失败')); return; }
583
+ try { resolve(JSON.parse(response.bodyJsonString)); } catch { reject(new Error('ChatGPT 输出附件响应无效')); }
584
+ };
585
+ const timer = setTimeout(() => { finish(); reject(new Error('ChatGPT 输出附件读取超时')); }, ${this.timeoutMs});
586
+ window.addEventListener('message', receive);
587
+ try { window.electronBridge.sendMessageFromView({ type: 'fetch', requestId, method: 'POST', url: 'vscode://codex/' + name, body: JSON.stringify(params) }); }
588
+ catch (error) { finish(); reject(error); }
589
+ });
590
+ const params = { hostId: ${JSON.stringify(active.host)}, path: ${JSON.stringify(path)} };
591
+ const metadata = await call('read-file-metadata', params);
592
+ if (!metadata.isFile || !Number.isSafeInteger(metadata.sizeBytes) || metadata.sizeBytes <= 0) throw new Error('ChatGPT 输出附件元数据无效');
593
+ if (!Number.isFinite(metadata.createdAtMs) || metadata.createdAtMs < ${startedAt - 5000}) throw new Error('ChatGPT 输出附件不是本轮创建的文件');
594
+ if (metadata.sizeBytes > ${attachments_1.MAX_ATTACHMENT_BYTES}) throw new Error('单个附件不能超过 32 MiB');
595
+ if (metadata.sizeBytes > ${attachments_1.MAX_TOTAL_ATTACHMENT_BYTES - total}) throw new Error('单条消息附件总大小不能超过 64 MiB');
596
+ { ${sessionGuard} }
597
+ const entry = [...document.scripts].find(script => /\\/assets\\/index-[^/]+\\.js$/.test(script.src));
598
+ if (!entry) throw new Error('ChatGPT 桌面原生接口入口未找到');
599
+ const source = await (await fetch(entry.src)).text();
600
+ const rpc = source.match(/["'](\\.\\/rpc-[\\w-]+\\.js)["']/);
601
+ if (!rpc) throw new Error('ChatGPT 桌面原生附件接口未找到');
602
+ const { appServices: services } = await import(new URL(rpc[1], entry.src).href);
603
+ { ${sessionGuard} }
604
+ const file = await services.workspaceFiles.read({ ...params, representation: 'blob' });
605
+ { ${sessionGuard} }
606
+ if (globalThis[${JSON.stringify(fileSlot)}] !== '') throw new Error('ChatGPT 输出附件读取已取消');
607
+ if (typeof file.blob !== 'string' || file.blob.length !== Math.ceil(metadata.sizeBytes / 3) * 4) throw new Error('ChatGPT 输出附件内容无效');
608
+ globalThis[${JSON.stringify(fileSlot)}] = file.blob;
609
+ return { length: file.blob.length, size: metadata.sizeBytes };
610
+ })()`);
611
+ if (!Number.isSafeInteger(metadata.length) || metadata.length <= 0 || metadata.length > Math.ceil(attachments_1.MAX_ATTACHMENT_BYTES / 3) * 4)
612
+ throw new Error("ChatGPT 输出附件内容无效");
613
+ const chunks = [];
614
+ for (let offset = 0; offset < metadata.length; offset += 256 * 1024) {
615
+ const chunk = await this.evaluate(`(() => { ${sessionGuard} return globalThis[${JSON.stringify(fileSlot)}]?.slice(${offset}, ${offset + 256 * 1024}); })()`);
616
+ if (typeof chunk !== "string" || chunk.length !== Math.min(256 * 1024, metadata.length - offset))
617
+ throw new Error("ChatGPT 输出附件分块读取失败");
618
+ chunks.push(chunk);
619
+ }
620
+ file = { ...metadata, contentsBase64: chunks.join("") };
621
+ }
622
+ catch (error) {
623
+ readFailure = error;
624
+ throw error;
625
+ }
626
+ finally {
627
+ try {
628
+ await this.evaluate(`delete globalThis[${JSON.stringify(fileSlot)}]`);
629
+ }
630
+ catch (cleanupError) {
631
+ if (readFailure === undefined)
632
+ throw cleanupError;
633
+ throw new AggregateError([readFailure, cleanupError], `${readFailure instanceof Error ? readFailure.message : String(readFailure)};输出附件缓存清理失败`, { cause: readFailure });
634
+ }
635
+ }
636
+ if (typeof file.contentsBase64 !== "string" || file.contentsBase64.length > Math.ceil(attachments_1.MAX_ATTACHMENT_BYTES / 3) * 4)
637
+ throw new Error("ChatGPT 输出附件内容无效");
638
+ const data = Buffer.from(file.contentsBase64, "base64");
639
+ if (data.toString("base64") !== file.contentsBase64 || data.byteLength !== file.size)
640
+ throw new Error("ChatGPT 输出附件大小或编码不一致");
641
+ const filename = (0, attachments_1.attachmentFilename)(path);
642
+ const imageType = (0, attachments_1.detectImageContentType)(data);
643
+ 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" };
644
+ const contentType = imageType ?? file.mimeType ?? types[filename.split(".").at(-1)?.toLowerCase() ?? ""] ?? "application/octet-stream";
645
+ output.push({ kind: imageType ? "image" : "file", data, filename, contentType });
646
+ total += data.byteLength;
647
+ (0, attachments_1.validateAttachments)(output);
648
+ }
649
+ return output;
650
+ }
318
651
  async waitFor(expression, message) {
319
652
  const deadline = Date.now() + this.timeoutMs;
320
653
  while (Date.now() < deadline) {