mu-agent-bridge 1.0.34 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +4 -2
  2. package/dist/src-ts/channel-sessions.js +90 -24
  3. package/dist/src-ts/control-server.js +48 -7
  4. package/dist/src-ts/dsh-agent.js +141 -46
  5. package/dist/src-ts/headless-account.patch.yml +8 -1
  6. package/dist/src-ts/plugin-manager.js +69 -3
  7. package/dist/src-ts/plugins/aun/attachments.js +44 -13
  8. package/dist/src-ts/plugins/aun/index.js +5 -6
  9. package/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  10. package/dist/src-ts/plugins/chatgpt/index.js +127 -7
  11. package/dist/src-ts/plugins/desktop-launch.js +19 -0
  12. package/dist/src-ts/plugins/doubao/attachments.js +42 -0
  13. package/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  14. package/dist/src-ts/plugins/doubao/index.js +114 -25
  15. package/dist/src-ts/plugins/doubao/session-history.js +123 -0
  16. package/dist/src-ts/plugins/dsh/index.js +18 -2
  17. package/dist/src-ts/plugins/dsh/session-history.js +371 -0
  18. package/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  19. package/dist/src-ts/plugins/qwenwork/index.js +42 -3
  20. package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  21. package/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  22. package/dist/src-ts/plugins/workbuddy/index.js +23 -3
  23. package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  24. package/dist/src-ts/session-archive.js +509 -0
  25. package/dist/src-ts/session-history.js +2 -0
  26. package/dist/src-ts/session-management.js +665 -0
  27. package/dist/src-ts/session-summary.js +171 -0
  28. package/dist/tests-ts/aun-attachments.test.js +41 -3
  29. package/dist/tests-ts/aun-plugin.test.js +28 -5
  30. package/dist/tests-ts/bundled-package.test.js +3 -1
  31. package/dist/tests-ts/channel-sessions.test.js +255 -2
  32. package/dist/tests-ts/chatgpt-history.test.js +197 -0
  33. package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
  34. package/dist/tests-ts/control-server.test.js +39 -0
  35. package/dist/tests-ts/desktop-launch.test.js +41 -0
  36. package/dist/tests-ts/doubao-history.test.js +172 -0
  37. package/dist/tests-ts/doubao-plugin.test.js +549 -2
  38. package/dist/tests-ts/dsh-agent.test.js +199 -0
  39. package/dist/tests-ts/dsh-history.test.js +165 -0
  40. package/dist/tests-ts/native-session-management.test.js +195 -0
  41. package/dist/tests-ts/plugin-manager.test.js +354 -4
  42. package/dist/tests-ts/qwenwork-history.test.js +339 -0
  43. package/dist/tests-ts/qwenwork-images.test.js +166 -0
  44. package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
  45. package/dist/tests-ts/session-archive.test.js +422 -0
  46. package/dist/tests-ts/session-management.test.js +323 -0
  47. package/dist/tests-ts/session-summary.test.js +199 -0
  48. package/dist/tests-ts/workbuddy-history.test.js +256 -0
  49. package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
  50. package/package.json +1 -1
  51. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
  52. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
  53. package/plugins/aun/package.json +1 -1
  54. package/plugins/aun/plugin.json +1 -1
  55. package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
  56. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  57. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +127 -7
  58. package/plugins/chatgpt/package.json +1 -1
  59. package/plugins/chatgpt/plugin.json +14 -1
  60. package/plugins/doubao/config.schema.json +3 -0
  61. package/plugins/doubao/dist/src-ts/attachments.js +62 -0
  62. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
  63. package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
  64. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  65. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +114 -25
  66. package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
  67. package/plugins/doubao/package.json +1 -1
  68. package/plugins/doubao/plugin.json +1 -1
  69. package/plugins/dsh/config.schema.json +2 -1
  70. package/plugins/dsh/dist/src-ts/attachments.js +62 -0
  71. package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
  72. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
  73. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +18 -2
  74. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
  75. package/plugins/dsh/package.json +5 -2
  76. package/plugins/dsh/plugin.json +1 -1
  77. package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
  78. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
  79. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  80. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +42 -3
  81. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  82. package/plugins/qwenwork/package.json +1 -1
  83. package/plugins/qwenwork/plugin.json +1 -1
  84. package/plugins/workbuddy/config.schema.json +1 -1
  85. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  86. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +23 -3
  87. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  88. package/plugins/workbuddy/package.json +1 -1
  89. package/plugins/workbuddy/plugin.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,8 @@ npm run start
14
14
 
15
15
  默认监听 `http://127.0.0.1:26107`,管理页面为 `http://127.0.0.1:26107/abweb/`,管理 API 为 `/api/health`、`/api/plugins`,千问办公的
16
16
  OpenAI 兼容 API 为 `/v1/models` 和 `/v1/chat/completions`。管理页面位于 `/abweb`。
17
+ 千问办公支持通过 AUN 输入文件、图片和视频附件,并从本轮“已展示最终文件”收集实际输出附件。输入附件文件名会追加本轮唯一后缀并保留扩展名,以便失败时只清理桥接自己的附件。插件等待生成结束后再回传,输出保留文件名和 MIME 类型;桌面客户端不支持的媒体能力以实际回复为准。
18
+
17
19
  千问办公插件使用 CDP `9228`;没有运行千问办公时,daemon 仍会启动,qwenwork 会显示为失败状态,
18
20
  可以通过管理 API 重载。
19
21
 
@@ -42,6 +44,8 @@ abweb 的“配置码”栏;配对码 10 分钟有效,只能使用一次,
42
44
  `AGENT_BRIDGE_PLUGINS_DIR`、`AGENT_BRIDGE_STATE_DIR`。默认插件目录是仓库的 `plugins/`,状态目录
43
45
  是 Windows 的 `%APPDATA%\agent-bridge\state`。
44
46
 
47
+ 跨会话读取支持按来源授权的 AUN/飞书托管记录,以及另外授权的 ChatGPT、WorkBuddy 受限原生历史,并通过独立无工具端点总结。归档默认关闭,千问办公暂不支持原生历史。配置与 `/sessions` 命令见[跨会话读取说明](docs/cross-session-reading.md)。
48
+
45
49
  ## npm 插件
46
50
 
47
51
  默认包 `mu-agent-bridge` 已内置 abweb、AUN、千问办公、DSH、豆包、飞书、WorkBuddy 和 ChatGPT/Codex 八个插件,直接安装即可使用:
@@ -142,5 +146,3 @@ Windows 路径时,可设置 `DSH_INSTALL_DIR`。
142
146
  ## AUN 身份管理
143
147
 
144
148
  创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md);给 Codex、Claude Code 直接执行的 CLI 说明见 [使用说明](使用说明.md)。
145
-
146
- WorkBuddy 执行中的可见消息可经原通道逐条回传,同一请求去重并排除思考过程;配合 `agent-bridge-workbuddy@1.1.11` 使用。插件升级同时刷新内部依赖,避免继续运行旧版客户端。
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChannelSessions = void 0;
4
+ exports.channelSourceKey = channelSourceKey;
4
5
  const node_crypto_1 = require("node:crypto");
5
6
  const promises_1 = require("node:fs/promises");
6
7
  const node_path_1 = require("node:path");
8
+ function channelSourceKey(message) {
9
+ return JSON.stringify([message.channel, message.recipientId || "", message.chatId || "", message.peerId, message.threadId || ""]);
10
+ }
11
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
+ const validSession = (value) => {
13
+ if (!isRecord(value))
14
+ return false;
15
+ return [value.source, value.agentPlugin, value.sessionId, value.updatedAt].every(item => typeof item === "string")
16
+ && (value.newSession === undefined || typeof value.newSession === "boolean");
17
+ };
7
18
  const unsupportedCommands = /^(?:\/(?:s|session|slist|name|rename|del|status|stop|pause|resume|compact|fork|rewind|rw|repair|renew))(?:[ \t]+[^\r\n]*)?$/;
8
19
  class ChannelSessions {
9
20
  directory;
10
21
  sessions = {};
22
+ active = {};
11
23
  loaded;
12
24
  writes = Promise.resolve();
13
25
  pending = new Map();
@@ -18,25 +30,87 @@ class ChannelSessions {
18
30
  this.loaded ??= (async () => {
19
31
  try {
20
32
  const data = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.directory, "channel-sessions.json"), "utf8"));
21
- if (!data || typeof data !== "object" || Array.isArray(data))
33
+ if (!isRecord(data))
22
34
  throw new Error("通道会话文件格式无效");
23
- for (const [key, value] of Object.entries(data)) {
24
- const entry = value;
25
- if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string")
26
- throw new Error("通道会话记录无效");
27
- this.sessions[key] = entry;
35
+ const sessions = {};
36
+ const active = {};
37
+ if (data.version === 1) {
38
+ if (!isRecord(data.sessions) || !isRecord(data.active))
39
+ throw new Error("通道会话文件格式无效");
40
+ for (const [id, entry] of Object.entries(data.sessions)) {
41
+ if (!validSession(entry) || entry.logicalSessionId !== id || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id))
42
+ throw new Error("通道会话记录无效");
43
+ sessions[id] = entry;
44
+ }
45
+ for (const [source, id] of Object.entries(data.active)) {
46
+ if (typeof id !== "string" || sessions[id]?.source !== source)
47
+ throw new Error("通道活动绑定无效");
48
+ active[source] = id;
49
+ }
50
+ }
51
+ else {
52
+ for (const [source, entry] of Object.entries(data)) {
53
+ if (!validSession(entry) || entry.source !== source)
54
+ throw new Error("通道会话记录无效");
55
+ const logicalSessionId = (0, node_crypto_1.randomUUID)();
56
+ sessions[logicalSessionId] = { ...entry, logicalSessionId };
57
+ active[source] = logicalSessionId;
58
+ }
59
+ }
60
+ if (data.version !== 1)
61
+ await this.persist(active, sessions);
62
+ else {
63
+ this.sessions = sessions;
64
+ this.active = active;
28
65
  }
29
66
  }
30
67
  catch (error) {
31
- if (error.code !== "ENOENT")
68
+ if (error.code !== "ENOENT") {
69
+ this.loaded = undefined;
32
70
  throw error;
71
+ }
33
72
  }
34
73
  })();
35
74
  await this.loaded;
36
75
  }
37
- async list() { await this.load(); return Object.values(this.sessions).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); }
76
+ async persist(active, sessions) {
77
+ await (0, promises_1.mkdir)(this.directory, { recursive: true, mode: 0o700 });
78
+ await (0, promises_1.chmod)(this.directory, 0o700);
79
+ const file = (0, node_path_1.join)(this.directory, "channel-sessions.json");
80
+ const temporary = `${file}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
81
+ try {
82
+ await (0, promises_1.writeFile)(temporary, `${JSON.stringify({ version: 1, active, sessions }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
83
+ await (0, promises_1.rename)(temporary, file);
84
+ }
85
+ finally {
86
+ await (0, promises_1.rm)(temporary, { force: true });
87
+ }
88
+ this.active = active;
89
+ this.sessions = sessions;
90
+ }
91
+ save(entry) {
92
+ const save = this.writes.catch(() => { }).then(() => this.persist({ ...this.active, [entry.source]: entry.logicalSessionId }, { ...this.sessions, [entry.logicalSessionId]: entry }));
93
+ this.writes = save;
94
+ return save;
95
+ }
96
+ async list() { await this.load(); return Object.values(this.sessions).map(entry => ({ ...entry })).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); }
97
+ isBusy(source) { return this.pending.has(source); }
98
+ async current(message, agentPlugin) {
99
+ await this.load();
100
+ const source = channelSourceKey(message);
101
+ const current = this.writes.catch(() => { }).then(async () => {
102
+ const entry = this.sessions[this.active[source]];
103
+ if (entry)
104
+ return { ...entry };
105
+ const next = { source, logicalSessionId: (0, node_crypto_1.randomUUID)(), agentPlugin, sessionId: (0, node_crypto_1.randomUUID)(), updatedAt: new Date().toISOString(), newSession: true };
106
+ await this.persist({ ...this.active, [source]: next.logicalSessionId }, { ...this.sessions, [next.logicalSessionId]: next });
107
+ return { ...next };
108
+ });
109
+ this.writes = current.then(() => undefined, () => undefined);
110
+ return current;
111
+ }
38
112
  async run(message, agentPlugin, call, control) {
39
- const source = JSON.stringify([message.channel, message.recipientId || "", message.chatId || "", message.peerId, message.threadId || ""]);
113
+ const source = channelSourceKey(message);
40
114
  const hasAttachments = Boolean(message.attachments?.length);
41
115
  const command = hasAttachments ? undefined : message.text?.trim().match(/^\/new(?:[ \t]+([^\r\n]+))?$/);
42
116
  const unsupported = hasAttachments ? undefined : message.text?.trim().match(unsupportedCommands);
@@ -59,25 +133,17 @@ class ChannelSessions {
59
133
  const previous = this.pending.get(source) ?? Promise.resolve();
60
134
  const turn = previous.catch(() => { }).then(async () => {
61
135
  await this.load();
62
- const entry = this.sessions[source];
63
- const sessionId = !command && entry?.agentPlugin === agentPlugin ? entry.sessionId : (0, node_crypto_1.randomUUID)();
136
+ const active = this.sessions[this.active[source]];
137
+ const entry = !command && active?.agentPlugin === agentPlugin ? active : undefined;
138
+ const sessionId = entry?.sessionId ?? (0, node_crypto_1.randomUUID)();
64
139
  const response = command
65
140
  ? await control({ operation: "new", sessionId, ...(command[1]?.trim() ? { name: command[1].trim() } : {}) })
66
- : await call(sessionId);
141
+ : await call(sessionId, !entry || entry.newSession === true);
67
142
  if (command && (typeof response.sessionId !== "string" || !response.sessionId.trim()))
68
143
  throw new Error("插件未返回有效的新会话标识,原会话绑定保持不变");
69
- const next = { source, agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString() };
70
- const save = this.writes.catch(() => { }).then(async () => {
71
- const snapshot = { ...this.sessions, [source]: next };
72
- await (0, promises_1.mkdir)(this.directory, { recursive: true });
73
- const file = (0, node_path_1.join)(this.directory, "channel-sessions.json");
74
- await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify(snapshot, null, 2)}\n`, { mode: 0o600 });
75
- await (0, promises_1.rename)(`${file}.tmp`, file);
76
- this.sessions = snapshot;
77
- });
78
- this.writes = save;
79
- await save;
80
- return response;
144
+ const next = { source, logicalSessionId: entry?.logicalSessionId ?? (0, node_crypto_1.randomUUID)(), agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString(), ...(command ? { newSession: true } : {}) };
145
+ await this.save(next);
146
+ return command ? response : { ...response, source, logicalSessionId: next.logicalSessionId, agentPlugin, request: message, response };
81
147
  });
82
148
  this.pending.set(source, turn);
83
149
  try {
@@ -135,6 +135,36 @@ class ControlServer {
135
135
  const authorized = (Boolean(this.options.token) && authorization === `Bearer ${this.options.token}`) || (authorization?.startsWith("Bearer ") && (this.pairedTokens.get((0, node_crypto_1.createHash)("sha256").update(authorization.slice(7)).digest("hex")) ?? 0) > Date.now());
136
136
  if (this.options.token && !isLoopback(req.socket.remoteAddress) && !authorized && !(method === "GET" && url.pathname.startsWith("/abweb")))
137
137
  return this.json(res, 401, { error: "unauthorized" });
138
+ if (parts[0] === "api" && parts[1] === "session-history") {
139
+ if (!isLoopback(req.socket.remoteAddress) && !authorized)
140
+ return this.json(res, 401, { error: "会话归档管理仅允许本机或已认证调用方" });
141
+ const history = this.options.manager.sessionManagement;
142
+ const actor = authorized ? "authenticated" : "local";
143
+ if (method === "GET" && parts.length === 3 && parts[2] === "providers")
144
+ return this.json(res, 200, history.providers());
145
+ if (method === "GET" && parts.length === 3 && parts[2] === "config")
146
+ return this.json(res, 200, await history.config());
147
+ if (method === "PATCH" && parts.length === 3 && parts[2] === "config") {
148
+ await history.saveConfig(await this.body(req, 64 * 1024), actor);
149
+ return this.json(res, 200, { ok: true });
150
+ }
151
+ if (method === "DELETE" && parts.length === 2) {
152
+ await history.clear(actor);
153
+ return this.json(res, 200, { ok: true });
154
+ }
155
+ if (method === "DELETE" && parts.length === 4 && parts[2] === "sessions") {
156
+ await history.deleteSession(decodeURIComponent(parts[3]), actor);
157
+ return this.json(res, 200, { ok: true });
158
+ }
159
+ if (method === "POST" && parts.length === 3 && parts[2] === "delete-source") {
160
+ const body = await this.body(req, 64 * 1024);
161
+ if (typeof body.source !== "string")
162
+ return this.json(res, 400, { error: "需要有效的完整来源键" });
163
+ await history.deleteSource(body.source, actor);
164
+ return this.json(res, 200, { ok: true });
165
+ }
166
+ return this.json(res, 404, { error: "会话归档接口不存在" });
167
+ }
138
168
  if (parts[0] === "api" && parts[1] === "agents" && parts[2] && parts[3] === "v1") {
139
169
  const id = decodeURIComponent(parts[2]);
140
170
  if (req.headers["x-evolcore-session-id"] !== undefined && !authorized && !isLoopback(req.socket.remoteAddress))
@@ -312,14 +342,25 @@ class ControlServer {
312
342
  const loaded = this.options.manager.get("aun");
313
343
  return loaded?.record.status === "ready" ? loaded.instance?.identity : undefined;
314
344
  }
315
- async body(req) { let raw = ""; for await (const chunk of req)
316
- raw += chunk; if (!raw)
317
- return {}; try {
318
- return JSON.parse(raw);
345
+ async body(req, maxBytes = Infinity) {
346
+ const chunks = [];
347
+ let size = 0;
348
+ for await (const chunk of req) {
349
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
350
+ size += buffer.length;
351
+ if (size > maxBytes)
352
+ throw Object.assign(new Error("请求正文不能超过 64 KiB"), { statusCode: 413 });
353
+ chunks.push(buffer);
354
+ }
355
+ if (!size)
356
+ return {};
357
+ try {
358
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
359
+ }
360
+ catch {
361
+ throw new Error("请求正文必须是有效 JSON");
362
+ }
319
363
  }
320
- catch {
321
- throw new Error("invalid JSON body");
322
- } }
323
364
  json(res, status, value) { res.statusCode = status; res.setHeader("content-type", "application/json; charset=utf-8"); res.end(JSON.stringify(value)); }
324
365
  }
325
366
  exports.ControlServer = ControlServer;
@@ -2,8 +2,64 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DshAgent = void 0;
4
4
  const node_child_process_1 = require("node:child_process");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_fs_1 = require("node:fs");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = require("node:path");
9
+ const node_os_1 = require("node:os");
5
10
  const agent_1 = require("./agent");
11
+ const attachments_1 = require("./attachments");
6
12
  const dsh_paths_1 = require("./dsh-paths");
13
+ const outputMimeTypes = {
14
+ ".csv": "text/csv", ".html": "text/html", ".json": "application/json", ".md": "text/markdown",
15
+ ".pdf": "application/pdf", ".txt": "text/plain", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
16
+ ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp",
17
+ };
18
+ function digest(data) { return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex"); }
19
+ function outputContentType(filename, data) {
20
+ return (0, attachments_1.detectImageContentType)(data) ?? outputMimeTypes[(0, node_path_1.extname)(filename).toLowerCase()] ?? "application/octet-stream";
21
+ }
22
+ async function readPresentedFiles(directory, paths, cwd, inputs, returnPaths) {
23
+ if (paths.length > attachments_1.MAX_ATTACHMENTS)
24
+ throw new Error("单条消息最多包含 8 个附件");
25
+ const result = [];
26
+ const root = (0, node_path_1.resolve)(directory);
27
+ if (await (0, promises_1.realpath)(directory) !== root || !(await (0, promises_1.lstat)(directory)).isDirectory())
28
+ throw new Error("DSH 输出目录已被替换");
29
+ let total = 0;
30
+ for (const value of paths) {
31
+ const target = (0, node_path_1.resolve)(cwd, value);
32
+ if ((0, node_path_1.dirname)(target) !== root || (0, node_path_1.dirname)(await (0, promises_1.realpath)(target)) !== root)
33
+ throw new Error("DSH 回传附件路径超出输出目录");
34
+ if (!(await (0, promises_1.lstat)(target)).isFile())
35
+ throw new Error("DSH 回传附件不是普通文件");
36
+ const handle = await (0, promises_1.open)(target, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0));
37
+ let data;
38
+ try {
39
+ const info = await handle.stat();
40
+ if (!info.isFile() || info.size < 1 || info.size > attachments_1.MAX_ATTACHMENT_BYTES)
41
+ throw new Error("DSH 回传附件大小无效,单个附件不能超过 32 MiB");
42
+ total += info.size;
43
+ if (total > attachments_1.MAX_TOTAL_ATTACHMENT_BYTES)
44
+ throw new Error("单条消息附件总大小不能超过 64 MiB");
45
+ const buffer = Buffer.alloc(info.size + 1);
46
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
47
+ if (bytesRead !== info.size)
48
+ throw new Error("DSH 回传附件大小发生变化");
49
+ data = new Uint8Array(buffer.subarray(0, bytesRead));
50
+ }
51
+ finally {
52
+ await handle.close();
53
+ }
54
+ const source = inputs[returnPaths.indexOf(target)];
55
+ const original = source && digest(source.data) === digest(data) ? source : inputs.find((input) => input.filename === (0, node_path_1.basename)(target) && digest(input.data) === digest(data));
56
+ const filename = (0, attachments_1.attachmentFilename)(original?.filename ?? (0, node_path_1.basename)(target));
57
+ const contentType = original?.contentType ?? outputContentType(filename, data);
58
+ result.push({ kind: contentType.startsWith("image/") ? "image" : "file", data, filename, contentType });
59
+ }
60
+ (0, attachments_1.validateAttachments)(result);
61
+ return result;
62
+ }
7
63
  class DshAgent {
8
64
  name = "dsh";
9
65
  paths;
@@ -17,54 +73,93 @@ class DshAgent {
17
73
  async send(request) {
18
74
  if (!request.task.trim())
19
75
  throw new agent_1.AgentError("DSH task must not be empty");
20
- const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
21
- if (request.sessionId)
22
- args.push("--session-id", request.sessionId);
23
- args.push("-");
24
- const child = this.spawnProcess(this.paths.executable, args, {
25
- cwd: request.cwd,
26
- env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
27
- stdio: ["pipe", "pipe", "pipe"],
28
- });
29
- child.stdin.end(request.task, "utf8");
30
- let output = "";
31
- let errors = "";
32
- let sessionId = request.sessionId;
33
- let finalText = "";
34
- child.stdout.setEncoding?.("utf8");
35
- child.stderr.setEncoding?.("utf8");
36
- child.stdout.on("data", (chunk) => {
37
- output += chunk;
38
- for (const line of output.split(/\r?\n/).slice(0, -1)) {
39
- try {
40
- const event = JSON.parse(line);
41
- if (event.type === "session" && event.sessionId)
42
- sessionId = event.sessionId;
43
- if (event.type === "final" && event.text)
44
- finalText = event.text.trim();
76
+ (0, attachments_1.validateAttachments)(request.attachments);
77
+ const workDirectory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-dsh-"));
78
+ let failed = false;
79
+ try {
80
+ const outputDirectory = (0, node_path_1.join)(workDirectory, "output");
81
+ await (0, promises_1.mkdir)(outputDirectory, { recursive: true });
82
+ for (const [index, item] of (request.attachments ?? []).entries())
83
+ await (0, promises_1.writeFile)((0, node_path_1.join)(workDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), item.data, { mode: 0o600 });
84
+ const inputPaths = (request.attachments ?? []).map((item, index) => ({ path: (0, node_path_1.join)(workDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), returnPath: (0, node_path_1.join)(outputDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), filename: item.filename, contentType: item.contentType }));
85
+ for (const [index, item] of (request.attachments ?? []).entries())
86
+ await (0, promises_1.writeFile)(inputPaths[index].returnPath, item.data, { mode: 0o600 });
87
+ const task = `${request.task}\n\n请使用真实文件工具读取输入附件;生成的回传文件必须写入 outputDirectory 的直接子文件,并调用 present 工具声明文件。原附件已准备 returnPath 副本,原样回传时直接 present 此路径,无需 shell 复制。图片或视频无法理解时请明确说明,仍可原样回传。\n附件传输:\n${JSON.stringify({ inputs: inputPaths, outputDirectory })}`;
88
+ const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
89
+ if (request.sessionId && !request.newSession)
90
+ args.push("--session-id", request.sessionId);
91
+ args.push("-");
92
+ const child = this.spawnProcess(this.paths.executable, args, {
93
+ cwd: request.cwd,
94
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
95
+ stdio: ["pipe", "pipe", "pipe"],
96
+ });
97
+ let output = "";
98
+ let errors = "";
99
+ let sessionId = request.newSession ? undefined : request.sessionId;
100
+ let sessionCwd = request.cwd ?? process.cwd();
101
+ let finalText = "";
102
+ const presented = new Set();
103
+ const presentCalls = new Map();
104
+ child.stdout.setEncoding?.("utf8");
105
+ child.stderr.setEncoding?.("utf8");
106
+ child.stdout.on("data", (chunk) => {
107
+ output += chunk;
108
+ for (const line of output.split(/\r?\n/).slice(0, -1)) {
109
+ try {
110
+ const event = JSON.parse(line);
111
+ if (event.type === "session" && event.sessionId)
112
+ sessionId = event.sessionId;
113
+ if (event.type === "session" && event.cwd)
114
+ sessionCwd = event.cwd;
115
+ if (event.type === "final" && event.text)
116
+ finalText = event.text.trim();
117
+ if (event.type === "tool_call" && event.tool === "present" && event.callId)
118
+ presentCalls.set(event.callId, (event.input?.files ?? []).map((file) => file.path).filter((path) => typeof path === "string"));
119
+ if (event.type === "tool_result" && event.status === "completed" && event.callId)
120
+ for (const path of presentCalls.get(event.callId) ?? [])
121
+ presented.add(path);
122
+ }
123
+ catch { /* 忽略诊断输出和未完成的 JSON 行。 */ }
45
124
  }
46
- catch { /* diagnostics and partial lines are ignored */ }
47
- }
48
- output = output.split(/\r?\n/).at(-1) ?? "";
49
- });
50
- child.stderr.on("data", (chunk) => { errors += chunk; });
51
- await new Promise((resolve, reject) => {
52
- const timer = setTimeout(() => {
53
- child.kill();
54
- reject(new agent_1.AgentError(`DSH task timed out after ${request.timeoutMs ?? this.defaultTimeoutMs}ms`));
55
- }, request.timeoutMs ?? this.defaultTimeoutMs);
56
- child.once("error", (error) => { clearTimeout(timer); reject(new agent_1.AgentError("failed to start DSH", { cause: error })); });
57
- child.once("close", (code) => {
58
- clearTimeout(timer);
59
- if (code !== 0 && !finalText)
60
- reject(new agent_1.AgentError(errors.trim() || `DSH exited with code ${code}`));
61
- else
62
- resolve();
125
+ output = output.split(/\r?\n/).at(-1) ?? "";
126
+ });
127
+ child.stderr.on("data", (chunk) => { errors += chunk; });
128
+ await new Promise((resolve, reject) => {
129
+ const timer = setTimeout(() => {
130
+ child.kill();
131
+ reject(new agent_1.AgentError(`DSH task timed out after ${request.timeoutMs ?? this.defaultTimeoutMs}ms`));
132
+ }, request.timeoutMs ?? this.defaultTimeoutMs);
133
+ child.once("error", (error) => { clearTimeout(timer); reject(new agent_1.AgentError("failed to start DSH", { cause: error })); });
134
+ child.once("close", (code) => {
135
+ clearTimeout(timer);
136
+ if (code !== 0 && !finalText)
137
+ reject(new agent_1.AgentError(errors.trim() || `DSH exited with code ${code}`));
138
+ else
139
+ resolve();
140
+ });
141
+ child.stdin.end(task, "utf8");
63
142
  });
64
- });
65
- if (!finalText)
66
- throw new agent_1.AgentError("DSH returned no final text");
67
- return { text: finalText, sessionId };
143
+ if (request.newSession && !sessionId)
144
+ throw new agent_1.AgentError("DSH 未返回真实新会话标识,无法确认后续续聊目标");
145
+ const attachments = await readPresentedFiles(outputDirectory, [...presented], sessionCwd, request.attachments ?? [], inputPaths.map((item) => item.returnPath));
146
+ if (!finalText && !attachments.length)
147
+ throw new agent_1.AgentError("DSH 未返回正文或附件");
148
+ return { text: finalText, sessionId, ...(attachments?.length ? { attachments } : {}) };
149
+ }
150
+ catch (error) {
151
+ failed = true;
152
+ throw error;
153
+ }
154
+ finally {
155
+ try {
156
+ await (0, promises_1.rm)(workDirectory, { recursive: true, force: true });
157
+ }
158
+ catch (error) {
159
+ if (!failed)
160
+ throw error;
161
+ }
162
+ }
68
163
  }
69
164
  }
70
165
  exports.DshAgent = DshAgent;
@@ -1,5 +1,12 @@
1
- # Select the account-backed model used by the DeepSeek Harness desktop login.
1
+ # 复用 DeepSeek Harness 桌面端登录账号的模型。
2
2
  - id: agent-default-model
3
3
  config:
4
4
  provider: deepseek-account
5
5
  model: deepseek-chat
6
+
7
+ # headless 默认未挂载交付工具,显式启用真实文件交付事件。
8
+ - insert:
9
+ - id: tool-present
10
+ name: '@deepseek-ai/dsh-tool-present'
11
+ config:
12
+ maxFiles: 8
@@ -18,6 +18,7 @@ const version_manager_1 = require("./version-manager");
18
18
  const plugin_catalog_1 = require("./plugin-catalog");
19
19
  const log_store_1 = require("./log-store");
20
20
  const channel_sessions_1 = require("./channel-sessions");
21
+ const session_management_1 = require("./session-management");
21
22
  const _2020_1 = __importDefault(require("ajv/dist/2020"));
22
23
  const attachments_1 = require("./attachments");
23
24
  const logger = { info() { }, warn() { }, error() { } };
@@ -26,9 +27,11 @@ class PluginManager {
26
27
  options;
27
28
  controlUrl;
28
29
  plugins = new Map();
30
+ upgrades = new Map();
29
31
  httpRoutes = new Map();
30
32
  root;
31
33
  messages = [];
34
+ activeIngress = new Map();
32
35
  trustConfig = defaultPluginTrustConfig();
33
36
  trustLoaded = false;
34
37
  publisherLookups = new Map();
@@ -36,12 +39,14 @@ class PluginManager {
36
39
  versions;
37
40
  logs;
38
41
  sessions;
42
+ sessionManagement;
39
43
  constructor(options) {
40
44
  this.options = options;
41
45
  this.root = (0, node_path_1.resolve)(options.pluginsDir);
42
46
  this.versions = options.versions ?? new version_manager_1.VersionManager({ stateDir: options.stateDir });
43
47
  this.logs = new log_store_1.LogStore((0, node_path_1.join)(options.stateDir, "logs"));
44
48
  this.sessions = new channel_sessions_1.ChannelSessions(options.stateDir);
49
+ this.sessionManagement = new session_management_1.SessionManagement(options.stateDir, (operation, fields) => this.log("info", operation, fields, "会话管理"), { list: () => this.sessions.list(), isBusy: source => this.sessions.isBusy(source) || this.activeIngress.has(source) }, () => [...this.plugins.entries()].flatMap(([pluginId, loaded]) => loaded.instance?.sessionHistory ? [{ pluginId, provider: loaded.instance.sessionHistory, identity: loaded.instance, status: loaded.record.status }] : []));
45
50
  }
46
51
  log(level, message, fields, source = "mab") { this.logs.append({ time: new Date().toISOString(), level, source, message, ...(fields ? { fields } : {}) }); }
47
52
  setControlUrl(url) { this.controlUrl = url; }
@@ -162,6 +167,22 @@ class PluginManager {
162
167
  })]);
163
168
  }
164
169
  async upgradePackage(id, packageName = `agent-bridge-${id}`, version) {
170
+ const pending = this.upgrades.get(id);
171
+ if (pending) {
172
+ if (pending.packageName === packageName && pending.version === version)
173
+ return pending.task;
174
+ throw new agent_1.AgentError("插件正在升级,请等待完成后再请求其他版本");
175
+ }
176
+ const task = this.performUpgrade(id, packageName, version);
177
+ this.upgrades.set(id, { packageName, version, task });
178
+ try {
179
+ return await task;
180
+ }
181
+ finally {
182
+ this.upgrades.delete(id);
183
+ }
184
+ }
185
+ async performUpgrade(id, packageName, version) {
165
186
  if (!/^agent-bridge-[a-z0-9-]+$/.test(packageName))
166
187
  throw new agent_1.AgentError("仅支持 agent-bridge-* npm 包名");
167
188
  if (version && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
@@ -358,20 +379,41 @@ class PluginManager {
358
379
  pluginStatus: (targetId) => this.plugins.get(targetId)?.record.status,
359
380
  ingress: {
360
381
  publish: async (message) => {
382
+ if (message.channel !== id)
383
+ throw new agent_1.AgentError("消息来源与通道插件身份不一致");
384
+ const trustedChannel = loaded.record.type === "channel" && (id === "aun" || id === "feishu");
385
+ if (trustedChannel && (typeof message.peerId !== "string" || !message.peerId.trim() || [message.peerId, message.recipientId, message.chatId, message.threadId].some(value => value !== undefined && (typeof value !== "string" || value.length > 512 || /[\u0000-\u001f*]/.test(value)))))
386
+ throw new agent_1.AgentError("通道来源标识无效");
361
387
  const target = message.agentPlugin || "qwenwork";
388
+ if (/^\/sessions(?:\s|$)/.test(message.text.trimStart())) {
389
+ if (!trustedChannel)
390
+ throw new agent_1.AgentError("当前通道不支持会话管理");
391
+ const current = await this.sessions.current(message, target);
392
+ const result = await this.sessionManagement.command(message.text, { source: current.source, sessionId: current.logicalSessionId });
393
+ await message.reply(result);
394
+ return;
395
+ }
396
+ if (trustedChannel)
397
+ await this.sessionManagement.ready();
398
+ const archiveToken = trustedChannel ? this.sessionManagement.archiveWriteToken((0, channel_sessions_1.channelSourceKey)(message)) : undefined;
362
399
  this.recordMessage(target, "request", message.text, message.peerId);
400
+ const source = (0, channel_sessions_1.channelSourceKey)(message);
401
+ if (trustedChannel)
402
+ this.activeIngress.set(source, (this.activeIngress.get(source) ?? 0) + 1);
363
403
  try {
364
404
  const instance = this.plugins.get(target)?.instance;
365
- const control = instance?.sessionOperations?.includes("new") && instance.controlSession
405
+ const control = instance?.sessionOperations?.includes("new")
366
406
  ? (request) => this.controlSession(target, request)
367
407
  : undefined;
368
408
  const task = !message.text.trim() && message.attachments?.length ? "请处理收到的附件" : message.text;
369
409
  const forwarded = new Set();
410
+ const sent = [];
370
411
  const onMessage = async (text) => {
371
412
  if (!text.trim() || forwarded.has(text))
372
413
  return;
373
414
  await message.reply(text);
374
415
  forwarded.add(text);
416
+ sent.push({ text });
375
417
  this.recordMessage(target, "reply", text, message.peerId);
376
418
  };
377
419
  const response = await this.sessions.run(message, target, (sessionId, newSession) => this.handle(target, { task, sessionId, onMessage, ...(newSession ? { newSession: true } : {}), ...(message.attachments ? { attachments: message.attachments } : {}) }), control);
@@ -383,15 +425,37 @@ class PluginManager {
383
425
  throw new agent_1.AgentError("当前通道不支持附件回传");
384
426
  }
385
427
  await message.replyAttachments(attachments, response.text);
428
+ sent.push({ text: response.text, attachments });
386
429
  this.recordMessage(target, "reply", response.text, message.peerId);
387
430
  }
388
431
  else
389
432
  await onMessage(response.text);
433
+ if (trustedChannel && response.source && response.logicalSessionId) {
434
+ const identity = { sessionId: response.logicalSessionId, source: response.source, principal: response.source, channel: message.channel, agentPlugin: target, completed: true, time: new Date().toISOString() };
435
+ const requestId = message.messageId || (0, node_crypto_1.randomUUID)();
436
+ const metadata = (items) => items?.map(item => ({ filename: item.filename, contentType: item.contentType, sizeBytes: item.data.byteLength }));
437
+ try {
438
+ await this.sessionManagement.recordIngressTurns(source, archiveToken, [
439
+ { ...identity, role: "user", text: message.text, requestId, messageId: message.messageId ? `${message.messageId}:user` : undefined, attachments: metadata(message.attachments) },
440
+ ...sent.map((item, index) => ({ ...identity, role: "assistant", text: item.text, requestId, messageId: message.messageId ? `${message.messageId}:assistant:${index}` : undefined, attachments: metadata(item.attachments) })),
441
+ ]);
442
+ }
443
+ catch {
444
+ this.log("warn", "会话归档失败", { sessionId: identity.sessionId, status: "failed" }, "会话管理");
445
+ }
446
+ }
390
447
  }
391
448
  catch (error) {
392
449
  this.recordMessage(target, "error", error instanceof Error ? error.message : String(error), message.peerId);
393
450
  throw error;
394
451
  }
452
+ finally {
453
+ const remaining = (this.activeIngress.get(source) ?? 1) - 1;
454
+ if (remaining)
455
+ this.activeIngress.set(source, remaining);
456
+ else
457
+ this.activeIngress.delete(source);
458
+ }
395
459
  },
396
460
  },
397
461
  runtime: { controlUrl: this.controlUrl, ports: {
@@ -557,9 +621,11 @@ class PluginManager {
557
621
  const loaded = this.plugins.get(id);
558
622
  if (!loaded?.instance || loaded.record.status !== "ready")
559
623
  throw new agent_1.AgentError(`插件 ${id} 未就绪,无法控制会话`);
560
- if (!loaded.instance.sessionOperations?.includes(request.operation) || !loaded.instance.controlSession)
624
+ if (!loaded.instance.sessionOperations?.includes(request.operation))
561
625
  throw new agent_1.AgentError(`插件 ${id} 不支持新建会话`);
562
- const response = await loaded.instance.controlSession(request);
626
+ const response = loaded.instance.controlSession
627
+ ? await loaded.instance.controlSession(request)
628
+ : { text: `${loaded.record.displayName} 已创建新会话`, sessionId: request.sessionId };
563
629
  if (typeof response.sessionId !== "string" || !response.sessionId.trim())
564
630
  throw new agent_1.AgentError("插件未返回有效的新会话标识,原会话绑定保持不变");
565
631
  return response;