agent-bridge-aun 1.0.22 → 1.0.23

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.
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_TOTAL_ATTACHMENT_BYTES = exports.MAX_ATTACHMENTS = exports.MAX_ATTACHMENT_BYTES = void 0;
4
+ exports.detectImageContentType = detectImageContentType;
5
+ exports.attachmentFilename = attachmentFilename;
6
+ exports.validateAttachment = validateAttachment;
7
+ exports.validateAttachments = validateAttachments;
8
+ exports.asAttachments = asAttachments;
9
+ exports.MAX_ATTACHMENT_BYTES = 32 * 1024 * 1024;
10
+ exports.MAX_ATTACHMENTS = 8;
11
+ exports.MAX_TOTAL_ATTACHMENT_BYTES = 64 * 1024 * 1024;
12
+ function detectImageContentType(data) {
13
+ if (data.length >= 8 && data.slice(0, 8).every((value, index) => value === [137, 80, 78, 71, 13, 10, 26, 10][index]))
14
+ return "image/png";
15
+ if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
16
+ return "image/jpeg";
17
+ if (data.length >= 12 && String.fromCharCode(...data.slice(0, 4)) === "RIFF" && String.fromCharCode(...data.slice(8, 12)) === "WEBP")
18
+ return "image/webp";
19
+ return undefined;
20
+ }
21
+ function attachmentFilename(filename) {
22
+ if (typeof filename !== "string" || /[\u0000-\u001f\u007f]/.test(filename))
23
+ throw new Error("附件文件名无效");
24
+ const name = filename.split(/[\\/]/).pop() ?? "";
25
+ if (!name.trim() || name === "." || name === "..")
26
+ throw new Error("附件文件名无效");
27
+ return name;
28
+ }
29
+ function validateAttachment(item) {
30
+ if (!item || !(item.data instanceof Uint8Array) || item.data.byteLength === 0)
31
+ throw new Error("附件内容不能为空");
32
+ if (item.data.byteLength > exports.MAX_ATTACHMENT_BYTES)
33
+ throw new Error("单个附件不能超过 32 MiB");
34
+ attachmentFilename(item.filename);
35
+ if (item.kind !== "image" && item.kind !== "file")
36
+ throw new Error("附件类型无效");
37
+ if (typeof item.contentType !== "string" || !/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(item.contentType))
38
+ throw new Error("附件 MIME 类型无效");
39
+ if (item.kind === "image") {
40
+ const detected = detectImageContentType(item.data);
41
+ if (!detected || detected !== item.contentType.toLowerCase())
42
+ throw new Error("图片内容与 MIME 类型不一致");
43
+ }
44
+ }
45
+ function validateAttachments(items) {
46
+ if (!items)
47
+ return;
48
+ if (!Array.isArray(items))
49
+ throw new Error("附件列表无效");
50
+ if (items.length > exports.MAX_ATTACHMENTS)
51
+ throw new Error("单条消息最多包含 8 个附件");
52
+ let total = 0;
53
+ for (const item of items) {
54
+ validateAttachment(item);
55
+ total += item.data.byteLength;
56
+ }
57
+ if (total > exports.MAX_TOTAL_ATTACHMENT_BYTES)
58
+ throw new Error("单条消息附件总大小不能超过 64 MiB");
59
+ }
60
+ function asAttachments(images) {
61
+ return (images ?? []).map((image) => ({ ...image, kind: "image" }));
62
+ }
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseAunAttachment = parseAunAttachment;
4
+ exports.downloadAunAttachments = downloadAunAttachments;
5
+ exports.downloadAunAttachment = downloadAunAttachment;
6
+ exports.uploadAunAttachments = uploadAunAttachments;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const attachments_1 = require("../../attachments");
9
+ const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
10
+ const mimePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
11
+ const imageTypes = new Set(["image/png", "image/jpeg", "image/webp"]);
12
+ function digest(data) { return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex"); }
13
+ function validKey(key) { return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(key) && !key.split("/").some((part) => part === "." || part === ".."); }
14
+ function validHash(value) { return /^[a-f0-9]{64}$/i.test(value); }
15
+ function validateReference(ref) {
16
+ if (!aidPattern.test(ref.ownerAid) || !/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/.test(ref.bucket) || !validKey(ref.objectKey))
17
+ throw new Error("AUN 附件引用路径无效");
18
+ (0, attachments_1.attachmentFilename)(ref.filename);
19
+ if (ref.contentType !== undefined && !mimePattern.test(ref.contentType))
20
+ throw new Error("AUN 附件 MIME 类型无效");
21
+ if (ref.sizeBytes !== undefined && (!Number.isSafeInteger(ref.sizeBytes) || ref.sizeBytes < 1 || ref.sizeBytes > attachments_1.MAX_ATTACHMENT_BYTES))
22
+ throw new Error("AUN 附件大小无效,单个附件不能超过 32 MiB");
23
+ if (ref.sha256 !== undefined && !validHash(ref.sha256))
24
+ throw new Error("AUN 附件哈希无效");
25
+ if (ref.token !== undefined && !/^[A-Za-z0-9._~-]{1,4096}$/.test(ref.token))
26
+ throw new Error("AUN 附件分享令牌无效");
27
+ }
28
+ function pathToken(value, ownerAid, objectKey) {
29
+ const url = new URL(value);
30
+ if (url.protocol !== "https:" || url.username || url.password || url.port || url.hash || url.hostname !== ownerAid || url.pathname !== `/${objectKey}`)
31
+ throw new Error("AUN 附件链接与对象引用不一致");
32
+ const tokens = url.searchParams.getAll("t");
33
+ if (tokens.length > 1 || tokens.length === 1 && !tokens[0])
34
+ throw new Error("AUN 附件分享令牌无效");
35
+ return tokens[0];
36
+ }
37
+ function parseAunAttachment(value) {
38
+ if (!value || typeof value !== "object" || Array.isArray(value))
39
+ throw new Error("AUN 附件引用无效");
40
+ const item = value;
41
+ const ownerAid = typeof item.owner_aid === "string" ? item.owner_aid : "";
42
+ const objectKey = typeof item.object_key === "string" ? item.object_key : "";
43
+ const bucket = item.bucket === undefined ? "default" : typeof item.bucket === "string" ? item.bucket : "";
44
+ const filename = (0, attachments_1.attachmentFilename)(typeof item.filename === "string" ? item.filename : objectKey);
45
+ 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
+ }
51
+ if (item.size_bytes !== undefined) {
52
+ if (typeof item.size_bytes !== "number")
53
+ throw new Error("AUN 附件大小无效");
54
+ ref.sizeBytes = item.size_bytes;
55
+ }
56
+ if (item.sha256 !== undefined) {
57
+ if (typeof item.sha256 !== "string")
58
+ throw new Error("AUN 附件哈希无效");
59
+ ref.sha256 = item.sha256.toLowerCase();
60
+ }
61
+ validateReference(ref);
62
+ if (item.url !== undefined) {
63
+ if (typeof item.url !== "string")
64
+ throw new Error("AUN 附件链接无效");
65
+ const token = pathToken(item.url, ownerAid, objectKey);
66
+ if (token)
67
+ ref.token = token;
68
+ }
69
+ validateReference(ref);
70
+ return ref;
71
+ }
72
+ function metadataValue(meta, field, alias) { return meta[field] ?? (alias ? meta[alias] : undefined); }
73
+ function checkedMetadata(meta, ref) {
74
+ const size = metadataValue(meta, "size", "size_bytes");
75
+ if (typeof size !== "number" || !Number.isSafeInteger(size) || size < 1 || size > attachments_1.MAX_ATTACHMENT_BYTES)
76
+ throw new Error("AUN 附件元数据大小无效");
77
+ if (ref.sizeBytes !== undefined && ref.sizeBytes !== size)
78
+ throw new Error("AUN 附件大小校验失败");
79
+ if (meta.type && meta.type !== "file")
80
+ throw new Error("AUN 附件对象不是文件");
81
+ if ([meta.owner, meta.owner_aid].some((owner) => owner && owner !== ref.ownerAid) || meta.bucket && meta.bucket !== ref.bucket
82
+ || [meta.path, meta.object_key].some((path) => path && path !== `/${ref.objectKey}` && path !== ref.objectKey))
83
+ 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";
88
+ if (ref.contentType && rawType && ref.contentType.toLowerCase() !== contentType)
89
+ throw new Error("AUN 附件 MIME 类型校验失败");
90
+ const sha256 = typeof meta.sha256 === "string" && meta.sha256 ? meta.sha256.toLowerCase() : undefined;
91
+ if (sha256 && !validHash(sha256))
92
+ throw new Error("AUN 附件元数据哈希无效");
93
+ if (sha256 && ref.sha256 && sha256 !== ref.sha256.toLowerCase())
94
+ throw new Error("AUN 附件哈希校验失败");
95
+ return { size, contentType, ...(sha256 ? { sha256 } : {}) };
96
+ }
97
+ async function downloadAunAttachments(client, refs) {
98
+ if (refs.length > attachments_1.MAX_ATTACHMENTS)
99
+ throw new Error("单条消息最多包含 8 个附件");
100
+ const entries = [];
101
+ let total = 0;
102
+ for (const ref of refs) {
103
+ validateReference(ref);
104
+ const options = { owner: ref.ownerAid, bucket: ref.bucket, ...(ref.token ? { token: ref.token } : {}) };
105
+ let inlineContent;
106
+ let downloadUrl;
107
+ let rawMetadata;
108
+ if (ref.token) {
109
+ if (!client.storage.lowlevel?.getByShare)
110
+ throw new Error("AUN SDK 不支持受限分享附件");
111
+ rawMetadata = await client.storage.lowlevel.getByShare({ shareId: ref.token });
112
+ if (!rawMetadata.object_key && !rawMetadata.path)
113
+ throw new Error("AUN 分享附件缺少对象引用");
114
+ if (typeof rawMetadata.content === "string")
115
+ inlineContent = rawMetadata.content;
116
+ else if (typeof rawMetadata.download_url === "string")
117
+ downloadUrl = rawMetadata.download_url;
118
+ else
119
+ throw new Error("AUN 分享附件缺少内容或下载票据");
120
+ }
121
+ else
122
+ rawMetadata = await client.storage.stat(`/${ref.objectKey}`, options);
123
+ const metadata = checkedMetadata(rawMetadata, ref);
124
+ total += metadata.size;
125
+ if (total > attachments_1.MAX_TOTAL_ATTACHMENT_BYTES)
126
+ throw new Error("单条消息附件总大小不能超过 64 MiB");
127
+ entries.push({ ref, options, inlineContent, downloadUrl, ...metadata });
128
+ }
129
+ const result = [];
130
+ for (const { ref, options, inlineContent, downloadUrl, size, contentType, sha256 } of entries) {
131
+ if (inlineContent !== undefined && (inlineContent.length > Math.ceil(attachments_1.MAX_ATTACHMENT_BYTES / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(inlineContent)))
132
+ throw new Error("AUN 分享附件 Base64 无效");
133
+ let data;
134
+ if (inlineContent !== undefined)
135
+ data = new Uint8Array(Buffer.from(inlineContent, "base64"));
136
+ else if (downloadUrl !== undefined)
137
+ data = await readDownloadTicket(downloadUrl, size);
138
+ else {
139
+ try {
140
+ data = await client.storage.readBytes(`/${ref.objectKey}`, { ...options, limit: attachments_1.MAX_ATTACHMENT_BYTES + 1 });
141
+ }
142
+ catch (error) {
143
+ const failure = error;
144
+ const inlineLimit = ["EINLINE", "EINLINE_LIMIT", "ERR_INLINE_LIMIT", "INLINE_LIMIT", "-32033"].includes(String(failure.code)) || failure.code === -32602 && /inline/i.test(failure.message ?? "");
145
+ if (!inlineLimit || !client.storage.lowlevel?.createDownloadTicket)
146
+ throw error;
147
+ const ticket = await client.storage.lowlevel.createDownloadTicket({ owner: ref.ownerAid, bucket: ref.bucket, objectKey: ref.objectKey });
148
+ if (typeof ticket.download_url !== "string")
149
+ throw new Error("AUN 附件缺少下载票据");
150
+ if (ticket.sha256 && ticket.sha256 !== sha256 && ticket.sha256 !== ref.sha256)
151
+ throw new Error("AUN 附件下载票据哈希不一致");
152
+ data = await readDownloadTicket(ticket.download_url, size);
153
+ }
154
+ }
155
+ if (!(data instanceof Uint8Array) || data.byteLength !== size)
156
+ throw new Error("AUN 附件大小校验失败");
157
+ const actualHash = digest(data);
158
+ if (ref.sha256 && actualHash !== ref.sha256.toLowerCase() || sha256 && actualHash !== sha256)
159
+ throw new Error("AUN 附件哈希校验失败");
160
+ result.push({ kind: imageTypes.has(contentType) ? "image" : "file", data, filename: (0, attachments_1.attachmentFilename)(ref.filename), contentType });
161
+ }
162
+ (0, attachments_1.validateAttachments)(result);
163
+ return result;
164
+ }
165
+ 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 下载地址无效");
168
+ return url;
169
+ }
170
+ async function readDownloadTicket(value, expectedSize) {
171
+ let url = checkedDownloadUrl(new URL(value));
172
+ const signal = AbortSignal.timeout(60_000);
173
+ let response;
174
+ // 票据和每跳地址都需校验;共用超时,不携带 AID 凭据或向跳转地址补充查询参数。
175
+ for (let redirects = 0;; redirects++) {
176
+ response = await fetch(url.href, { redirect: "manual", signal });
177
+ if (![301, 302, 303, 307, 308].includes(response.status))
178
+ break;
179
+ const location = response.headers.get("location");
180
+ await response.body?.cancel();
181
+ if (!location)
182
+ throw new Error("AUN 附件下载重定向缺少地址");
183
+ if (redirects >= 3)
184
+ throw new Error("AUN 附件下载重定向次数过多");
185
+ url = checkedDownloadUrl(new URL(location, url));
186
+ }
187
+ if (!response.ok || !response.body) {
188
+ await response.body?.cancel();
189
+ throw new Error(`AUN 附件下载失败:HTTP ${response.status}`);
190
+ }
191
+ const length = response.headers.get("content-length");
192
+ if (length !== null && Number(length) !== expectedSize) {
193
+ await response.body.cancel();
194
+ throw new Error("AUN 附件大小校验失败");
195
+ }
196
+ const reader = response.body.getReader();
197
+ const chunks = [];
198
+ let size = 0;
199
+ try {
200
+ while (true) {
201
+ const item = await reader.read();
202
+ if (item.done)
203
+ break;
204
+ size += item.value.byteLength;
205
+ if (size > expectedSize)
206
+ throw new Error("AUN 附件大小校验失败");
207
+ chunks.push(item.value);
208
+ }
209
+ }
210
+ finally {
211
+ await reader.cancel();
212
+ }
213
+ if (size !== expectedSize)
214
+ throw new Error("AUN 附件大小校验失败");
215
+ return new Uint8Array(Buffer.concat(chunks));
216
+ }
217
+ async function downloadAunAttachment(client, ref) { return (await downloadAunAttachments(client, [ref]))[0]; }
218
+ async function uploadAunAttachments(client, ownerAid, peerAid, items) {
219
+ (0, attachments_1.validateAttachments)(items);
220
+ if (!aidPattern.test(ownerAid) || !aidPattern.test(peerAid))
221
+ throw new Error("AUN 附件分享 AID 无效");
222
+ const result = [];
223
+ for (const item of items) {
224
+ const filename = (0, attachments_1.attachmentFilename)(item.filename);
225
+ const objectKey = `shared/${(0, node_crypto_1.randomUUID)()}/${filename.replace(/[^A-Za-z0-9._-]/g, "_")}`;
226
+ 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 });
229
+ checkedMetadata({ ...uploaded, size: uploaded.size ?? uploaded.size_bytes ?? item.data.byteLength }, ref);
230
+ const share = await client.storage.lowlevel.createShareLink({ owner: ownerAid, bucket: "default", objectKey, allowedAids: [peerAid], expireInSeconds: 86400 });
231
+ if (share.owner_aid && share.owner_aid !== ownerAid || share.bucket && share.bucket !== "default" || share.object_key && share.object_key !== objectKey)
232
+ throw new Error("AUN 附件分享对象不一致");
233
+ if (share.allowed_aids !== undefined && (!Array.isArray(share.allowed_aids) || share.allowed_aids.length !== 1 || share.allowed_aids[0] !== peerAid))
234
+ throw new Error("AUN 附件分享授权不一致");
235
+ const shareId = typeof share.share_id === "string" && /^[A-Za-z0-9]{10}$/.test(share.share_id) ? share.share_id : undefined;
236
+ if (share.share_id !== undefined && !shareId)
237
+ throw new Error("AUN 附件分享标识无效");
238
+ const url = typeof share.path_url === "string" && share.path_url ? share.path_url : shareId ? `https://${ownerAid}/${objectKey}?t=${shareId}` : "";
239
+ const token = url ? pathToken(url, ownerAid, objectKey) : undefined;
240
+ if (!token || shareId && shareId !== token)
241
+ throw new Error("AUN 附件分享链接无效");
242
+ result.push({ owner_aid: ownerAid, bucket: "default", object_key: objectKey, url, filename, content_type: item.contentType, size_bytes: item.data.byteLength, sha256 });
243
+ }
244
+ return result;
245
+ }
@@ -9,8 +9,9 @@ const node_path_1 = require("node:path");
9
9
  const node_os_1 = require("node:os");
10
10
  const node_url_1 = require("node:url");
11
11
  const messages_1 = require("./messages");
12
+ const attachments_1 = require("./attachments");
12
13
  const manifest = {
13
- id: "aun", version: "1.0.22", type: "channel", displayName: "AUN 通道",
14
+ id: "aun", version: "1.0.23", type: "channel", displayName: "AUN 通道",
14
15
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
15
16
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
16
17
  configSchema: "config.schema.json",
@@ -71,6 +72,8 @@ class AunPlugin {
71
72
  status = "degraded";
72
73
  healthMessage = "尚未配置 AID";
73
74
  seen = new messages_1.DuplicateTracker();
75
+ pendingMessages = new Map();
76
+ acceptingMessages = false;
74
77
  constructor(options = {}) {
75
78
  this.options = options;
76
79
  this.sdk = options.sdk;
@@ -251,6 +254,7 @@ class AunPlugin {
251
254
  }
252
255
  if (this.aid)
253
256
  this.identities.add(this.aid);
257
+ this.acceptingMessages = true;
254
258
  for (const aid of this.identities)
255
259
  await this.connectAid(aid);
256
260
  // 密钥库中的其余 AID 也自动连接:不再依赖 ec 或 EvolCore 进程
@@ -291,10 +295,12 @@ class AunPlugin {
291
295
  }
292
296
  async deactivate(_reason) {
293
297
  this.status = "degraded";
294
- for (const { client, subscription } of this.clients.values()) {
298
+ this.acceptingMessages = false;
299
+ for (const { subscription } of this.clients.values())
295
300
  subscription?.unsubscribe();
301
+ await Promise.allSettled(this.pendingMessages.values());
302
+ for (const { client } of this.clients.values())
296
303
  await client.close();
297
- }
298
304
  this.clients.clear();
299
305
  await this.store?.close();
300
306
  this.store = undefined;
@@ -735,28 +741,109 @@ class AunPlugin {
735
741
  }
736
742
  };
737
743
  async onMessage(payload, aid) {
744
+ if (!this.acceptingMessages)
745
+ return;
738
746
  const message = (0, messages_1.normalizeAunMessage)(payload, aid, typeof this.config.allow_from === "string" ? this.config.allow_from : undefined);
739
747
  if (!message || !this.seen.add(message.messageId ? `${aid}:${message.messageId}` : ""))
740
748
  return;
749
+ const sourceKey = JSON.stringify([aid, message.peerId, message.threadId ?? ""]);
750
+ const pending = this.pendingMessages.get(sourceKey);
751
+ if (pending && !message.attachments?.length && !message.attachmentError && newSessionCommand.test(message.text.trim())) {
752
+ try {
753
+ await this.sendText(aid, message.peerId, "新建会话失败:当前会话正在执行任务,请等待完成后再新建会话", message.threadId);
754
+ }
755
+ catch (error) {
756
+ this.context?.logger.warn("AUN 错误回复失败", { error: safeError(error) });
757
+ }
758
+ return;
759
+ }
760
+ const current = (pending ?? Promise.resolve()).catch(() => undefined).then(() => this.processMessage(message, aid));
761
+ this.pendingMessages.set(sourceKey, current);
762
+ try {
763
+ await current;
764
+ }
765
+ finally {
766
+ if (this.pendingMessages.get(sourceKey) === current)
767
+ this.pendingMessages.delete(sourceKey);
768
+ }
769
+ }
770
+ async processMessage(message, aid) {
741
771
  let replyAttempted = false;
772
+ const client = this.clients.get(aid)?.client;
773
+ let attachments = [];
774
+ let uploadedItems;
775
+ let uploadedRefs;
776
+ try {
777
+ if (message.attachmentError)
778
+ throw new Error(message.attachmentError);
779
+ if (message.attachments?.length) {
780
+ if (!client?.storage)
781
+ throw new Error("AUN SDK 不支持附件存储接口");
782
+ attachments = await (0, attachments_1.downloadAunAttachments)({ storage: client.storage }, message.attachments);
783
+ }
784
+ }
785
+ catch (error) {
786
+ this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error) });
787
+ try {
788
+ await this.sendText(aid, message.peerId, `附件接收失败:${safeError(error)}`, message.threadId);
789
+ }
790
+ catch { /* 尽力反馈 */ }
791
+ return;
792
+ }
742
793
  const channelMessage = {
743
794
  channel: "aun", recipientId: aid, peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
795
+ ...(attachments.length ? { attachments } : {}),
744
796
  agentPlugin: this.bindings[aid] || (typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork"),
745
797
  reply: async (text) => {
746
798
  if (!text.trim())
747
799
  return;
748
800
  replyAttempted = true;
749
- const payload = { type: "text", text };
750
- if (message.threadId)
751
- payload.thread_id = message.threadId;
752
- const params = { to: message.peerId, payload, encrypt: true };
801
+ await this.sendText(aid, message.peerId, text, message.threadId);
802
+ },
803
+ replyAttachments: async (items, text) => {
804
+ if (!items.length)
805
+ return;
806
+ replyAttempted = true;
807
+ let sent = 0;
808
+ let groupsCount = 0;
809
+ let sending = false;
753
810
  try {
754
- await this.clients.get(aid).client.call("message.send", params);
811
+ if (!client?.storage)
812
+ throw new Error("AUN SDK 不支持附件存储接口");
813
+ const refs = uploadedRefs && uploadedItems === items ? uploadedRefs : await (0, attachments_1.uploadAunAttachments)({ storage: client.storage }, aid, message.peerId, items);
814
+ if (!uploadedRefs || uploadedItems !== items) {
815
+ uploadedItems = items;
816
+ uploadedRefs = refs;
817
+ }
818
+ const groups = new Map();
819
+ for (const [index, item] of items.entries()) {
820
+ const kind = item.kind;
821
+ const ref = refs[index];
822
+ const group = groups.get(kind) ?? [];
823
+ group.push(ref);
824
+ groups.set(kind, group);
825
+ }
826
+ groupsCount = groups.size;
827
+ for (const [kind, group] of groups) {
828
+ const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : "文件已生成。"), attachments: group };
829
+ if (message.threadId)
830
+ payload.thread_id = message.threadId;
831
+ if (message.messageId)
832
+ payload.ref_message_id = message.messageId;
833
+ sending = true;
834
+ // message.send 失败时可能已送达,不自动重试或降级明文,避免重复发送和泄露引用。
835
+ await client.call("message.send", { to: message.peerId, payload, encrypt: true });
836
+ sending = false;
837
+ sent++;
838
+ }
755
839
  }
756
840
  catch (error) {
757
- if (!this.isE2eeError(error))
758
- throw error;
759
- await this.clients.get(aid).client.call("message.send", { ...params, encrypt: false });
841
+ const progress = sending ? `(已确认发送 ${sent}/${groupsCount} 组,当前组结果未确认,请勿重复发送)` : "";
842
+ try {
843
+ await this.sendText(aid, message.peerId, `附件回传失败${progress}:${safeError(error)}`, message.threadId);
844
+ }
845
+ catch { /* 尽力反馈 */ }
846
+ throw error;
760
847
  }
761
848
  },
762
849
  };
@@ -765,9 +852,10 @@ class AunPlugin {
765
852
  }
766
853
  catch (error) {
767
854
  this.context?.logger.warn("AUN 消息处理失败", { error: safeError(error) });
768
- if (!replyAttempted && newSessionCommand.test(message.text.trim())) {
855
+ if (!replyAttempted) {
856
+ const action = attachments.length > 0 ? "附件处理失败" : newSessionCommand.test(message.text.trim()) ? "新建会话失败" : "消息处理失败";
769
857
  try {
770
- await channelMessage.reply(`新建会话失败:${safeError(error)}`);
858
+ await channelMessage.reply(`${action}:${safeError(error)}`);
771
859
  }
772
860
  catch (replyError) {
773
861
  this.context?.logger.warn("AUN 错误回复失败", { error: safeError(replyError) });
@@ -775,6 +863,20 @@ class AunPlugin {
775
863
  }
776
864
  }
777
865
  }
866
+ async sendText(aid, peerId, text, threadId) {
867
+ const payload = { type: "text", text };
868
+ if (threadId)
869
+ payload.thread_id = threadId;
870
+ const params = { to: peerId, payload, encrypt: true };
871
+ try {
872
+ await this.clients.get(aid).client.call("message.send", params);
873
+ }
874
+ catch (error) {
875
+ if (!this.isE2eeError(error))
876
+ throw error;
877
+ await this.clients.get(aid).client.call("message.send", { ...params, encrypt: false });
878
+ }
879
+ }
778
880
  isE2eeError(error) {
779
881
  const sdk = this.sdk;
780
882
  return [sdk.E2EEError, sdk.E2EEDecryptFailedError, sdk.E2EEDegradedError].some((type) => !!type && error instanceof type)
@@ -820,5 +922,6 @@ async function readResponse(response, limit) {
820
922
  return Buffer.concat(chunks).toString("utf8");
821
923
  }
822
924
  function redact(message) {
823
- return message.replace(/(private[_ -]?key|seed|token|certificate)\s*[:=]\s*[^\s,;]+/gi, "$1=[已隐藏]");
925
+ return message.replace(/https?:\/\/[^\s"'<>]+/gi, (value) => value.includes("?") ? `${value.slice(0, value.indexOf("?"))}?[已隐藏]` : value)
926
+ .replace(/(private[_ -]?key|seed|token|share_id|certificate)["']?\s*[:=]\s*["']?[^\s,;"'}]+/gi, "$1=[已隐藏]");
824
927
  }
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DuplicateTracker = void 0;
4
4
  exports.normalizeAunMessage = normalizeAunMessage;
5
+ const attachments_1 = require("./attachments");
6
+ const attachments_2 = require("../../attachments");
5
7
  function normalizeAunMessage(payload, selfAid, allowFrom) {
6
8
  if (!payload || typeof payload !== "object")
7
9
  return undefined;
@@ -12,13 +14,35 @@ function normalizeAunMessage(payload, selfAid, allowFrom) {
12
14
  return undefined;
13
15
  const body = data.payload && typeof data.payload === "object" ? data.payload : {};
14
16
  const text = typeof data.payload === "string" ? data.payload : typeof body.text === "string" ? body.text : "";
15
- if (!text.trim())
17
+ let attachments = [];
18
+ let attachmentError;
19
+ if (body.attachments !== undefined) {
20
+ try {
21
+ if (!Array.isArray(body.attachments))
22
+ throw new Error("AUN 附件列表无效");
23
+ if (body.attachments.length > attachments_2.MAX_ATTACHMENTS)
24
+ throw new Error("单条消息最多包含 8 个附件");
25
+ attachments = body.attachments.map((item) => {
26
+ const ref = (0, attachments_1.parseAunAttachment)(item);
27
+ if (!ref.token && ref.ownerAid !== peerId)
28
+ throw new Error("AUN 附件所有者必须是消息对端,第三方对象需要显式分享令牌");
29
+ return ref;
30
+ });
31
+ }
32
+ catch (error) {
33
+ attachments = [];
34
+ attachmentError = error instanceof Error ? error.message : "AUN 附件引用无效";
35
+ }
36
+ }
37
+ if (!text.trim() && !attachments.length && !attachmentError)
16
38
  return undefined;
17
39
  return {
18
40
  peerId,
19
41
  ...(typeof data.message_id === "string" ? { messageId: data.message_id } : {}),
20
42
  text,
21
43
  ...(typeof body.thread_id === "string" ? { threadId: body.thread_id } : {}),
44
+ ...(attachments.length ? { attachments } : {}),
45
+ ...(attachmentError ? { attachmentError } : {}),
22
46
  };
23
47
  }
24
48
  class DuplicateTracker {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-aun",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
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.22",
3
+ "version": "1.0.23",
4
4
  "type": "channel",
5
5
  "displayName": "AUN 通道",
6
6
  "entry": "dist/src-ts/plugins/aun/index.js",