agent-bridge-aun 1.0.35 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  该插件依赖 `@agentunion/fastaun`,要求 Node.js 22.16 或更高版本。
12
12
 
13
- 附件回复按图片、视频和普通文件分组发送。MIME 类型为 `video/*` 的附件使用 EvolCore 标准 `type: "video"` 消息,使支持视频预览的 Peer 客户端显示播放器;保留原话题和请求引用。视频沿用 EvolCore 标准上传流程:通过 SDK 上传可公开读取的对象,发送不带分享 URL 的标准对象引用,便于接收端取得下载票据及播放器分段读取;持有对象地址者可读取视频。图片和普通文件继续使用仅原 Peer 可访问的限时分享。该流程由插件独立实现,不调用 `ec` 命令,也不改变各 Agent 插件的产物回传策略。
13
+ 附件回复按图片、视频和普通文件分组发送,并保留原话题和请求引用。图片使用不带普通说明正文的 `type: "image"` 消息,说明和下载链接另发加密文本,兼容 AUN 网页的图片卡片显示规则;MIME 类型为 `video/*` 的附件使用 `type: "video"`,普通文件使用 `type: "file"`,二者正文均附下载链接。三类附件参考 EvolCore 的标准附件格式,由插件直接通过 AUN SDK 上传可公开读取的对象,发送不带受限分享 URL 的标准对象引用,便于接收端取得下载票据及播放器分段读取。持有对象地址者可读取附件;旧版本已发送的私有分享不会自动改为公开。发送失败或状态未确认时仍向原 Peer 提供下载链接,不自动重发附件。图片已发送而说明失败时明确报告说明发送阶段,不误报 FS 上传失败。附件链路不调用 `ec` 命令,也不依赖 EvolCore 运行。
14
14
 
15
15
  ABWeb 的 AUN 设置只展示已连接 AID,并按来源分组。成功注册并记录在插件 `created_aids` 中的 AID 属于本工程;共享密钥库中其余 AID 属于 EvolCore。旧版 `created-aids.json` 记录继续兼容。来源只用于展示,连接和关联由 AUN 插件直接管理。列表先显示已关联 Agent 的身份,各组按最近成功关联的时间倒序排列,时间相同或旧记录没有时间时按 AID 名称排序。关联时间保存在插件配置的 `binding_times` 中,刷新或重启后保留;旧记录不补造时间,再次关联成功后开始记录。
16
16
 
@@ -1,7 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AgentOperationError = exports.AgentError = void 0;
4
+ exports.abortable = abortable;
4
5
  exports.errorDetails = errorDetails;
6
+ async function abortable(promise, signal) {
7
+ if (!signal)
8
+ return promise;
9
+ if (signal.aborted) {
10
+ // promise 已在调用方启动,先观察其拒绝,避免预取消时产生未处理拒绝。
11
+ void promise.catch(() => undefined);
12
+ signal.throwIfAborted();
13
+ }
14
+ return new Promise((resolve, reject) => {
15
+ let settled = false;
16
+ const cleanup = () => signal.removeEventListener("abort", abort);
17
+ const abort = () => {
18
+ if (settled)
19
+ return;
20
+ settled = true;
21
+ cleanup();
22
+ reject(signal.reason);
23
+ };
24
+ signal.addEventListener("abort", abort, { once: true });
25
+ promise.then((value) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ cleanup();
30
+ resolve(value);
31
+ }, (error) => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ cleanup();
36
+ reject(error);
37
+ });
38
+ });
39
+ }
5
40
  class AgentError extends Error {
6
41
  constructor(message, options) {
7
42
  super(message, options);
@@ -250,30 +250,12 @@ async function uploadAunAttachments(client, ownerAid, peerAid, items) {
250
250
  const objectKey = `shared/${(0, node_crypto_1.randomUUID)()}/${filename.replace(/[^A-Za-z0-9._-]/g, "_")}`;
251
251
  const sha256 = digest(item.data);
252
252
  const contentType = item.contentType.toLowerCase();
253
- // 视频播放器不携带 AID 凭据,沿用 EvolCore 的标准可读对象上传方式。
254
- const video = contentType.startsWith("video/");
253
+ // 会话媒体控件不携带 AID 凭据,沿用 ec msg 的标准可读对象上传方式。
255
254
  const ref = { ownerAid, bucket: "default", objectKey, filename, contentType, sizeBytes: item.data.byteLength, sha256 };
256
- const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType, public: video, overwrite: video })
255
+ const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType, public: true, overwrite: true })
257
256
  .catch(error => { throw new agent_1.AgentOperationError("AUN_FS_UPLOAD_FAILED", `上传 AUN FS 文件“${filename}”`, error); });
258
257
  checkedMetadata({ ...uploaded, size: uploaded.size ?? uploaded.size_bytes ?? item.data.byteLength }, ref);
259
- if (video) {
260
- result.push({ owner_aid: ownerAid, object_key: objectKey, filename, content_type: contentType, size_bytes: item.data.byteLength, sha256 });
261
- continue;
262
- }
263
- const share = await client.storage.lowlevel.createShareLink({ owner: ownerAid, bucket: "default", objectKey, allowedAids: [peerAid], expireInSeconds: 86400 })
264
- .catch(error => { throw new agent_1.AgentOperationError("AUN_FS_SHARE_FAILED", `创建 AUN FS 文件“${filename}”的接收方授权`, error); });
265
- if (share.owner_aid && share.owner_aid !== ownerAid || share.bucket && share.bucket !== "default" || share.object_key && share.object_key !== objectKey)
266
- throw new Error("AUN 附件分享对象不一致");
267
- if (share.allowed_aids !== undefined && (!Array.isArray(share.allowed_aids) || share.allowed_aids.length !== 1 || share.allowed_aids[0] !== peerAid))
268
- throw new Error("AUN 附件分享授权不一致");
269
- const shareId = typeof share.share_id === "string" && /^[A-Za-z0-9]{10}$/.test(share.share_id) ? share.share_id : undefined;
270
- if (share.share_id !== undefined && !shareId)
271
- throw new Error("AUN 附件分享标识无效");
272
- const url = typeof share.path_url === "string" && share.path_url ? share.path_url : shareId ? `https://${ownerAid}/${objectKey}?t=${shareId}` : "";
273
- const token = url ? pathToken(url, ownerAid, objectKey) : undefined;
274
- if (!token || shareId && shareId !== token)
275
- throw new Error("AUN 附件分享链接无效");
276
- result.push({ owner_aid: ownerAid, bucket: "default", object_key: objectKey, url, filename, content_type: item.contentType, size_bytes: item.data.byteLength, sha256 });
258
+ result.push({ owner_aid: ownerAid, object_key: objectKey, filename, content_type: contentType, size_bytes: item.data.byteLength, sha256 });
277
259
  }
278
260
  return result;
279
261
  }
@@ -13,13 +13,30 @@ const evolcore_1 = require("./evolcore");
13
13
  const messages_1 = require("./messages");
14
14
  const attachments_1 = require("./attachments");
15
15
  const manifest = {
16
- id: "aun", version: "1.0.35", type: "channel", displayName: "AUN 通道",
16
+ id: "aun", version: "1.0.39", type: "channel", displayName: "AUN 通道",
17
17
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
18
18
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
19
19
  configSchema: "config.schema.json",
20
20
  managementPage: "/abweb/channels/aun",
21
21
  };
22
22
  const newSessionCommand = /^\/new(?:[ \t]+[^\r\n]+)?$/;
23
+ const successfulSendStatuses = new Set(["accepted", "sent", "delivered", "duplicate"]);
24
+ function attachmentDownloadLinks(refs) {
25
+ return refs.flatMap((ref) => typeof ref.url === "string" && ref.url ? [ref.url]
26
+ : typeof ref.owner_aid === "string" && typeof ref.object_key === "string" ? [`https://${ref.owner_aid}/${ref.object_key}`] : []);
27
+ }
28
+ function withAttachmentDownloadLinks(text, kind, refs) {
29
+ const links = attachmentDownloadLinks(refs);
30
+ if (!links.length)
31
+ return text;
32
+ return `${text || "附件已生成。"}\n${kind === "file" ? "文件" : "附件"}下载链接:\n${links.join("\n")}`;
33
+ }
34
+ function withFailureDownloadLinks(text, refs) {
35
+ const links = attachmentDownloadLinks(refs);
36
+ if (!links.length || text?.includes("下载链接"))
37
+ return text;
38
+ return `${text || "附件已生成。"}\n附件下载链接(消息未确认时):\n${links.join("\n")}`;
39
+ }
23
40
  async function loadSdk() {
24
41
  const resolver = (0, node_module_1.createRequire)(__filename);
25
42
  const dynamicImport = new Function("url", "return import(url)");
@@ -81,6 +98,60 @@ class AunPlugin {
81
98
  this.sdk = options.sdk;
82
99
  this.ingress = options.ingress;
83
100
  }
101
+ aunManagement = {
102
+ list: async () => {
103
+ const groups = await this.identity.list();
104
+ const aids = [...new Set([...groups.evolcore, ...groups.muAgentBridge, ...(groups.localMuAgentBridge ?? []), ...this.listStoreAids()])]
105
+ .filter((aid) => aidPattern.test(aid)).sort();
106
+ const bindings = Object.fromEntries(Object.entries(this.bindings)
107
+ .filter(([aid, agentId]) => aidPattern.test(aid) && /^[a-z][a-z0-9-]{1,63}$/.test(agentId) && agentId !== "aun" && agentId !== "abweb")
108
+ .sort(([left], [right]) => left.localeCompare(right)));
109
+ return { aids, bindings };
110
+ },
111
+ create: async ({ aid, ownerAid }) => {
112
+ if (typeof aid !== "string" || !aidPattern.test(aid.trim()))
113
+ throw new AunCommandError("AID 格式无效");
114
+ const validAid = aid.trim();
115
+ const owner = typeof ownerAid === "string" && ownerAid.trim()
116
+ ? ownerAid.trim()
117
+ : typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
118
+ if (!owner)
119
+ throw new AunCommandError("未提供 Owner AID,且未配置默认 Owner");
120
+ if (!aidPattern.test(owner))
121
+ throw new AunCommandError("Owner AID 格式无效");
122
+ const result = await this.createIdentity(validAid, owner);
123
+ const connection = result.connection === "ready" ? "ready" : "degraded";
124
+ if (connection !== "ready")
125
+ throw new AunCommandError("AID 已创建但连接失败,请先查询 AID 列表确认状态,不要重试创建", 502, { created: true, aid: validAid, connection });
126
+ return { aid: validAid, status: result.status, connection };
127
+ },
128
+ bind: async ({ aid, agentId, replace }) => {
129
+ if (typeof aid !== "string" || !aidPattern.test(aid.trim()))
130
+ throw new AunCommandError("AID 格式无效");
131
+ if (typeof agentId !== "string" || !/^[a-z][a-z0-9-]{1,63}$/.test(agentId) || agentId === "aun" || agentId === "abweb")
132
+ throw new AunCommandError("Agent 插件 ID 无效");
133
+ if (typeof replace !== "boolean")
134
+ throw new AunCommandError("必须明确指定是否替换现有绑定");
135
+ const validAid = aid.trim();
136
+ const previousAgent = this.bindings[validAid];
137
+ const existing = Object.entries(this.bindings)
138
+ .filter(([, target]) => target === agentId)
139
+ .map(([boundAid]) => boundAid)
140
+ .filter((previousAid) => previousAid !== validAid)
141
+ .sort();
142
+ if (previousAgent && previousAgent !== agentId)
143
+ throw new AunCommandError("该 AID 已绑定到其他 Agent,不能通过替换转移");
144
+ if (existing.length && !replace)
145
+ throw new AunCommandError("目标 Agent 已有其他 AID 绑定;如需替换请明确设置 replace");
146
+ try {
147
+ await this.patchAgentAid(agentId, validAid);
148
+ }
149
+ catch {
150
+ throw new AunCommandError("AUN 身份关联失败", 502);
151
+ }
152
+ return { aid: validAid, agent: agentId, replacedAids: existing };
153
+ },
154
+ };
84
155
  identity = {
85
156
  list: async () => {
86
157
  const own = new Set([...this.createdAids, ...this.legacyCreatedAids]);
@@ -672,16 +743,7 @@ class AunPlugin {
672
743
  this.validatePluginId(agent);
673
744
  await this.requireAgent(agent);
674
745
  }
675
- let created;
676
- try {
677
- created = await this.identity.create(aid, owner);
678
- }
679
- catch (error) {
680
- const exists = this.identities.has(aid) || this.listStoreAids().includes(aid);
681
- if (exists)
682
- throw new AunCommandError("AID 可能已创建,但后续连接或保存失败;请先执行 mab aun list 确认,不要重试创建", 502, { created: true, aid });
683
- throw new AunCommandError(`AID 创建失败:${safeError(error).replaceAll(owner, "[已隐藏]")}`, 502);
684
- }
746
+ const created = await this.createIdentity(aid, owner);
685
747
  const data = { created: true, aid, connection: created.connection ?? "ready" };
686
748
  if (created.connection !== "ready")
687
749
  throw new AunCommandError("AID 已创建,但连接失败;请先执行 mab aun list 确认,不要重试创建", 502, { ...data, connectionError: created.connectionError });
@@ -699,6 +761,17 @@ class AunPlugin {
699
761
  data: { ...data, ...(agent ? { agent } : {}) },
700
762
  };
701
763
  }
764
+ async createIdentity(aid, owner) {
765
+ try {
766
+ return await this.identity.create(aid, owner);
767
+ }
768
+ catch (error) {
769
+ if (this.identities.has(aid) || this.listStoreAids().includes(aid)) {
770
+ throw new AunCommandError("AID 可能已创建,但后续连接或保存失败;请先执行 mab aun list 确认,不要重试创建", 502, { created: true, aid });
771
+ }
772
+ throw new AunCommandError(`AID 创建失败:${safeError(error).replaceAll(owner, "[已隐藏]")}`, 502);
773
+ }
774
+ }
702
775
  async agents(readyOnly = true) {
703
776
  const payload = await this.controlRequest("/api/plugins");
704
777
  if (!Array.isArray(payload.plugins))
@@ -904,11 +977,13 @@ class AunPlugin {
904
977
  let sent = 0;
905
978
  let groupsCount = 0;
906
979
  let sending = false;
980
+ let fallbackText;
981
+ let fallbackLinks = [];
907
982
  try {
908
983
  if (!client?.storage)
909
984
  throw new Error("AUN SDK 不支持附件存储接口");
910
985
  const refs = uploadedRefs && uploadedItems === items ? uploadedRefs : await (0, attachments_1.uploadAunAttachments)({ storage: client.storage }, aid, message.peerId, items);
911
- this.context?.logger.info("AUN FS 文件上传与授权完成", { ...trace, stage: "上传并授权附件", files: items.map(item => ({ filename: item.filename, sizeBytes: item.data.byteLength })) });
986
+ this.context?.logger.info("AUN FS 标准附件上传完成", { ...trace, stage: "上传附件", files: items.map(item => ({ filename: item.filename, sizeBytes: item.data.byteLength })) });
912
987
  if (!uploadedRefs || uploadedItems !== items) {
913
988
  uploadedItems = items;
914
989
  uploadedRefs = refs;
@@ -922,25 +997,44 @@ class AunPlugin {
922
997
  }
923
998
  groupsCount = groups.size;
924
999
  for (const [kind, group] of groups) {
925
- const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : kind === "video" ? "视频已生成。" : "文件已生成。"), attachments: group };
1000
+ const payloadText = withAttachmentDownloadLinks(text || (kind === "image" ? "图片已生成。" : kind === "video" ? "视频已生成。" : "文件已生成。"), kind, group);
1001
+ // AUN 网页会隐藏带普通正文的单图卡片,图片说明需另发文本。
1002
+ const payload = { type: kind, ...(kind === "image" ? {} : { text: payloadText }), attachments: group };
926
1003
  if (message.threadId)
927
1004
  payload.thread_id = message.threadId;
928
1005
  if (message.messageId)
929
1006
  payload.ref_message_id = message.messageId;
1007
+ fallbackText = payloadText;
1008
+ fallbackLinks = attachmentDownloadLinks(group);
930
1009
  sending = true;
931
1010
  // message.send 失败时可能已送达,不自动重试或降级明文,避免重复发送和泄露引用。
932
- await client.call("message.send", { to: message.peerId, payload, encrypt: true });
1011
+ const sendResult = await this.callAunWithRecovery(aid, { to: message.peerId, payload, encrypt: true });
1012
+ if (typeof sendResult?.status !== "string" || !successfulSendStatuses.has(sendResult.status))
1013
+ throw new Error(`AUN 附件发送状态未确认:${sendResult?.status === undefined ? "未返回状态" : String(sendResult.status)}`);
933
1014
  this.context?.logger.info("AUN 附件消息已发送", { ...trace, stage: "发送附件消息", kind, count: group.length });
934
1015
  sending = false;
935
1016
  sent++;
1017
+ if (kind === "image" && payloadText) {
1018
+ try {
1019
+ const { attachments: _attachments, ...caption } = payload;
1020
+ const result = await this.callAunWithRecovery(aid, { to: message.peerId, payload: { ...caption, type: "text", text: payloadText }, encrypt: true });
1021
+ if (typeof result?.status !== "string" || !successfulSendStatuses.has(result.status))
1022
+ throw new Error(`AUN 图片说明发送状态未确认:${result?.status === undefined ? "未返回状态" : String(result.status)}`);
1023
+ this.context?.logger.info("AUN 图片说明已发送", { ...trace, stage: "发送图片说明" });
1024
+ }
1025
+ catch (error) {
1026
+ throw new agent_1.AgentOperationError("AUN_ATTACHMENT_CAPTION_SEND_FAILED", "图片已发送,发送 AUN 图片说明(结果未确认,不自动重发)", error);
1027
+ }
1028
+ }
936
1029
  }
937
1030
  }
938
1031
  catch (error) {
939
1032
  deliveryFailed = true;
940
1033
  const failure = sending ? new agent_1.AgentOperationError("AUN_ATTACHMENT_SEND_FAILED", "发送 AUN 附件消息(FS 上传已完成)", error) : error;
941
1034
  const progress = sending ? `(已确认发送 ${sent}/${groupsCount} 组,当前组结果未确认,请勿重复发送)` : "";
1035
+ const fallback = fallbackText && fallbackLinks.length ? `\n${withFailureDownloadLinks(fallbackText, fallbackLinks.map((url) => ({ url })))}` : "";
942
1036
  try {
943
- await this.sendText(aid, message.peerId, `附件回传失败${progress}:${safeError((0, agent_1.errorDetails)(failure))}`, message.threadId);
1037
+ await this.sendText(aid, message.peerId, `附件回传失败${progress}:${safeError((0, agent_1.errorDetails)(failure))}${fallback}`, message.threadId, true);
944
1038
  }
945
1039
  catch { /* 尽力反馈 */ }
946
1040
  throw failure;
@@ -965,18 +1059,44 @@ class AunPlugin {
965
1059
  }
966
1060
  }
967
1061
  }
968
- async sendText(aid, peerId, text, threadId) {
1062
+ async sendText(aid, peerId, text, threadId, encryptedOnly = false) {
969
1063
  const payload = { type: "text", text };
970
1064
  if (threadId)
971
1065
  payload.thread_id = threadId;
972
1066
  const params = { to: peerId, payload, encrypt: true };
973
1067
  try {
974
- await this.clients.get(aid).client.call("message.send", params);
1068
+ await this.callAunWithRecovery(aid, params);
1069
+ }
1070
+ catch (error) {
1071
+ if (encryptedOnly || !this.isE2eeError(error))
1072
+ throw error;
1073
+ await this.callAunWithRecovery(aid, { ...params, encrypt: false });
1074
+ }
1075
+ }
1076
+ async callAunWithRecovery(aid, params) {
1077
+ const connection = this.clients.get(aid);
1078
+ if (!connection)
1079
+ throw new agent_1.AgentOperationError("AUN_CONNECTION_MISSING", "查找 AUN 身份连接", new Error(`AID ${aid} 当前没有已连接的客户端`));
1080
+ try {
1081
+ return await connection.client.call("message.send", params);
975
1082
  }
976
1083
  catch (error) {
977
- if (!this.isE2eeError(error))
1084
+ if (!isAunWriteNotStarted(error))
978
1085
  throw error;
979
- await this.clients.get(aid).client.call("message.send", { ...params, encrypt: false });
1086
+ this.context?.logger.warn("AUN 发送前连接已断开,正在恢复 AUN 到 Peer 的连接并重试一次", { aid, peer: params.to, stage: "恢复 AUN 连接", error: safeError((0, agent_1.errorDetails)(error)) });
1087
+ try {
1088
+ connection.reconnecting ??= connection.client.connect({ auto_reconnect: true }).finally(() => { connection.reconnecting = undefined; });
1089
+ await connection.reconnecting;
1090
+ }
1091
+ catch (cause) {
1092
+ throw new agent_1.AgentOperationError("AUN_RECONNECT_FAILED", "恢复 AUN 到 Peer 的连接(消息确认未写出)", cause);
1093
+ }
1094
+ try {
1095
+ return await connection.client.call("message.send", params);
1096
+ }
1097
+ catch (cause) {
1098
+ throw new agent_1.AgentOperationError("AUN_SEND_FAILED", "AUN 重连后发送消息(仅重试一次)", cause);
1099
+ }
980
1100
  }
981
1101
  }
982
1102
  isE2eeError(error) {
@@ -995,6 +1115,14 @@ exports.default = AunPlugin;
995
1115
  function safeError(error) {
996
1116
  return redact(error instanceof Error ? error.message : String(error));
997
1117
  }
1118
+ function isAunWriteNotStarted(error) {
1119
+ for (let current = error; current && typeof current === "object"; current = current.cause) {
1120
+ const value = current;
1121
+ if (value.name === "ConnectionError" && typeof value.message === "string" && /^(?:client is not connected|transport not connected|send cancelled before write:|transport closed$)/i.test(value.message))
1122
+ return true;
1123
+ }
1124
+ return false;
1125
+ }
998
1126
  async function readResponse(response, limit) {
999
1127
  if (!response.body) {
1000
1128
  const raw = await response.text();
@@ -13,6 +13,9 @@ function normalizeAunMessage(payload, selfAid, allowFrom) {
13
13
  if (!peerId || peerId === selfAid || (rules.length > 0 && !rules.includes(peerId)))
14
14
  return undefined;
15
15
  const body = data.payload && typeof data.payload === "object" ? data.payload : {};
16
+ // 对端的系统事件只作通知,转为任务回复会导致两个 Agent 循环互答。
17
+ if (body.type === "error" || body.type === "notice")
18
+ return undefined;
16
19
  const text = typeof data.payload === "string" ? data.payload : typeof body.text === "string" ? body.text : "";
17
20
  let attachments = [];
18
21
  let attachmentError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-aun",
3
- "version": "1.0.35",
3
+ "version": "1.0.39",
4
4
  "description": "mu-agent-bridge 的 AUN 通道插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "aun",
3
- "version": "1.0.35",
3
+ "version": "1.0.39",
4
4
  "type": "channel",
5
5
  "displayName": "AUN 通道",
6
6
  "entry": "dist/src-ts/plugins/aun/index.js",