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.
- package/README.md +2 -2
- package/dist/src-ts/channel-sessions.js +3 -3
- 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 +22 -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 +270 -9
- package/dist/src-ts/plugins/chatgpt/index.js +6 -4
- 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 +302 -45
- package/dist/src-ts/plugins/doubao/index.js +37 -24
- package/dist/src-ts/plugins/dsh/index.js +3 -1
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +263 -10
- package/dist/src-ts/plugins/qwenwork/index.js +31 -3
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +9 -2
- package/dist/src-ts/plugins/workbuddy/index.js +3 -3
- 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 +22 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
- package/dist/tests-ts/desktop-launch.test.js +41 -0
- package/dist/tests-ts/doubao-plugin.test.js +428 -2
- package/dist/tests-ts/dsh-agent.test.js +199 -0
- package/dist/tests-ts/plugin-manager.test.js +100 -1
- package/dist/tests-ts/qwenwork-images.test.js +166 -0
- package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
- 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 +270 -9
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +6 -4
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +1 -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 +302 -45
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +37 -24
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -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 +3 -1
- 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 +263 -10
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +31 -3
- 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 +9 -2
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +3 -3
- package/plugins/workbuddy/package.json +1 -1
- 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)
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
}`;
|