mu-agent-bridge 1.0.32 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +1 -15
  2. package/dist/src-ts/attachments.js +62 -0
  3. package/dist/src-ts/channel-sessions.js +8 -7
  4. package/dist/src-ts/cli-commands.js +2 -6
  5. package/dist/src-ts/control-server.js +0 -2
  6. package/dist/src-ts/daemon-process.js +2 -3
  7. package/dist/src-ts/dsh-agent.js +2 -4
  8. package/dist/src-ts/plugin-manager.js +29 -25
  9. package/dist/src-ts/plugins/aun/attachments.js +245 -0
  10. package/dist/src-ts/plugins/aun/index.js +117 -14
  11. package/dist/src-ts/plugins/aun/messages.js +25 -1
  12. package/dist/src-ts/plugins/cdp-draft.js +54 -0
  13. package/dist/src-ts/plugins/chatgpt/cdp-client.js +10 -1
  14. package/dist/src-ts/plugins/chatgpt/index.js +16 -6
  15. package/dist/src-ts/plugins/doubao/cdp-client.js +72 -7
  16. package/dist/src-ts/plugins/doubao/index.js +2 -2
  17. package/dist/src-ts/plugins/dsh/index.js +0 -1
  18. package/dist/src-ts/plugins/feishu/index.js +8 -21
  19. package/dist/src-ts/plugins/feishu/public/app.js +8 -20
  20. package/dist/src-ts/plugins/feishu/public/index.html +1 -1
  21. package/dist/src-ts/plugins/qwenwork/cdp-client.js +26 -0
  22. package/dist/src-ts/plugins/qwenwork/index.js +2 -1
  23. package/dist/src-ts/plugins/workbuddy/cdp-client.js +330 -12
  24. package/dist/src-ts/plugins/workbuddy/index.js +56 -11
  25. package/dist/tests-ts/attachments.test.js +89 -0
  26. package/dist/tests-ts/aun-attachments.test.js +209 -0
  27. package/dist/tests-ts/aun-messages.test.js +41 -0
  28. package/dist/tests-ts/aun-plugin.test.js +222 -6
  29. package/dist/tests-ts/cdp-draft-recovery.test.js +149 -0
  30. package/dist/tests-ts/channel-sessions.test.js +0 -22
  31. package/dist/tests-ts/chatgpt-plugin.test.js +66 -6
  32. package/dist/tests-ts/cli-commands.test.js +0 -58
  33. package/dist/tests-ts/daemon-process.test.js +0 -1
  34. package/dist/tests-ts/doubao-plugin.test.js +143 -6
  35. package/dist/tests-ts/dsh-agent.test.js +0 -25
  36. package/dist/tests-ts/feishu-page.test.js +3 -10
  37. package/dist/tests-ts/feishu-plugin.test.js +1 -24
  38. package/dist/tests-ts/plugin-manager.test.js +61 -97
  39. package/dist/tests-ts/qwenwork-plugin.test.js +3 -7
  40. package/dist/tests-ts/workbuddy-plugin.test.js +589 -7
  41. package/package.json +1 -2
  42. package/plugins/aun/dist/src-ts/attachments.js +62 -0
  43. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +245 -0
  44. package/plugins/aun/dist/src-ts/plugins/aun/index.js +117 -14
  45. package/plugins/aun/dist/src-ts/plugins/aun/messages.js +25 -1
  46. package/plugins/aun/package.json +1 -1
  47. package/plugins/aun/plugin.json +1 -1
  48. package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +54 -0
  49. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +10 -1
  50. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +16 -6
  51. package/plugins/chatgpt/package.json +1 -1
  52. package/plugins/chatgpt/plugin.json +1 -1
  53. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +72 -7
  54. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +2 -2
  55. package/plugins/doubao/package.json +1 -1
  56. package/plugins/doubao/plugin.json +1 -1
  57. package/plugins/dsh/dist/src-ts/dsh-agent.js +2 -4
  58. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +0 -1
  59. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +8 -21
  60. package/plugins/feishu/dist/src-ts/plugins/feishu/public/app.js +8 -20
  61. package/plugins/feishu/dist/src-ts/plugins/feishu/public/index.html +1 -1
  62. package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +54 -0
  63. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +26 -0
  64. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +2 -1
  65. package/plugins/qwenwork/package.json +1 -1
  66. package/plugins/qwenwork/plugin.json +1 -1
  67. package/plugins/workbuddy/dist/src-ts/attachments.js +62 -0
  68. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +330 -12
  69. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +56 -11
  70. package/plugins/workbuddy/package.json +1 -1
  71. package/plugins/workbuddy/plugin.json +1 -1
  72. package/dist/installer/mu-agent-bridge.msi +0 -0
  73. package/dist/src-ts/secret-input.js +0 -47
  74. package/dist/tests-ts/installer-assets.test.js +0 -84
@@ -9,8 +9,9 @@ const node_path_1 = require("node:path");
9
9
  const node_os_1 = require("node:os");
10
10
  const node_url_1 = require("node:url");
11
11
  const messages_1 = require("./messages");
12
+ const attachments_1 = require("./attachments");
12
13
  const manifest = {
13
- id: "aun", version: "1.0.22", type: "channel", displayName: "AUN 通道",
14
+ id: "aun", version: "1.0.23", type: "channel", displayName: "AUN 通道",
14
15
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
15
16
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
16
17
  configSchema: "config.schema.json",
@@ -71,6 +72,8 @@ class AunPlugin {
71
72
  status = "degraded";
72
73
  healthMessage = "尚未配置 AID";
73
74
  seen = new messages_1.DuplicateTracker();
75
+ pendingMessages = new Map();
76
+ acceptingMessages = false;
74
77
  constructor(options = {}) {
75
78
  this.options = options;
76
79
  this.sdk = options.sdk;
@@ -251,6 +254,7 @@ class AunPlugin {
251
254
  }
252
255
  if (this.aid)
253
256
  this.identities.add(this.aid);
257
+ this.acceptingMessages = true;
254
258
  for (const aid of this.identities)
255
259
  await this.connectAid(aid);
256
260
  // 密钥库中的其余 AID 也自动连接:不再依赖 ec 或 EvolCore 进程
@@ -291,10 +295,12 @@ class AunPlugin {
291
295
  }
292
296
  async deactivate(_reason) {
293
297
  this.status = "degraded";
294
- for (const { client, subscription } of this.clients.values()) {
298
+ this.acceptingMessages = false;
299
+ for (const { subscription } of this.clients.values())
295
300
  subscription?.unsubscribe();
301
+ await Promise.allSettled(this.pendingMessages.values());
302
+ for (const { client } of this.clients.values())
296
303
  await client.close();
297
- }
298
304
  this.clients.clear();
299
305
  await this.store?.close();
300
306
  this.store = undefined;
@@ -735,28 +741,109 @@ class AunPlugin {
735
741
  }
736
742
  };
737
743
  async onMessage(payload, aid) {
744
+ if (!this.acceptingMessages)
745
+ return;
738
746
  const message = (0, messages_1.normalizeAunMessage)(payload, aid, typeof this.config.allow_from === "string" ? this.config.allow_from : undefined);
739
747
  if (!message || !this.seen.add(message.messageId ? `${aid}:${message.messageId}` : ""))
740
748
  return;
749
+ const sourceKey = JSON.stringify([aid, message.peerId, message.threadId ?? ""]);
750
+ const pending = this.pendingMessages.get(sourceKey);
751
+ if (pending && !message.attachments?.length && !message.attachmentError && newSessionCommand.test(message.text.trim())) {
752
+ try {
753
+ await this.sendText(aid, message.peerId, "新建会话失败:当前会话正在执行任务,请等待完成后再新建会话", message.threadId);
754
+ }
755
+ catch (error) {
756
+ this.context?.logger.warn("AUN 错误回复失败", { error: safeError(error) });
757
+ }
758
+ return;
759
+ }
760
+ const current = (pending ?? Promise.resolve()).catch(() => undefined).then(() => this.processMessage(message, aid));
761
+ this.pendingMessages.set(sourceKey, current);
762
+ try {
763
+ await current;
764
+ }
765
+ finally {
766
+ if (this.pendingMessages.get(sourceKey) === current)
767
+ this.pendingMessages.delete(sourceKey);
768
+ }
769
+ }
770
+ async processMessage(message, aid) {
741
771
  let replyAttempted = false;
772
+ const client = this.clients.get(aid)?.client;
773
+ let attachments = [];
774
+ let uploadedItems;
775
+ let uploadedRefs;
776
+ try {
777
+ if (message.attachmentError)
778
+ throw new Error(message.attachmentError);
779
+ if (message.attachments?.length) {
780
+ if (!client?.storage)
781
+ throw new Error("AUN SDK 不支持附件存储接口");
782
+ attachments = await (0, attachments_1.downloadAunAttachments)({ storage: client.storage }, message.attachments);
783
+ }
784
+ }
785
+ catch (error) {
786
+ this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error) });
787
+ try {
788
+ await this.sendText(aid, message.peerId, `附件接收失败:${safeError(error)}`, message.threadId);
789
+ }
790
+ catch { /* 尽力反馈 */ }
791
+ return;
792
+ }
742
793
  const channelMessage = {
743
794
  channel: "aun", recipientId: aid, peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
795
+ ...(attachments.length ? { attachments } : {}),
744
796
  agentPlugin: this.bindings[aid] || (typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork"),
745
797
  reply: async (text) => {
746
798
  if (!text.trim())
747
799
  return;
748
800
  replyAttempted = true;
749
- const payload = { type: "text", text };
750
- if (message.threadId)
751
- payload.thread_id = message.threadId;
752
- const params = { to: message.peerId, payload, encrypt: true };
801
+ await this.sendText(aid, message.peerId, text, message.threadId);
802
+ },
803
+ replyAttachments: async (items, text) => {
804
+ if (!items.length)
805
+ return;
806
+ replyAttempted = true;
807
+ let sent = 0;
808
+ let groupsCount = 0;
809
+ let sending = false;
753
810
  try {
754
- await this.clients.get(aid).client.call("message.send", params);
811
+ if (!client?.storage)
812
+ throw new Error("AUN SDK 不支持附件存储接口");
813
+ const refs = uploadedRefs && uploadedItems === items ? uploadedRefs : await (0, attachments_1.uploadAunAttachments)({ storage: client.storage }, aid, message.peerId, items);
814
+ if (!uploadedRefs || uploadedItems !== items) {
815
+ uploadedItems = items;
816
+ uploadedRefs = refs;
817
+ }
818
+ const groups = new Map();
819
+ for (const [index, item] of items.entries()) {
820
+ const kind = item.kind;
821
+ const ref = refs[index];
822
+ const group = groups.get(kind) ?? [];
823
+ group.push(ref);
824
+ groups.set(kind, group);
825
+ }
826
+ groupsCount = groups.size;
827
+ for (const [kind, group] of groups) {
828
+ const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : "文件已生成。"), attachments: group };
829
+ if (message.threadId)
830
+ payload.thread_id = message.threadId;
831
+ if (message.messageId)
832
+ payload.ref_message_id = message.messageId;
833
+ sending = true;
834
+ // message.send 失败时可能已送达,不自动重试或降级明文,避免重复发送和泄露引用。
835
+ await client.call("message.send", { to: message.peerId, payload, encrypt: true });
836
+ sending = false;
837
+ sent++;
838
+ }
755
839
  }
756
840
  catch (error) {
757
- if (!this.isE2eeError(error))
758
- throw error;
759
- await this.clients.get(aid).client.call("message.send", { ...params, encrypt: false });
841
+ const progress = sending ? `(已确认发送 ${sent}/${groupsCount} 组,当前组结果未确认,请勿重复发送)` : "";
842
+ try {
843
+ await this.sendText(aid, message.peerId, `附件回传失败${progress}:${safeError(error)}`, message.threadId);
844
+ }
845
+ catch { /* 尽力反馈 */ }
846
+ throw error;
760
847
  }
761
848
  },
762
849
  };
@@ -765,9 +852,10 @@ class AunPlugin {
765
852
  }
766
853
  catch (error) {
767
854
  this.context?.logger.warn("AUN 消息处理失败", { error: safeError(error) });
768
- if (!replyAttempted && newSessionCommand.test(message.text.trim())) {
855
+ if (!replyAttempted) {
856
+ const action = attachments.length > 0 ? "附件处理失败" : newSessionCommand.test(message.text.trim()) ? "新建会话失败" : "消息处理失败";
769
857
  try {
770
- await channelMessage.reply(`新建会话失败:${safeError(error)}`);
858
+ await channelMessage.reply(`${action}:${safeError(error)}`);
771
859
  }
772
860
  catch (replyError) {
773
861
  this.context?.logger.warn("AUN 错误回复失败", { error: safeError(replyError) });
@@ -775,6 +863,20 @@ class AunPlugin {
775
863
  }
776
864
  }
777
865
  }
866
+ async sendText(aid, peerId, text, threadId) {
867
+ const payload = { type: "text", text };
868
+ if (threadId)
869
+ payload.thread_id = threadId;
870
+ const params = { to: peerId, payload, encrypt: true };
871
+ try {
872
+ await this.clients.get(aid).client.call("message.send", params);
873
+ }
874
+ catch (error) {
875
+ if (!this.isE2eeError(error))
876
+ throw error;
877
+ await this.clients.get(aid).client.call("message.send", { ...params, encrypt: false });
878
+ }
879
+ }
778
880
  isE2eeError(error) {
779
881
  const sdk = this.sdk;
780
882
  return [sdk.E2EEError, sdk.E2EEDecryptFailedError, sdk.E2EEDegradedError].some((type) => !!type && error instanceof type)
@@ -820,5 +922,6 @@ async function readResponse(response, limit) {
820
922
  return Buffer.concat(chunks).toString("utf8");
821
923
  }
822
924
  function redact(message) {
823
- return message.replace(/(private[_ -]?key|seed|token|certificate)\s*[:=]\s*[^\s,;]+/gi, "$1=[已隐藏]");
925
+ return message.replace(/https?:\/\/[^\s"'<>]+/gi, (value) => value.includes("?") ? `${value.slice(0, value.indexOf("?"))}?[已隐藏]` : value)
926
+ .replace(/(private[_ -]?key|seed|token|share_id|certificate)["']?\s*[:=]\s*["']?[^\s,;"'}]+/gi, "$1=[已隐藏]");
824
927
  }
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DuplicateTracker = void 0;
4
4
  exports.normalizeAunMessage = normalizeAunMessage;
5
+ const attachments_1 = require("./attachments");
6
+ const attachments_2 = require("../../attachments");
5
7
  function normalizeAunMessage(payload, selfAid, allowFrom) {
6
8
  if (!payload || typeof payload !== "object")
7
9
  return undefined;
@@ -12,13 +14,35 @@ function normalizeAunMessage(payload, selfAid, allowFrom) {
12
14
  return undefined;
13
15
  const body = data.payload && typeof data.payload === "object" ? data.payload : {};
14
16
  const text = typeof data.payload === "string" ? data.payload : typeof body.text === "string" ? body.text : "";
15
- if (!text.trim())
17
+ let attachments = [];
18
+ let attachmentError;
19
+ if (body.attachments !== undefined) {
20
+ try {
21
+ if (!Array.isArray(body.attachments))
22
+ throw new Error("AUN 附件列表无效");
23
+ if (body.attachments.length > attachments_2.MAX_ATTACHMENTS)
24
+ throw new Error("单条消息最多包含 8 个附件");
25
+ attachments = body.attachments.map((item) => {
26
+ const ref = (0, attachments_1.parseAunAttachment)(item);
27
+ if (!ref.token && ref.ownerAid !== peerId)
28
+ throw new Error("AUN 附件所有者必须是消息对端,第三方对象需要显式分享令牌");
29
+ return ref;
30
+ });
31
+ }
32
+ catch (error) {
33
+ attachments = [];
34
+ attachmentError = error instanceof Error ? error.message : "AUN 附件引用无效";
35
+ }
36
+ }
37
+ if (!text.trim() && !attachments.length && !attachmentError)
16
38
  return undefined;
17
39
  return {
18
40
  peerId,
19
41
  ...(typeof data.message_id === "string" ? { messageId: data.message_id } : {}),
20
42
  text,
21
43
  ...(typeof body.thread_id === "string" ? { threadId: body.thread_id } : {}),
44
+ ...(attachments.length ? { attachments } : {}),
45
+ ...(attachmentError ? { attachmentError } : {}),
22
46
  };
23
47
  }
24
48
  class DuplicateTracker {
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recoverCdpDraft = recoverCdpDraft;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_os_1 = require("node:os");
6
+ const node_path_1 = require("node:path");
7
+ /** 空闲时先备份旧草稿,再通过编辑器原生删除事件清理,避免只修改 DOM。 */
8
+ async function recoverCdpDraft(evaluate, editorSelector, stopSelector, dataDir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-draft-recovery")) {
9
+ const state = await evaluate(`(() => ({
10
+ text: document.querySelector(${JSON.stringify(editorSelector)})?.innerText ?? null,
11
+ generating: Boolean(document.querySelector(${JSON.stringify(stopSelector)})),
12
+ url: document.URL ?? ''
13
+ }))()`);
14
+ if (state.generating)
15
+ throw new Error("桌面端正在生成回复,不能清理输入或切换会话");
16
+ if (state.text === null)
17
+ throw new Error("桌面端输入框未就绪");
18
+ if (!state.text?.trim())
19
+ return;
20
+ await (0, promises_1.mkdir)(dataDir, { recursive: true, mode: 0o700 });
21
+ await (0, promises_1.appendFile)((0, node_path_1.join)(dataDir, "draft-recovery.jsonl"), JSON.stringify({ time: new Date().toISOString(), url: state.url, text: state.text }) + "\n", { encoding: "utf8", mode: 0o600 });
22
+ const cleared = await evaluate(`(() => {
23
+ const editor = document.querySelector(${JSON.stringify(editorSelector)});
24
+ const unchanged = () => editor && editor.innerText === ${JSON.stringify(state.text)}
25
+ && !document.querySelector(${JSON.stringify(stopSelector)});
26
+ if (!unchanged()) return false;
27
+ editor.focus();
28
+ if (!unchanged()) return false;
29
+ // 千问编辑器由 React 管理内部值,优先调用编辑器控制器,避免只清空 DOM。
30
+ let fiber = editor[Object.keys(editor).find(key => key.startsWith('__reactFiber')) ?? ''];
31
+ for (let depth = 0; fiber && depth < 24; depth++, fiber = fiber.return) {
32
+ const controller = fiber.memoizedProps?.ref?.current;
33
+ if (controller && typeof controller.clear === 'function') {
34
+ controller.clear();
35
+ return !editor.innerText.trim();
36
+ }
37
+ if (controller && typeof controller.setValue === 'function') {
38
+ controller.setValue('');
39
+ return !editor.innerText.trim();
40
+ }
41
+ }
42
+ const selection = window.getSelection();
43
+ if (!selection) return false;
44
+ const range = document.createRange();
45
+ range.selectNodeContents(editor);
46
+ selection.removeAllRanges();
47
+ selection.addRange(range);
48
+ document.execCommand('delete');
49
+ const current = document.querySelector(${JSON.stringify(editorSelector)});
50
+ return Boolean(current && !current.innerText.trim());
51
+ })()`);
52
+ if (cleared !== true)
53
+ throw new Error("旧草稿已备份,但输入内容发生变化或清理失败,保留当前内容");
54
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChatgptCdpClient = exports.userBubbleSelector = exports.turnSelector = exports.editorSelector = void 0;
4
4
  const node_child_process_1 = require("node:child_process");
5
5
  const session_store_1 = require("./session-store");
6
+ const cdp_draft_1 = require("../cdp-draft");
6
7
  exports.editorSelector = '[data-codex-composer="true"][contenteditable="true"]';
7
8
  exports.turnSelector = '[data-chatgpt-conversation-turn="true"]';
8
9
  exports.userBubbleSelector = "[data-user-message-bubble]";
@@ -24,12 +25,14 @@ class ChatgptCdpClient {
24
25
  fetchImpl;
25
26
  webSocketFactory;
26
27
  ssh;
28
+ dataDir;
27
29
  constructor(options = {}) {
28
30
  this.port = options.port ?? options.ssh?.localPort ?? 9230;
29
31
  this.timeoutMs = options.timeoutMs ?? 30_000;
30
32
  this.fetchImpl = options.fetchImpl ?? fetch;
31
33
  this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
32
34
  this.ssh = options.ssh;
35
+ this.dataDir = options.dataDir;
33
36
  }
34
37
  connected() { return Boolean(this.socket); }
35
38
  async listSessions() {
@@ -42,7 +45,7 @@ class ChatgptCdpClient {
42
45
  })()`);
43
46
  if (!Array.isArray(entries))
44
47
  throw new Error("ChatGPT 侧栏会话列表格式无效");
45
- return entries.filter(session_store_1.validSession);
48
+ return [...new Map(entries.filter(session_store_1.validSession).map(item => [JSON.stringify([item.host, item.id]), item])).values()];
46
49
  }
47
50
  async activeSession() {
48
51
  const entry = await this.evaluate(`(() => {
@@ -56,6 +59,7 @@ class ChatgptCdpClient {
56
59
  return (0, session_store_1.validSession)(entry) ? entry : undefined;
57
60
  }
58
61
  async switchSession(session) {
62
+ await this.recoverDraft();
59
63
  const locate = `[...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].find(item =>
60
64
  item.getAttribute('data-app-action-sidebar-thread-id') === ${JSON.stringify(session.id)}
61
65
  && item.getAttribute('data-app-action-sidebar-thread-host-id') === ${JSON.stringify(session.host)})`;
@@ -78,6 +82,7 @@ class ChatgptCdpClient {
78
82
  })()`, "ChatGPT 会话切换未确认,未发送消息");
79
83
  }
80
84
  async createSession(_name) {
85
+ await this.recoverDraft();
81
86
  const before = await this.activeSession();
82
87
  const state = await this.evaluate(`JSON.stringify({ editor: document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText ?? null, turns: document.querySelectorAll(${JSON.stringify(exports.turnSelector)}).length, generating: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) })`);
83
88
  const initial = JSON.parse(String(state));
@@ -187,6 +192,7 @@ class ChatgptCdpClient {
187
192
  }
188
193
  async direct(text, timeoutMs = 240_000) {
189
194
  await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "ChatGPT 桌面端输入框未就绪");
195
+ await this.recoverDraft();
190
196
  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 })))`);
191
197
  const beforeTurns = JSON.parse(String(beforeTurnsJson || "[]"));
192
198
  const beforeAssistantCount = Number(await this.evaluate(`document.querySelectorAll(${JSON.stringify(assistantMessageSelector)}).length`));
@@ -306,6 +312,9 @@ class ChatgptCdpClient {
306
312
  });
307
313
  });
308
314
  }
315
+ recoverDraft() {
316
+ return (0, cdp_draft_1.recoverCdpDraft)(this.evaluate.bind(this), exports.editorSelector, stopButtonSelector, this.dataDir);
317
+ }
309
318
  async waitFor(expression, message) {
310
319
  const deadline = Date.now() + this.timeoutMs;
311
320
  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.3",
8
+ version: "1.0.6",
9
9
  displayName: "ChatGPT(Codex)",
10
10
  type: "agent",
11
11
  entry: "dist/index.js",
@@ -60,6 +60,7 @@ class ChatgptPlugin {
60
60
  this.failure = undefined;
61
61
  this.client = this.options.client ?? new cdp_client_1.ChatgptCdpClient({
62
62
  port: port(config.localCdpPort, 9230),
63
+ dataDir: context.dataDir,
63
64
  timeoutMs: typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 30_000,
64
65
  ssh,
65
66
  });
@@ -206,11 +207,20 @@ class ChatgptPlugin {
206
207
  const reply = await this.client.direct(message, request.timeoutMs);
207
208
  const baseline = request.sessionId ? store.pendingBindings[request.sessionId] : undefined;
208
209
  if (baseline) {
209
- const existing = new Set(baseline);
210
- const created = (await this.client.listSessions?.() ?? []).filter(item => !existing.has(JSON.stringify([item.host, item.id])));
211
- if (created.length !== 1)
212
- throw new Error(`ChatGPT 新消息已发送,但新会话标识无法唯一确认(找到 ${created.length} 个)`);
213
- selected = created[0];
210
+ try {
211
+ const existing = new Set(baseline);
212
+ const created = (await this.client.listSessions?.() ?? []).filter(item => !existing.has(JSON.stringify([item.host, item.id])));
213
+ const active = created.length === 1 ? created[0] : await this.client.activeSession?.();
214
+ if (!active || !created.some(item => item.id === active.id && item.host === active.host)) {
215
+ this.logger?.warn("ChatGPT 已生成回复,但新会话仍待确认", { created: created.length });
216
+ return reply;
217
+ }
218
+ selected = active;
219
+ }
220
+ catch (error) {
221
+ this.logger?.warn("ChatGPT 已生成回复,但新会话状态读取失败", { error: error instanceof Error ? error.message : String(error) });
222
+ return reply;
223
+ }
214
224
  delete store.pendingBindings[request.sessionId];
215
225
  }
216
226
  if (selected) {
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DoubaoCdpClient = exports.responseTextScript = void 0;
3
+ exports.DoubaoCdpClient = exports.responseImagesScript = exports.responseTextScript = void 0;
4
4
  exports.doubaoStreamErrorMessage = doubaoStreamErrorMessage;
5
5
  exports.requestBody = requestBody;
6
6
  exports.mergeDoubaoReply = mergeDoubaoReply;
7
7
  exports.cleanDoubaoReply = cleanDoubaoReply;
8
+ exports.downloadDoubaoImage = downloadDoubaoImage;
8
9
  const node_crypto_1 = require("node:crypto");
9
10
  function doubaoStreamErrorMessage(data) {
10
11
  let code = "";
@@ -39,7 +40,7 @@ function mergeDoubaoReply(current, next) {
39
40
  return current;
40
41
  if (!current)
41
42
  return next;
42
- if (current.includes(next))
43
+ if (current.endsWith(next))
43
44
  return current;
44
45
  if (next.includes(current))
45
46
  return next;
@@ -57,6 +58,64 @@ function cleanDoubaoReply(reply, prompt) {
57
58
  return value.slice(input.length).trim();
58
59
  return value;
59
60
  }
61
+ exports.responseImagesScript = `(value) => {
62
+ const urls = new Set();
63
+ const visit = (item) => {
64
+ if (Array.isArray(item)) { for (const child of item) visit(child); return; }
65
+ if (!item || typeof item !== 'object') return;
66
+ for (const creation of item.creation_block?.creations || []) {
67
+ const image = creation.image;
68
+ if (image?.status === 2 && typeof image.image_ori?.url === 'string' && image.image_ori.url) urls.add(image.image_ori.url);
69
+ }
70
+ for (const child of Object.values(item)) visit(child);
71
+ };
72
+ visit(value); return [...urls];
73
+ }`;
74
+ async function downloadDoubaoImage(address, fetchImpl = fetch, signal) {
75
+ const url = new URL(address);
76
+ if (url.protocol !== "https:" || !/^p\d+-flow-imagex-sign\.byteimg\.com$/.test(url.hostname) || url.username || url.password || (url.port && url.port !== "443"))
77
+ throw new Error("豆包图片地址无效");
78
+ const response = await fetchImpl(url.href, { redirect: "error", signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000) });
79
+ if (!response.ok)
80
+ throw new Error(`下载豆包图片 HTTP ${response.status}`);
81
+ const contentType = response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() ?? "";
82
+ const extension = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" }[contentType];
83
+ if (!extension)
84
+ throw new Error("豆包图片类型不支持");
85
+ const limit = 32 * 1024 * 1024;
86
+ if (Number(response.headers.get("content-length")) > limit) {
87
+ await response.body?.cancel();
88
+ throw new Error("豆包图片过大(上限 32 MiB)");
89
+ }
90
+ const reader = response.body?.getReader();
91
+ if (!reader)
92
+ throw new Error("豆包图片内容为空");
93
+ const chunks = [];
94
+ let size = 0;
95
+ try {
96
+ for (;;) {
97
+ const { done, value } = await reader.read();
98
+ if (done)
99
+ break;
100
+ size += value.byteLength;
101
+ if (size > limit) {
102
+ await reader.cancel();
103
+ throw new Error("豆包图片过大(上限 32 MiB)");
104
+ }
105
+ chunks.push(value);
106
+ }
107
+ }
108
+ finally {
109
+ reader.releaseLock();
110
+ }
111
+ const data = Buffer.concat(chunks);
112
+ const valid = contentType === "image/png" ? data.subarray(0, 8).toString("hex") === "89504e470d0a1a0a"
113
+ : contentType === "image/jpeg" ? data.subarray(0, 3).toString("hex") === "ffd8ff"
114
+ : data.subarray(0, 4).toString() === "RIFF" && data.subarray(8, 12).toString() === "WEBP";
115
+ if (!valid)
116
+ throw new Error("豆包图片内容与类型不符");
117
+ return { data, contentType, filename: `doubao-${(0, node_crypto_1.randomUUID)()}.${extension}` };
118
+ }
60
119
  class DoubaoCdpClient {
61
120
  socket;
62
121
  sequence = 0;
@@ -98,6 +157,9 @@ class DoubaoCdpClient {
98
157
  this.pending.clear();
99
158
  }
100
159
  async direct(text, signal) {
160
+ return (await this.complete(text, signal)).text;
161
+ }
162
+ async complete(text, signal) {
101
163
  await this.evaluate(buildInjection(requestBody(text, this.options.conversationId, this.options.botId, this.options.pluginList)));
102
164
  const deadline = Date.now() + this.options.timeoutMs;
103
165
  const requestSignal = signal ? AbortSignal.any([signal, this.closeController.signal]) : this.closeController.signal;
@@ -109,9 +171,12 @@ class DoubaoCdpClient {
109
171
  const parsed = JSON.parse(result);
110
172
  if (!parsed.ok)
111
173
  throw new Error(`豆包请求失败: ${parsed.reason ?? "未知错误"}`);
174
+ const images = [];
175
+ for (const address of new Set(parsed.images ?? []))
176
+ images.push(await downloadDoubaoImage(address, this.options.fetchImpl, requestSignal));
112
177
  const reply = cleanDoubaoReply(parsed.reply ?? "", text);
113
- if (reply)
114
- return reply;
178
+ if (images.length || reply)
179
+ return { text: reply || "图片已生成。", ...(images.length ? { images } : {}) };
115
180
  }
116
181
  throw new Error("等待豆包回复超时");
117
182
  }
@@ -152,11 +217,11 @@ function buildInjection(body) {
152
217
  const response = await fetch(requestUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'include', body: ${JSON.stringify(body)}, signal: window.__mab_doubao_controller.signal });
153
218
  if (!response.ok) throw new Error('HTTP ' + response.status);
154
219
  const reader = response.body?.getReader(); if (!reader) throw new Error('响应无 body');
155
- const decoder = new TextDecoder(); let buffer = '', reply = '', events = [];
156
- const consume = (block) => { const event = (block.match(/^event:\s*(.+)$/m) || [])[1] || ''; const data = (block.match(/^data:\s*(.+)$/m) || [])[1] || ''; if (event) events.push(event); if (event === 'STREAM_ERROR') throw new Error((${doubaoStreamErrorMessage.toString()})(data)); if (!data) return; try { const json = JSON.parse(data); if (event === 'STREAM_CHUNK') for (const patch of (json.patch_op || [])) reply = merge(reply, patch.patch_value?.tts_content || ''); if (event === 'CHUNK_DELTA' && typeof json.text === 'string') reply = merge(reply, json.text); if (event === 'STREAM_MSG_NOTIFY' || event === 'FULL_MSG_NOTIFY') for (const text of (${exports.responseTextScript})(json)) reply = merge(reply, text); if (event === 'SSE_REPLY_END') reply = merge(reply, json.msg_finish_attr?.brief || json.data?.msg_finish_attr?.brief || ''); } catch {} };
220
+ const decoder = new TextDecoder(); let buffer = '', reply = '', events = []; const images = new Set();
221
+ const consume = (block) => { const event = (block.match(/^event:\s*(.+)$/m) || [])[1] || ''; const data = (block.match(/^data:\s*(.+)$/m) || [])[1] || ''; if (event) events.push(event); if (event === 'STREAM_ERROR') throw new Error((${doubaoStreamErrorMessage.toString()})(data)); if (!data) return; try { const json = JSON.parse(data); if (['STREAM_CHUNK', 'STREAM_MSG_NOTIFY', 'FULL_MSG_NOTIFY'].includes(event)) for (const url of (${exports.responseImagesScript})(json)) images.add(url); if (event === 'STREAM_CHUNK') for (const patch of (json.patch_op || [])) reply = merge(reply, patch.patch_value?.tts_content || ''); if (event === 'CHUNK_DELTA' && typeof json.text === 'string') reply = merge(reply, json.text); if (event === 'STREAM_MSG_NOTIFY' || event === 'FULL_MSG_NOTIFY') for (const text of (${exports.responseTextScript})(json)) reply = merge(reply, text); if (event === 'SSE_REPLY_END') reply = merge(reply, json.msg_finish_attr?.brief || json.data?.msg_finish_attr?.brief || ''); } catch {} };
157
222
  for (;;) { const item = await reader.read(); if (item.done) break; buffer += decoder.decode(item.value, { stream: true }).replace(/\r\n/g, '\n'); let index; while ((index = buffer.indexOf('\n\n')) >= 0) { consume(buffer.slice(0, index)); buffer = buffer.slice(index + 2); } }
158
223
  if (buffer.trim()) consume(buffer);
159
- window.__mab_doubao_result = JSON.stringify({ ok: Boolean(reply.trim()), reply, reason: reply.trim() ? '' : 'SSE 未提取到文本; events=' + events.slice(0, 20).join(',') });
224
+ window.__mab_doubao_result = JSON.stringify({ ok: Boolean(reply.trim() || images.size), reply, images: [...images], reason: reply.trim() || images.size ? '' : 'SSE 未提取到文本或图片; events=' + events.slice(0, 20).join(',') });
160
225
  } catch (error) { window.__mab_doubao_result = JSON.stringify({ ok: false, reason: String(error) }); }
161
226
  })(); return 'started';
162
227
  })()`;
@@ -6,7 +6,7 @@ const plugin_1 = require("../../plugin");
6
6
  const desktop_launch_1 = require("../desktop-launch");
7
7
  exports.doubaoManifest = {
8
8
  id: "doubao",
9
- version: "1.0.12",
9
+ version: "1.0.14",
10
10
  displayName: "豆包",
11
11
  entry: "dist/index.js",
12
12
  apiVersion: "1",
@@ -79,7 +79,7 @@ class DoubaoPlugin {
79
79
  async handle(request) {
80
80
  if (this.state !== "ready" || !this.client)
81
81
  throw new Error("豆包插件未就绪");
82
- return { text: await this.client.direct(request.task) };
82
+ return this.client.complete ? this.client.complete(request.task) : { text: await this.client.direct(request.task) };
83
83
  }
84
84
  async handleHttp(request, response) {
85
85
  let raw = "";
@@ -16,7 +16,6 @@ exports.dshManifest = {
16
16
  };
17
17
  class DshPlugin {
18
18
  manifest = exports.dshManifest;
19
- sessionOperations = ["new"];
20
19
  agent;
21
20
  status = "failed";
22
21
  message = "插件尚未启动";
@@ -11,7 +11,6 @@ const commands = [
11
11
  { name: "status", summary: "查看飞书通道状态", usage: "mab feishu status" },
12
12
  { name: "list", summary: "列出机器人连接状态和关联 Agent", usage: "mab feishu list" },
13
13
  { name: "agents", summary: "列出已启动的 Agent 插件", usage: "mab feishu agents" },
14
- { name: "configure", summary: "保存机器人凭据并关联办公 Agent", usage: "mab feishu configure <app-id> [--agent <plugin-id>]", secretPrompt: "请输入飞书 App Secret" },
15
14
  { name: "bind", summary: "将已连接的机器人关联到 Agent", usage: "mab feishu bind <bot-id> <plugin-id>" },
16
15
  { name: "unbind", summary: "解除机器人关联并保留凭据", usage: "mab feishu unbind <bot-id>" },
17
16
  ];
@@ -150,10 +149,8 @@ class FeishuPlugin {
150
149
  }
151
150
  if (body.protocolVersion !== 1 || !Array.isArray(body.argv) || !body.argv.every((arg) => typeof arg === "string"))
152
151
  return this.json(response, 400, { ok: false, message: "命令协议版本或 argv 无效" });
153
- const [command, appId, pluginArg] = body.argv;
154
- const pluginId = command === "configure" && body.argv[2] === "--agent" ? body.argv[3] : pluginArg;
155
- const secret = typeof body.secret === "string" ? body.secret.trim() : "";
156
- const expectedLength = command === "bind" ? 3 : command === "unbind" ? 2 : command === "configure" ? (body.argv.length === 4 ? 4 : 2) : 1;
152
+ const [command, appId, pluginId] = body.argv;
153
+ const expectedLength = command === "bind" ? 3 : command === "unbind" ? 2 : 1;
157
154
  if (!commands.some((item) => item.name === command) || body.argv.length !== expectedLength || body.argv.some((arg) => !arg.trim()))
158
155
  return this.json(response, 400, { ok: false, message: "命令或参数无效,请查看 mab feishu help" });
159
156
  if (command === "status") {
@@ -164,44 +161,34 @@ class FeishuPlugin {
164
161
  const bots = this.bots.map((bot) => ({ app_id: bot.app_id, status: this.botStates.get(bot.app_id)?.status || "degraded", pluginId: this.bindings[bot.app_id] || "" }));
165
162
  return this.json(response, 200, { ok: true, message: bots.map((bot) => `${bot.app_id}:${bot.status},Agent:${bot.pluginId || "未关联"}`).join("\n") || "尚未配置飞书机器人", data: bots });
166
163
  }
167
- if (command === "configure" && (!appId || !secret || body.argv.length === 4 && (body.argv[2] !== "--agent" || !pluginId)))
168
- return this.json(response, 400, { ok: false, message: "用法:mab feishu configure <app-id> [--agent <plugin-id>],Secret 必须输入" });
169
- if (command !== "configure" && command !== "agents" && !this.bots.some((bot) => bot.app_id === appId))
164
+ if (command !== "agents" && !this.bots.some((bot) => bot.app_id === appId))
170
165
  return this.json(response, 400, { ok: false, message: "飞书机器人标识不存在" });
171
166
  if (command === "bind" && this.botStates.get(appId)?.status !== "ready")
172
167
  return this.json(response, 400, { ok: false, message: "飞书机器人当前未连接" });
173
- if (command === "agents" || command === "bind" || command === "configure" && pluginId) {
168
+ if (command === "agents" || command === "bind") {
174
169
  const runtime = this.context?.runtime;
175
170
  const controlUrl = runtime && "controlUrl" in runtime ? runtime.controlUrl : undefined;
176
171
  if (typeof controlUrl !== "string" || !controlUrl)
177
172
  return this.json(response, 503, { ok: false, message: "运行时未提供控制 API 地址,请升级核心并重启后台" });
178
173
  let agents;
179
174
  try {
180
- const result = await fetch(new URL(command === "configure" ? "/api/agents/availability" : "/api/plugins", controlUrl), { signal: AbortSignal.timeout(5000) });
175
+ const result = await fetch(new URL("/api/plugins", controlUrl), { signal: AbortSignal.timeout(5000) });
181
176
  if (!result.ok)
182
177
  throw new Error("控制 API 请求失败");
183
178
  const data = await result.json();
184
- const entries = command === "configure" ? data.agents : data.plugins;
185
- if (!Array.isArray(entries))
179
+ if (!Array.isArray(data.plugins))
186
180
  throw new Error("控制 API 响应无效");
187
- agents = entries.filter((item) => item && typeof item.id === "string" && (command === "configure" ? item.available === true : item.type === "agent" && item.status === "ready")).map((item) => ({ id: item.id }));
181
+ agents = data.plugins.filter((item) => item && item.type === "agent" && item.status === "ready" && typeof item.id === "string").map((item) => ({ id: item.id }));
188
182
  }
189
183
  catch {
190
184
  return this.json(response, 502, { ok: false, message: "无法读取当前 Agent 插件,请检查后台控制 API" });
191
185
  }
192
186
  if (command === "agents")
193
187
  return this.json(response, 200, { ok: true, message: agents.map((agent) => agent.id).join("\n") || "没有已启动的 Agent 插件", data: agents });
194
- if (pluginId && !agents.some((agent) => agent.id === pluginId))
188
+ if (!agents.some((agent) => agent.id === pluginId))
195
189
  return this.json(response, 400, { ok: false, message: "目标 Agent 插件未启动或类型无效" });
196
190
  }
197
191
  try {
198
- if (command === "configure") {
199
- this.saveBot(appId, secret);
200
- await this.persist();
201
- if (pluginId)
202
- await this.bindBot(appId, pluginId);
203
- return this.json(response, 200, { ok: true, message: "飞书凭据已保存,请重载飞书插件以连接机器人", data: { app_id: appId, ...(pluginId ? { pluginId } : {}) } });
204
- }
205
192
  await this.bindBot(appId, command === "bind" ? pluginId : "");
206
193
  this.json(response, 200, { ok: true, message: command === "bind" ? "飞书机器人已关联 Agent" : "飞书机器人已解除 Agent 关联", data: { app_id: appId, ...(command === "bind" ? { pluginId } : {}) } });
207
194
  }