mu-agent-bridge 1.0.33 → 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.
Files changed (65) hide show
  1. package/README.md +2 -0
  2. package/dist/src-ts/channel-sessions.js +3 -3
  3. package/dist/src-ts/dsh-agent.js +141 -46
  4. package/dist/src-ts/headless-account.patch.yml +8 -1
  5. package/dist/src-ts/plugin-manager.js +38 -8
  6. package/dist/src-ts/plugins/aun/attachments.js +44 -13
  7. package/dist/src-ts/plugins/aun/index.js +5 -6
  8. package/dist/src-ts/plugins/chatgpt/cdp-client.js +270 -9
  9. package/dist/src-ts/plugins/chatgpt/index.js +6 -4
  10. package/dist/src-ts/plugins/desktop-launch.js +19 -0
  11. package/dist/src-ts/plugins/doubao/attachments.js +42 -0
  12. package/dist/src-ts/plugins/doubao/cdp-client.js +302 -45
  13. package/dist/src-ts/plugins/doubao/index.js +37 -24
  14. package/dist/src-ts/plugins/dsh/index.js +3 -1
  15. package/dist/src-ts/plugins/qwenwork/cdp-client.js +263 -10
  16. package/dist/src-ts/plugins/qwenwork/index.js +31 -3
  17. package/dist/src-ts/plugins/workbuddy/cdp-client.js +139 -25
  18. package/dist/src-ts/plugins/workbuddy/index.js +43 -9
  19. package/dist/tests-ts/aun-attachments.test.js +41 -3
  20. package/dist/tests-ts/aun-plugin.test.js +28 -5
  21. package/dist/tests-ts/bundled-package.test.js +3 -1
  22. package/dist/tests-ts/channel-sessions.test.js +22 -0
  23. package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
  24. package/dist/tests-ts/desktop-launch.test.js +41 -0
  25. package/dist/tests-ts/doubao-plugin.test.js +428 -2
  26. package/dist/tests-ts/dsh-agent.test.js +199 -0
  27. package/dist/tests-ts/plugin-manager.test.js +100 -1
  28. package/dist/tests-ts/qwenwork-images.test.js +166 -0
  29. package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
  30. package/dist/tests-ts/workbuddy-plugin.test.js +283 -9
  31. package/package.json +1 -1
  32. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
  33. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
  34. package/plugins/aun/package.json +1 -1
  35. package/plugins/aun/plugin.json +1 -1
  36. package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
  37. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +270 -9
  38. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +6 -4
  39. package/plugins/chatgpt/package.json +1 -1
  40. package/plugins/chatgpt/plugin.json +1 -1
  41. package/plugins/doubao/config.schema.json +3 -0
  42. package/plugins/doubao/dist/src-ts/attachments.js +62 -0
  43. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
  44. package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
  45. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +302 -45
  46. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +37 -24
  47. package/plugins/doubao/package.json +1 -1
  48. package/plugins/doubao/plugin.json +1 -1
  49. package/plugins/dsh/dist/src-ts/attachments.js +62 -0
  50. package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
  51. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
  52. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +3 -1
  53. package/plugins/dsh/package.json +5 -2
  54. package/plugins/dsh/plugin.json +1 -1
  55. package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
  56. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
  57. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +263 -10
  58. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +31 -3
  59. package/plugins/qwenwork/package.json +1 -1
  60. package/plugins/qwenwork/plugin.json +1 -1
  61. package/plugins/workbuddy/config.schema.json +1 -1
  62. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +139 -25
  63. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +43 -9
  64. package/plugins/workbuddy/package.json +1 -1
  65. package/plugins/workbuddy/plugin.json +3 -3
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
 
@@ -22,7 +22,7 @@ class ChannelSessions {
22
22
  throw new Error("通道会话文件格式无效");
23
23
  for (const [key, value] of Object.entries(data)) {
24
24
  const entry = value;
25
- if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string")
25
+ if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string" || (entry.newSession !== undefined && typeof entry.newSession !== "boolean"))
26
26
  throw new Error("通道会话记录无效");
27
27
  this.sessions[key] = entry;
28
28
  }
@@ -63,10 +63,10 @@ class ChannelSessions {
63
63
  const sessionId = !command && entry?.agentPlugin === agentPlugin ? entry.sessionId : (0, node_crypto_1.randomUUID)();
64
64
  const response = command
65
65
  ? await control({ operation: "new", sessionId, ...(command[1]?.trim() ? { name: command[1].trim() } : {}) })
66
- : await call(sessionId);
66
+ : await call(sessionId, !entry || entry.agentPlugin !== agentPlugin || entry.newSession === true);
67
67
  if (command && (typeof response.sessionId !== "string" || !response.sessionId.trim()))
68
68
  throw new Error("插件未返回有效的新会话标识,原会话绑定保持不变");
69
- const next = { source, agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString() };
69
+ const next = { source, agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString(), ...(command ? { newSession: true } : {}) };
70
70
  const save = this.writes.catch(() => { }).then(async () => {
71
71
  const snapshot = { ...this.sessions, [source]: next };
72
72
  await (0, promises_1.mkdir)(this.directory, { recursive: true });
@@ -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
@@ -26,6 +26,7 @@ class PluginManager {
26
26
  options;
27
27
  controlUrl;
28
28
  plugins = new Map();
29
+ upgrades = new Map();
29
30
  httpRoutes = new Map();
30
31
  root;
31
32
  messages = [];
@@ -162,6 +163,22 @@ class PluginManager {
162
163
  })]);
163
164
  }
164
165
  async upgradePackage(id, packageName = `agent-bridge-${id}`, version) {
166
+ const pending = this.upgrades.get(id);
167
+ if (pending) {
168
+ if (pending.packageName === packageName && pending.version === version)
169
+ return pending.task;
170
+ throw new agent_1.AgentError("插件正在升级,请等待完成后再请求其他版本");
171
+ }
172
+ const task = this.performUpgrade(id, packageName, version);
173
+ this.upgrades.set(id, { packageName, version, task });
174
+ try {
175
+ return await task;
176
+ }
177
+ finally {
178
+ this.upgrades.delete(id);
179
+ }
180
+ }
181
+ async performUpgrade(id, packageName, version) {
165
182
  if (!/^agent-bridge-[a-z0-9-]+$/.test(packageName))
166
183
  throw new agent_1.AgentError("仅支持 agent-bridge-* npm 包名");
167
184
  if (version && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
@@ -362,11 +379,19 @@ class PluginManager {
362
379
  this.recordMessage(target, "request", message.text, message.peerId);
363
380
  try {
364
381
  const instance = this.plugins.get(target)?.instance;
365
- const control = instance?.sessionOperations?.includes("new") && instance.controlSession
382
+ const control = instance?.sessionOperations?.includes("new")
366
383
  ? (request) => this.controlSession(target, request)
367
384
  : undefined;
368
385
  const task = !message.text.trim() && message.attachments?.length ? "请处理收到的附件" : message.text;
369
- const response = await this.sessions.run(message, target, (sessionId) => this.handle(target, { task, sessionId, ...(message.attachments ? { attachments: message.attachments } : {}) }), control);
386
+ const forwarded = new Set();
387
+ const onMessage = async (text) => {
388
+ if (!text.trim() || forwarded.has(text))
389
+ return;
390
+ await message.reply(text);
391
+ forwarded.add(text);
392
+ this.recordMessage(target, "reply", text, message.peerId);
393
+ };
394
+ 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);
370
395
  const attachments = response.attachments?.length ? response.attachments : (0, attachments_1.asAttachments)(response.images);
371
396
  (0, attachments_1.validateAttachments)(attachments);
372
397
  if (attachments.length) {
@@ -375,10 +400,10 @@ class PluginManager {
375
400
  throw new agent_1.AgentError("当前通道不支持附件回传");
376
401
  }
377
402
  await message.replyAttachments(attachments, response.text);
403
+ this.recordMessage(target, "reply", response.text, message.peerId);
378
404
  }
379
405
  else
380
- await message.reply(response.text);
381
- this.recordMessage(target, "reply", response.text, message.peerId);
406
+ await onMessage(response.text);
382
407
  }
383
408
  catch (error) {
384
409
  this.recordMessage(target, "error", error instanceof Error ? error.message : String(error), message.peerId);
@@ -549,9 +574,11 @@ class PluginManager {
549
574
  const loaded = this.plugins.get(id);
550
575
  if (!loaded?.instance || loaded.record.status !== "ready")
551
576
  throw new agent_1.AgentError(`插件 ${id} 未就绪,无法控制会话`);
552
- if (!loaded.instance.sessionOperations?.includes(request.operation) || !loaded.instance.controlSession)
577
+ if (!loaded.instance.sessionOperations?.includes(request.operation))
553
578
  throw new agent_1.AgentError(`插件 ${id} 不支持新建会话`);
554
- const response = await loaded.instance.controlSession(request);
579
+ const response = loaded.instance.controlSession
580
+ ? await loaded.instance.controlSession(request)
581
+ : { text: `${loaded.record.displayName} 已创建新会话`, sessionId: request.sessionId };
555
582
  if (typeof response.sessionId !== "string" || !response.sessionId.trim())
556
583
  throw new agent_1.AgentError("插件未返回有效的新会话标识,原会话绑定保持不变");
557
584
  return response;
@@ -626,10 +653,13 @@ class PluginManager {
626
653
  const packagedFile = (0, node_path_1.join)(record.path, record.manifest.entry);
627
654
  const developmentFile = (0, node_path_1.resolve)(__dirname, "plugins", record.id, (0, node_path_1.basename)(record.manifest.entry));
628
655
  const file = (0, node_fs_1.existsSync)(packagedFile) ? packagedFile : developmentFile;
629
- // CommonJS is the platform's compiled format; evict the module cache for reload.
656
+ // 同时刷新插件内部依赖,避免升级后仍运行旧版 CDP 客户端。
630
657
  const req = (0, node_module_1.createRequire)(file);
631
658
  const resolved = req.resolve(file);
632
- delete req.cache[resolved];
659
+ const moduleRoot = (0, node_fs_1.existsSync)(packagedFile) ? (0, node_path_1.resolve)(record.path) : (0, node_path_1.dirname)(file);
660
+ for (const cached of Object.keys(req.cache))
661
+ if (cached === resolved || cached.startsWith(`${moduleRoot}${node_path_1.sep}`))
662
+ delete req.cache[cached];
633
663
  const mod = req(resolved);
634
664
  let value = mod.default ?? mod.plugin ?? mod;
635
665
  // Entries may export a ready instance, a factory, or a class. Supporting all
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AunDownloadUrlError = void 0;
3
4
  exports.parseAunAttachment = parseAunAttachment;
4
5
  exports.downloadAunAttachments = downloadAunAttachments;
5
6
  exports.downloadAunAttachment = downloadAunAttachment;
6
7
  exports.uploadAunAttachments = uploadAunAttachments;
7
8
  const node_crypto_1 = require("node:crypto");
9
+ const node_util_1 = require("node:util");
8
10
  const attachments_1 = require("../../attachments");
9
11
  const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
10
12
  const mimePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
@@ -12,6 +14,21 @@ const imageTypes = new Set(["image/png", "image/jpeg", "image/webp"]);
12
14
  function digest(data) { return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex"); }
13
15
  function validKey(key) { return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(key) && !key.split("/").some((part) => part === "." || part === ".."); }
14
16
  function validHash(value) { return /^[a-f0-9]{64}$/i.test(value); }
17
+ function normalizedMime(value) {
18
+ if (value === undefined)
19
+ return;
20
+ if (typeof value !== "string" || /[\u0000-\u001f\u007f]/.test(value))
21
+ throw new Error("AUN 附件 MIME 类型无效");
22
+ if (!value.trim())
23
+ return;
24
+ try {
25
+ const type = new node_util_1.MIMEType(value).essence;
26
+ if (mimePattern.test(type))
27
+ return type;
28
+ }
29
+ catch { /* 统一报告无效类型。 */ }
30
+ throw new Error("AUN 附件 MIME 类型无效");
31
+ }
15
32
  function validateReference(ref) {
16
33
  if (!aidPattern.test(ref.ownerAid) || !/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/.test(ref.bucket) || !validKey(ref.objectKey))
17
34
  throw new Error("AUN 附件引用路径无效");
@@ -43,11 +60,9 @@ function parseAunAttachment(value) {
43
60
  const bucket = item.bucket === undefined ? "default" : typeof item.bucket === "string" ? item.bucket : "";
44
61
  const filename = (0, attachments_1.attachmentFilename)(typeof item.filename === "string" ? item.filename : objectKey);
45
62
  const ref = { ownerAid, bucket, objectKey, filename };
46
- if (item.content_type !== undefined) {
47
- if (typeof item.content_type !== "string")
48
- throw new Error("AUN 附件 MIME 类型无效");
49
- ref.contentType = item.content_type.toLowerCase();
50
- }
63
+ const contentType = normalizedMime(item.content_type);
64
+ if (contentType)
65
+ ref.contentType = contentType;
51
66
  if (item.size_bytes !== undefined) {
52
67
  if (typeof item.size_bytes !== "number")
53
68
  throw new Error("AUN 附件大小无效");
@@ -81,10 +96,8 @@ function checkedMetadata(meta, ref) {
81
96
  if ([meta.owner, meta.owner_aid].some((owner) => owner && owner !== ref.ownerAid) || meta.bucket && meta.bucket !== ref.bucket
82
97
  || [meta.path, meta.object_key].some((path) => path && path !== `/${ref.objectKey}` && path !== ref.objectKey))
83
98
  throw new Error("AUN 附件元数据引用不一致");
84
- const rawType = metadataValue(meta, "contentType", "content_type");
85
- if (rawType && (typeof rawType !== "string" || !mimePattern.test(rawType)))
86
- throw new Error("AUN 附件元数据 MIME 类型无效");
87
- const contentType = typeof rawType === "string" && rawType ? rawType.toLowerCase() : ref.contentType ?? "application/octet-stream";
99
+ const rawType = normalizedMime(metadataValue(meta, "contentType", "content_type"));
100
+ const contentType = rawType ?? ref.contentType ?? "application/octet-stream";
88
101
  if (ref.contentType && rawType && ref.contentType.toLowerCase() !== contentType)
89
102
  throw new Error("AUN 附件 MIME 类型校验失败");
90
103
  const sha256 = typeof meta.sha256 === "string" && meta.sha256 ? meta.sha256.toLowerCase() : undefined;
@@ -162,9 +175,20 @@ async function downloadAunAttachments(client, refs) {
162
175
  (0, attachments_1.validateAttachments)(result);
163
176
  return result;
164
177
  }
178
+ class AunDownloadUrlError extends Error {
179
+ diagnostic;
180
+ constructor(url, reason) {
181
+ super("AUN 下载地址无效");
182
+ this.name = "AunDownloadUrlError";
183
+ this.diagnostic = { protocol: url.protocol, hostname: url.hostname, port: url.port, reason };
184
+ }
185
+ }
186
+ exports.AunDownloadUrlError = AunDownloadUrlError;
165
187
  function checkedDownloadUrl(url) {
166
- if (url.protocol !== "https:" || url.username || url.password || url.port || url.hash || !url.hostname.includes(".") || /^(?:localhost|127\.|0\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(url.hostname) || url.hostname.includes(":"))
167
- throw new Error("AUN 下载地址无效");
188
+ const storagePort = url.port === "9470" && /^storage\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i.test(url.hostname);
189
+ const reason = url.protocol !== "https:" ? "协议" : url.username || url.password ? "凭据" : url.port && !storagePort ? "端口" : url.hash ? "片段" : !url.hostname.includes(".") ? "主机名" : /^(?:localhost|127\.|0\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(url.hostname) ? "本机地址" : url.hostname.includes(":") ? "主机名" : undefined;
190
+ if (reason)
191
+ throw new AunDownloadUrlError(url, reason);
168
192
  return url;
169
193
  }
170
194
  async function readDownloadTicket(value, expectedSize) {
@@ -224,9 +248,16 @@ async function uploadAunAttachments(client, ownerAid, peerAid, items) {
224
248
  const filename = (0, attachments_1.attachmentFilename)(item.filename);
225
249
  const objectKey = `shared/${(0, node_crypto_1.randomUUID)()}/${filename.replace(/[^A-Za-z0-9._-]/g, "_")}`;
226
250
  const sha256 = digest(item.data);
227
- const ref = { ownerAid, bucket: "default", objectKey, filename, contentType: item.contentType.toLowerCase(), sizeBytes: item.data.byteLength, sha256 };
228
- const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType: item.contentType, public: false, overwrite: false });
251
+ const contentType = item.contentType.toLowerCase();
252
+ // 视频播放器不携带 AID 凭据,沿用 EvolCore 的标准可读对象上传方式。
253
+ const video = contentType.startsWith("video/");
254
+ const ref = { ownerAid, bucket: "default", objectKey, filename, contentType, sizeBytes: item.data.byteLength, sha256 };
255
+ const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType, public: video, overwrite: video });
229
256
  checkedMetadata({ ...uploaded, size: uploaded.size ?? uploaded.size_bytes ?? item.data.byteLength }, ref);
257
+ if (video) {
258
+ result.push({ owner_aid: ownerAid, object_key: objectKey, filename, content_type: contentType, size_bytes: item.data.byteLength, sha256 });
259
+ continue;
260
+ }
230
261
  const share = await client.storage.lowlevel.createShareLink({ owner: ownerAid, bucket: "default", objectKey, allowedAids: [peerAid], expireInSeconds: 86400 });
231
262
  if (share.owner_aid && share.owner_aid !== ownerAid || share.bucket && share.bucket !== "default" || share.object_key && share.object_key !== objectKey)
232
263
  throw new Error("AUN 附件分享对象不一致");
@@ -11,7 +11,7 @@ const node_url_1 = require("node:url");
11
11
  const messages_1 = require("./messages");
12
12
  const attachments_1 = require("./attachments");
13
13
  const manifest = {
14
- id: "aun", version: "1.0.23", type: "channel", displayName: "AUN 通道",
14
+ id: "aun", version: "1.0.30", type: "channel", displayName: "AUN 通道",
15
15
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
16
16
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
17
17
  configSchema: "config.schema.json",
@@ -783,7 +783,7 @@ class AunPlugin {
783
783
  }
784
784
  }
785
785
  catch (error) {
786
- this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error) });
786
+ this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error), ...(error instanceof attachments_1.AunDownloadUrlError ? { downloadUrl: error.diagnostic } : {}) });
787
787
  try {
788
788
  await this.sendText(aid, message.peerId, `附件接收失败:${safeError(error)}`, message.threadId);
789
789
  }
@@ -817,15 +817,14 @@ class AunPlugin {
817
817
  }
818
818
  const groups = new Map();
819
819
  for (const [index, item] of items.entries()) {
820
- const kind = item.kind;
821
- const ref = refs[index];
820
+ const kind = item.kind === "file" && item.contentType.toLowerCase().startsWith("video/") ? "video" : item.kind;
822
821
  const group = groups.get(kind) ?? [];
823
- group.push(ref);
822
+ group.push(refs[index]);
824
823
  groups.set(kind, group);
825
824
  }
826
825
  groupsCount = groups.size;
827
826
  for (const [kind, group] of groups) {
828
- const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : "文件已生成。"), attachments: group };
827
+ const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : kind === "video" ? "视频已生成。" : "文件已生成。"), attachments: group };
829
828
  if (message.threadId)
830
829
  payload.thread_id = message.threadId;
831
830
  if (message.messageId)