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.
- package/README.md +4 -2
- package/dist/src-ts/channel-sessions.js +90 -24
- package/dist/src-ts/control-server.js +48 -7
- package/dist/src-ts/dsh-agent.js +141 -46
- package/dist/src-ts/headless-account.patch.yml +8 -1
- package/dist/src-ts/plugin-manager.js +69 -3
- package/dist/src-ts/plugins/aun/attachments.js +44 -13
- package/dist/src-ts/plugins/aun/index.js +5 -6
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
- package/dist/src-ts/plugins/chatgpt/index.js +127 -7
- package/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/dist/src-ts/plugins/doubao/attachments.js +42 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
- package/dist/src-ts/plugins/doubao/index.js +114 -25
- package/dist/src-ts/plugins/doubao/session-history.js +123 -0
- package/dist/src-ts/plugins/dsh/index.js +18 -2
- package/dist/src-ts/plugins/dsh/session-history.js +371 -0
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
- package/dist/src-ts/plugins/qwenwork/index.js +42 -3
- package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
- package/dist/src-ts/plugins/workbuddy/index.js +23 -3
- package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/dist/src-ts/session-archive.js +509 -0
- package/dist/src-ts/session-history.js +2 -0
- package/dist/src-ts/session-management.js +665 -0
- package/dist/src-ts/session-summary.js +171 -0
- package/dist/tests-ts/aun-attachments.test.js +41 -3
- package/dist/tests-ts/aun-plugin.test.js +28 -5
- package/dist/tests-ts/bundled-package.test.js +3 -1
- package/dist/tests-ts/channel-sessions.test.js +255 -2
- package/dist/tests-ts/chatgpt-history.test.js +197 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
- package/dist/tests-ts/control-server.test.js +39 -0
- package/dist/tests-ts/desktop-launch.test.js +41 -0
- package/dist/tests-ts/doubao-history.test.js +172 -0
- package/dist/tests-ts/doubao-plugin.test.js +549 -2
- package/dist/tests-ts/dsh-agent.test.js +199 -0
- package/dist/tests-ts/dsh-history.test.js +165 -0
- package/dist/tests-ts/native-session-management.test.js +195 -0
- package/dist/tests-ts/plugin-manager.test.js +354 -4
- package/dist/tests-ts/qwenwork-history.test.js +339 -0
- package/dist/tests-ts/qwenwork-images.test.js +166 -0
- package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
- package/dist/tests-ts/session-archive.test.js +422 -0
- package/dist/tests-ts/session-management.test.js +323 -0
- package/dist/tests-ts/session-summary.test.js +199 -0
- package/dist/tests-ts/workbuddy-history.test.js +256 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
- package/package.json +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +127 -7
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +14 -1
- package/plugins/doubao/config.schema.json +3 -0
- package/plugins/doubao/dist/src-ts/attachments.js +62 -0
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +114 -25
- package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -1
- package/plugins/dsh/config.schema.json +2 -1
- package/plugins/dsh/dist/src-ts/attachments.js +62 -0
- package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
- package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +18 -2
- package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
- package/plugins/dsh/package.json +5 -2
- package/plugins/dsh/plugin.json +1 -1
- package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
- package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +42 -3
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/config.schema.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +23 -3
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/plugins/workbuddy/package.json +1 -1
- 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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
|
228
|
-
|
|
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.
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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) {
|