agent-bridge-aun 1.0.36 → 1.0.40

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
 
@@ -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,7 +13,7 @@ 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.36", type: "channel", displayName: "AUN 通道",
16
+ id: "aun", version: "1.0.40", 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",
@@ -22,15 +22,14 @@ const manifest = {
22
22
  const newSessionCommand = /^\/new(?:[ \t]+[^\r\n]+)?$/;
23
23
  const successfulSendStatuses = new Set(["accepted", "sent", "delivered", "duplicate"]);
24
24
  function attachmentDownloadLinks(refs) {
25
- return refs.flatMap((ref) => typeof ref.url === "string" && ref.url ? [ref.url] : []);
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}`] : []);
26
27
  }
27
28
  function withAttachmentDownloadLinks(text, kind, refs) {
28
- if (kind !== "image")
29
- return text;
30
29
  const links = attachmentDownloadLinks(refs);
31
30
  if (!links.length)
32
31
  return text;
33
- return `${text || "图片已生成。"}\n图片下载链接(预览不可用时):\n${links.join("\n")}`;
32
+ return `${text || "附件已生成。"}\n${kind === "file" ? "文件" : "附件"}下载链接:\n${links.join("\n")}`;
34
33
  }
35
34
  function withFailureDownloadLinks(text, refs) {
36
35
  const links = attachmentDownloadLinks(refs);
@@ -960,7 +959,8 @@ class AunPlugin {
960
959
  channel: "aun", recipientId: aid, peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
961
960
  ...(attachments.length ? { attachments } : {}),
962
961
  agentPlugin,
963
- reply: async (text) => {
962
+ reply: async (text, signal) => {
963
+ signal?.throwIfAborted();
964
964
  if (!text.trim())
965
965
  return;
966
966
  try {
@@ -972,7 +972,8 @@ class AunPlugin {
972
972
  throw new agent_1.AgentOperationError("AUN_TEXT_SEND_FAILED", "发送 AUN 文本消息(结果未确认,不自动重发)", error);
973
973
  }
974
974
  },
975
- replyAttachments: async (items, text) => {
975
+ replyAttachments: async (items, text, signal) => {
976
+ signal?.throwIfAborted();
976
977
  if (!items.length)
977
978
  return;
978
979
  let sent = 0;
@@ -984,7 +985,8 @@ class AunPlugin {
984
985
  if (!client?.storage)
985
986
  throw new Error("AUN SDK 不支持附件存储接口");
986
987
  const refs = uploadedRefs && uploadedItems === items ? uploadedRefs : await (0, attachments_1.uploadAunAttachments)({ storage: client.storage }, aid, message.peerId, items);
987
- this.context?.logger.info("AUN FS 文件上传与授权完成", { ...trace, stage: "上传并授权附件", files: items.map(item => ({ filename: item.filename, sizeBytes: item.data.byteLength })) });
988
+ signal?.throwIfAborted();
989
+ this.context?.logger.info("AUN FS 标准附件上传完成", { ...trace, stage: "上传附件", files: items.map(item => ({ filename: item.filename, sizeBytes: item.data.byteLength })) });
988
990
  if (!uploadedRefs || uploadedItems !== items) {
989
991
  uploadedItems = items;
990
992
  uploadedRefs = refs;
@@ -998,8 +1000,10 @@ class AunPlugin {
998
1000
  }
999
1001
  groupsCount = groups.size;
1000
1002
  for (const [kind, group] of groups) {
1003
+ signal?.throwIfAborted();
1001
1004
  const payloadText = withAttachmentDownloadLinks(text || (kind === "image" ? "图片已生成。" : kind === "video" ? "视频已生成。" : "文件已生成。"), kind, group);
1002
- const payload = { type: kind, text: payloadText, attachments: group };
1005
+ // AUN 网页会隐藏带普通正文的单图卡片,图片说明需另发文本。
1006
+ const payload = { type: kind, ...(kind === "image" ? {} : { text: payloadText }), attachments: group };
1003
1007
  if (message.threadId)
1004
1008
  payload.thread_id = message.threadId;
1005
1009
  if (message.messageId)
@@ -1008,15 +1012,29 @@ class AunPlugin {
1008
1012
  fallbackLinks = attachmentDownloadLinks(group);
1009
1013
  sending = true;
1010
1014
  // message.send 失败时可能已送达,不自动重试或降级明文,避免重复发送和泄露引用。
1011
- const sendResult = await client.call("message.send", { to: message.peerId, payload, encrypt: true });
1015
+ const sendResult = await this.callAunWithRecovery(aid, { to: message.peerId, payload, encrypt: true });
1012
1016
  if (typeof sendResult?.status !== "string" || !successfulSendStatuses.has(sendResult.status))
1013
1017
  throw new Error(`AUN 附件发送状态未确认:${sendResult?.status === undefined ? "未返回状态" : String(sendResult.status)}`);
1014
1018
  this.context?.logger.info("AUN 附件消息已发送", { ...trace, stage: "发送附件消息", kind, count: group.length });
1015
1019
  sending = false;
1016
1020
  sent++;
1021
+ if (kind === "image" && payloadText) {
1022
+ try {
1023
+ signal?.throwIfAborted();
1024
+ const { attachments: _attachments, ...caption } = payload;
1025
+ const result = await this.callAunWithRecovery(aid, { to: message.peerId, payload: { ...caption, type: "text", text: payloadText }, encrypt: true });
1026
+ if (typeof result?.status !== "string" || !successfulSendStatuses.has(result.status))
1027
+ throw new Error(`AUN 图片说明发送状态未确认:${result?.status === undefined ? "未返回状态" : String(result.status)}`);
1028
+ this.context?.logger.info("AUN 图片说明已发送", { ...trace, stage: "发送图片说明" });
1029
+ }
1030
+ catch (error) {
1031
+ throw new agent_1.AgentOperationError("AUN_ATTACHMENT_CAPTION_SEND_FAILED", "图片已发送,发送 AUN 图片说明(结果未确认,不自动重发)", error);
1032
+ }
1033
+ }
1017
1034
  }
1018
1035
  }
1019
1036
  catch (error) {
1037
+ signal?.throwIfAborted();
1020
1038
  deliveryFailed = true;
1021
1039
  const failure = sending ? new agent_1.AgentOperationError("AUN_ATTACHMENT_SEND_FAILED", "发送 AUN 附件消息(FS 上传已完成)", error) : error;
1022
1040
  const progress = sending ? `(已确认发送 ${sent}/${groupsCount} 组,当前组结果未确认,请勿重复发送)` : "";
@@ -1053,12 +1071,38 @@ class AunPlugin {
1053
1071
  payload.thread_id = threadId;
1054
1072
  const params = { to: peerId, payload, encrypt: true };
1055
1073
  try {
1056
- await this.clients.get(aid).client.call("message.send", params);
1074
+ await this.callAunWithRecovery(aid, params);
1057
1075
  }
1058
1076
  catch (error) {
1059
1077
  if (encryptedOnly || !this.isE2eeError(error))
1060
1078
  throw error;
1061
- await this.clients.get(aid).client.call("message.send", { ...params, encrypt: false });
1079
+ await this.callAunWithRecovery(aid, { ...params, encrypt: false });
1080
+ }
1081
+ }
1082
+ async callAunWithRecovery(aid, params) {
1083
+ const connection = this.clients.get(aid);
1084
+ if (!connection)
1085
+ throw new agent_1.AgentOperationError("AUN_CONNECTION_MISSING", "查找 AUN 身份连接", new Error(`AID ${aid} 当前没有已连接的客户端`));
1086
+ try {
1087
+ return await connection.client.call("message.send", params);
1088
+ }
1089
+ catch (error) {
1090
+ if (!isAunWriteNotStarted(error))
1091
+ throw error;
1092
+ this.context?.logger.warn("AUN 发送前连接已断开,正在恢复 AUN 到 Peer 的连接并重试一次", { aid, peer: params.to, stage: "恢复 AUN 连接", error: safeError((0, agent_1.errorDetails)(error)) });
1093
+ try {
1094
+ connection.reconnecting ??= connection.client.connect({ auto_reconnect: true }).finally(() => { connection.reconnecting = undefined; });
1095
+ await connection.reconnecting;
1096
+ }
1097
+ catch (cause) {
1098
+ throw new agent_1.AgentOperationError("AUN_RECONNECT_FAILED", "恢复 AUN 到 Peer 的连接(消息确认未写出)", cause);
1099
+ }
1100
+ try {
1101
+ return await connection.client.call("message.send", params);
1102
+ }
1103
+ catch (cause) {
1104
+ throw new agent_1.AgentOperationError("AUN_SEND_FAILED", "AUN 重连后发送消息(仅重试一次)", cause);
1105
+ }
1062
1106
  }
1063
1107
  }
1064
1108
  isE2eeError(error) {
@@ -1077,6 +1121,14 @@ exports.default = AunPlugin;
1077
1121
  function safeError(error) {
1078
1122
  return redact(error instanceof Error ? error.message : String(error));
1079
1123
  }
1124
+ function isAunWriteNotStarted(error) {
1125
+ for (let current = error; current && typeof current === "object"; current = current.cause) {
1126
+ const value = current;
1127
+ 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))
1128
+ return true;
1129
+ }
1130
+ return false;
1131
+ }
1080
1132
  async function readResponse(response, limit) {
1081
1133
  if (!response.body) {
1082
1134
  const raw = await response.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-aun",
3
- "version": "1.0.36",
3
+ "version": "1.0.40",
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.36",
3
+ "version": "1.0.40",
4
4
  "type": "channel",
5
5
  "displayName": "AUN 通道",
6
6
  "entry": "dist/src-ts/plugins/aun/index.js",