agent-bridge-aun 1.0.21 → 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.
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  `agent-bridge-aun` 为 mu-agent-bridge 提供 AUN 消息通道,支持使用已有 AID 或通过管理 API 注册 AID,并将收到的文本交给指定 Agent 插件处理。
4
4
 
5
- 安装后在 ABWeb 的插件管理中启动 AUN 通道。配置 `aid` 和可选的 `agent_plugin`;默认 Agent 插件为 `qwenwork`。AID 创建接口为 `POST /api/plugins/aun/aid`,请求内容为 `{"aid":"新AID.agentid.cn","owner_aid":"所有者AID.agentid.cn"}`。
5
+ 安装后在 ABWeb 的插件管理中启动 AUN 通道。配置 `aid` 和可选的 `agent_plugin`;默认 Agent 插件为 `qwenwork`。配置字段 `owner_aid` 是可选字符串,用作默认 Owner。AID 创建接口为 `POST /api/plugins/aun/aid`,请求内容为 `{"aid":"新AID.agentid.cn","owner_aid":"所有者AID.agentid.cn"}`。
6
6
 
7
7
  创建使用 SDK 的兼容注册流程;Owner AID 只保存为本地归属配置,不代表 AUN 协议 Owner 关系已签名或激活。Gateway 信任根使用随插件提供并校验指纹的 AUN 根证书,不从 Gateway 下载未经验证的 CA 链。
8
8
 
9
- 核心支持插件命令后,可运行 `mab aun status`、`mab aun list`、`mab aun agents`、`mab aun create <aid> --owner <owner-aid> [--agent <plugin-id>]`、`mab aun bind <aid> <plugin-id>` 和 `mab aun unbind <plugin-id>`。Agent 从当前已就绪的插件动态发现。创建后连接或关联失败时,响应会标明 AID 已创建;先用 `mab aun list` 检查,不要重试创建。命令不会记录原始参数或 Owner AID。
9
+ 核心支持插件命令后,可运行 `mab aun status`、`mab aun list`、`mab aun agents`、`mab aun default-owner`、`mab aun default-owner set <owner-aid>`、`mab aun create <aid> [--owner <owner-aid>] [--agent <plugin-id>]`、`mab aun bind <aid> <plugin-id>` 和 `mab aun unbind <plugin-id>`。`default-owner` 不带参数显示当前配置,`default-owner set <owner-aid>` 校验格式并持久化新的默认值。`create` 明确传入 `--owner` 时优先使用该值;省略时回退到同一配置中的 `owner_aid`,两者都没有时返回错误,不会创建 AID。创建新身份时,实际使用的 Owner 会保存为下次默认值。ABWeb 从同一插件配置读取 `owner_aid` 并预填创建表单,因此 CLI 和 ABWeb 共用默认 Owner。Agent 从当前已就绪的插件动态发现。创建后连接或关联失败时,响应会标明 AID 已创建;先用 `mab aun list` 检查,不要重试创建。运行日志不记录原始参数或 Owner AID;默认 Owner 查询会在终端显示当前值。
10
10
 
11
11
  该插件依赖 `@agentunion/fastaun`,要求 Node.js 22.16 或更高版本。
12
12
 
13
- ABWeb 的 AUN 设置只展示已连接 AID,并按来源分组。插件通过 SDK 密钥库枚举 AID,再逐个建立连接;只有连接成功的身份才会显示。EvolCore 来源身份只用于连接状态检测,不订阅消息,避免重复收信;本工程创建的身份负责消息收发。连接状态检测不调用 `ec agent list/show`。将 EvolCore 身份关联到 Agent 时仍使用既有 `ec agent set/reload` 路由配置,该配置不参与连接状态检测。列表先显示已关联 Agent 的身份,各组按最近成功关联的时间倒序排列,时间相同或旧记录没有时间时按 AID 名称排序。关联时间保存在插件配置的 `binding_times` 中,刷新或重启后保留;旧记录不补造时间,再次关联成功后开始记录。
13
+ ABWeb 的 AUN 设置只展示已连接 AID,并按来源分组。成功注册并记录在插件 `created_aids` 中的 AID 属于本工程;共享密钥库中其余 AID 属于 EvolCore。旧版 `created-aids.json` 记录继续兼容。来源只用于展示,连接和关联由 AUN 插件直接管理。列表先显示已关联 Agent 的身份,各组按最近成功关联的时间倒序排列,时间相同或旧记录没有时间时按 AID 名称排序。关联时间保存在插件配置的 `binding_times` 中,刷新或重启后保留;旧记录不补造时间,再次关联成功后开始记录。
14
14
 
15
15
  AUN 独立包支持核心后台每五分钟检测更新;可信提供者在自动安装策略下会下载新副本并重载通道。
@@ -2,6 +2,7 @@
2
2
  "type": "object",
3
3
  "properties": {
4
4
  "identities": { "type": "array", "items": { "type": "string" } },
5
+ "created_aids": { "type": "array", "items": { "type": "string" } },
5
6
  "bindings": { "type": "object", "additionalProperties": { "type": "string" } },
6
7
  "binding_times": { "type": "object", "additionalProperties": { "type": "string" } },
7
8
  "aid": { "type": "string", "minLength": 1 },
@@ -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,13 +9,15 @@ 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.21", 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",
17
18
  managementPage: "/abweb/channels/aun",
18
19
  };
20
+ const newSessionCommand = /^\/new(?:[ \t]+[^\r\n]+)?$/;
19
21
  async function loadSdk() {
20
22
  const resolver = (0, node_module_1.createRequire)(__filename);
21
23
  const dynamicImport = new Function("url", "return import(url)");
@@ -36,7 +38,8 @@ const cliCommands = [
36
38
  { name: "status", summary: "查看 AUN 通道状态", usage: "mab aun status" },
37
39
  { name: "list", summary: "查看已连接 AID 和 Agent 关联", usage: "mab aun list" },
38
40
  { name: "agents", summary: "列出当前已就绪的 Agent 插件", usage: "mab aun agents" },
39
- { name: "create", summary: "创建并连接 AID,可选关联 Agent", usage: "mab aun create <aid> --owner <owner-aid> [--agent <plugin-id>]", examples: ["mab aun create new.agentid.cn --owner owner.agentid.cn --agent qwenwork"] },
41
+ { name: "default-owner", summary: "查询或设置默认 Owner AID", usage: "mab aun default-owner [set <owner-aid>]", examples: ["mab aun default-owner", "mab aun default-owner set owner.agentid.cn"] },
42
+ { name: "create", summary: "创建并连接 AID,可选关联 Agent", usage: "mab aun create <aid> [--owner <owner-aid>] [--agent <plugin-id>]", examples: ["mab aun create new.agentid.cn --owner owner.agentid.cn --agent qwenwork"] },
40
43
  { name: "bind", summary: "将 AID 关联到 Agent", usage: "mab aun bind <aid> <plugin-id>", examples: ["mab aun bind new.agentid.cn qwenwork"] },
41
44
  { name: "unbind", summary: "解除 Agent 与 AID 的关联", usage: "mab aun unbind <plugin-id>", examples: ["mab aun unbind qwenwork"] },
42
45
  ];
@@ -58,6 +61,7 @@ class AunPlugin {
58
61
  clients = new Map();
59
62
  identities = new Set();
60
63
  legacyCreatedAids = new Set();
64
+ createdAids = new Set();
61
65
  bindings = {};
62
66
  bindingTimes = {};
63
67
  creating = false;
@@ -68,6 +72,8 @@ class AunPlugin {
68
72
  status = "degraded";
69
73
  healthMessage = "尚未配置 AID";
70
74
  seen = new messages_1.DuplicateTracker();
75
+ pendingMessages = new Map();
76
+ acceptingMessages = false;
71
77
  constructor(options = {}) {
72
78
  this.options = options;
73
79
  this.sdk = options.sdk;
@@ -75,10 +81,11 @@ class AunPlugin {
75
81
  }
76
82
  identity = {
77
83
  list: async () => {
84
+ const own = new Set([...this.createdAids, ...this.legacyCreatedAids]);
78
85
  return {
79
- evolcore: [],
80
- muAgentBridge: [...this.clients.keys()].sort(),
81
- localMuAgentBridge: [...this.identities].filter((aid) => !this.clients.has(aid)).sort(),
86
+ evolcore: [...this.clients.keys()].filter((aid) => !own.has(aid)).sort(),
87
+ muAgentBridge: [...this.clients.keys()].filter((aid) => own.has(aid)).sort(),
88
+ localMuAgentBridge: [...own].filter((aid) => !this.clients.has(aid)).sort(),
82
89
  };
83
90
  },
84
91
  create: async (aid, owner) => {
@@ -95,6 +102,8 @@ class AunPlugin {
95
102
  const result = await this.store.register(aid);
96
103
  if (!result.ok)
97
104
  throw new Error(redact(result.error?.message ?? result.error?.code ?? "AID 注册失败"));
105
+ this.createdAids.add(aid);
106
+ await this.persist();
98
107
  }
99
108
  const loaded = this.store.load(aid);
100
109
  if (!loaded.ok || !loaded.data)
@@ -180,6 +189,7 @@ class AunPlugin {
180
189
  };
181
190
  async persist() {
182
191
  this.config.identities = [...this.identities].sort();
192
+ this.config.created_aids = [...new Set([...this.createdAids, ...this.legacyCreatedAids])].sort();
183
193
  this.config.bindings = this.bindings;
184
194
  this.config.binding_times = this.bindingTimes;
185
195
  await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 });
@@ -188,6 +198,7 @@ class AunPlugin {
188
198
  this.context = context;
189
199
  this.config = { ...context.config };
190
200
  this.legacyCreatedAids.clear();
201
+ this.createdAids.clear();
191
202
  try {
192
203
  const created = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(context.dataDir, "..", "created-aids.json"), "utf8"));
193
204
  if (Array.isArray(created))
@@ -199,6 +210,10 @@ class AunPlugin {
199
210
  if (error.code !== "ENOENT")
200
211
  context.logger.warn("读取旧版身份创建记录失败", { error: safeError(error) });
201
212
  }
213
+ for (const aid of Array.isArray(this.config.created_aids) ? this.config.created_aids : []) {
214
+ if (typeof aid === "string" && aidPattern.test(aid))
215
+ this.createdAids.add(aid);
216
+ }
202
217
  for (const key of ["aid", "owner_aid", "aun_path", "encryption_seed", "root_ca_path", "allow_from", "agent_plugin"]) {
203
218
  if (this.config[key] !== undefined && typeof this.config[key] !== "string")
204
219
  throw new Error(`${key} 必须是字符串`);
@@ -239,6 +254,7 @@ class AunPlugin {
239
254
  }
240
255
  if (this.aid)
241
256
  this.identities.add(this.aid);
257
+ this.acceptingMessages = true;
242
258
  for (const aid of this.identities)
243
259
  await this.connectAid(aid);
244
260
  // 密钥库中的其余 AID 也自动连接:不再依赖 ec 或 EvolCore 进程
@@ -279,10 +295,12 @@ class AunPlugin {
279
295
  }
280
296
  async deactivate(_reason) {
281
297
  this.status = "degraded";
282
- for (const { client, subscription } of this.clients.values()) {
298
+ this.acceptingMessages = false;
299
+ for (const { subscription } of this.clients.values())
283
300
  subscription?.unsubscribe();
301
+ await Promise.allSettled(this.pendingMessages.values());
302
+ for (const { client } of this.clients.values())
284
303
  await client.close();
285
- }
286
304
  this.clients.clear();
287
305
  await this.store?.close();
288
306
  this.store = undefined;
@@ -487,6 +505,8 @@ class AunPlugin {
487
505
  const agents = await this.agents();
488
506
  return { message: agents.length ? `当前已就绪的 Agent 插件:\n${agents.map((agent) => `${agent.id}(${agent.displayName})`).join("\n")}` : "暂无已就绪的 Agent 插件", data: agents };
489
507
  }
508
+ if (command === "default-owner")
509
+ return this.defaultOwnerCommand(args);
490
510
  if (command === "create")
491
511
  return this.createCommand(args);
492
512
  if (command === "bind" && args.length === 2) {
@@ -522,10 +542,29 @@ class AunPlugin {
522
542
  }
523
543
  return { message: `已解除 ${pluginId} 的 AID 关联`, data: { agent: pluginId, unboundAids } };
524
544
  }
525
- if (!["status", "list", "agents", "create", "bind", "unbind"].includes(command ?? ""))
545
+ if (!["status", "list", "agents", "default-owner", "create", "bind", "unbind"].includes(command ?? ""))
526
546
  throw new AunCommandError(`未知 AUN 命令:${command ?? ""}`);
527
547
  throw new AunCommandError(`AUN 命令参数无效:${command}`);
528
548
  }
549
+ async defaultOwnerCommand(args) {
550
+ const owner = typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
551
+ if (!args.length)
552
+ return { message: owner ? `默认 Owner:${owner}` : "未配置默认 Owner", data: { owner_aid: owner || undefined } };
553
+ if (args.length !== 2 || args[0] !== "set")
554
+ throw new AunCommandError("AUN 默认 Owner 命令参数无效");
555
+ const nextOwner = args[1].trim();
556
+ if (!aidPattern.test(nextOwner))
557
+ throw new AunCommandError("Owner AID 格式无效");
558
+ this.config.owner_aid = nextOwner;
559
+ try {
560
+ await this.persist();
561
+ }
562
+ catch (error) {
563
+ this.config.owner_aid = owner;
564
+ throw error;
565
+ }
566
+ return { message: "默认 Owner 已保存", data: { configured: true } };
567
+ }
529
568
  async createCommand(args) {
530
569
  const aid = args[0] ?? "";
531
570
  if (!aidPattern.test(aid))
@@ -544,8 +583,10 @@ class AunPlugin {
544
583
  else
545
584
  throw new AunCommandError(`不支持的创建参数:${option}`);
546
585
  }
586
+ if (!owner)
587
+ owner = typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
547
588
  if (!owner || !aidPattern.test(owner))
548
- throw new AunCommandError("需要有效的 --owner AID");
589
+ throw new AunCommandError("需要有效的 --owner AID,或先设置默认 Owner");
549
590
  if (agent) {
550
591
  this.validatePluginId(agent);
551
592
  await this.requireAgent(agent);
@@ -700,26 +741,109 @@ class AunPlugin {
700
741
  }
701
742
  };
702
743
  async onMessage(payload, aid) {
744
+ if (!this.acceptingMessages)
745
+ return;
703
746
  const message = (0, messages_1.normalizeAunMessage)(payload, aid, typeof this.config.allow_from === "string" ? this.config.allow_from : undefined);
704
747
  if (!message || !this.seen.add(message.messageId ? `${aid}:${message.messageId}` : ""))
705
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) {
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
+ }
706
793
  const channelMessage = {
707
794
  channel: "aun", recipientId: aid, peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
795
+ ...(attachments.length ? { attachments } : {}),
708
796
  agentPlugin: this.bindings[aid] || (typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork"),
709
797
  reply: async (text) => {
710
798
  if (!text.trim())
711
799
  return;
712
- const payload = { type: "text", text };
713
- if (message.threadId)
714
- payload.thread_id = message.threadId;
715
- const params = { to: message.peerId, payload, encrypt: true };
800
+ replyAttempted = 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;
716
810
  try {
717
- 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
+ }
718
839
  }
719
840
  catch (error) {
720
- if (!this.isE2eeError(error))
721
- throw error;
722
- 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;
723
847
  }
724
848
  },
725
849
  };
@@ -728,6 +852,29 @@ class AunPlugin {
728
852
  }
729
853
  catch (error) {
730
854
  this.context?.logger.warn("AUN 消息处理失败", { error: safeError(error) });
855
+ if (!replyAttempted) {
856
+ const action = attachments.length > 0 ? "附件处理失败" : newSessionCommand.test(message.text.trim()) ? "新建会话失败" : "消息处理失败";
857
+ try {
858
+ await channelMessage.reply(`${action}:${safeError(error)}`);
859
+ }
860
+ catch (replyError) {
861
+ this.context?.logger.warn("AUN 错误回复失败", { error: safeError(replyError) });
862
+ }
863
+ }
864
+ }
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 });
731
878
  }
732
879
  }
733
880
  isE2eeError(error) {
@@ -775,5 +922,6 @@ async function readResponse(response, limit) {
775
922
  return Buffer.concat(chunks).toString("utf8");
776
923
  }
777
924
  function redact(message) {
778
- 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=[已隐藏]");
779
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 {
@@ -6,7 +6,7 @@
6
6
  <header><h1>AUN 身份</h1><button id="refresh" type="button">刷新</button></header>
7
7
  <p id="status" role="status"></p>
8
8
  <div class="table-scroll"><table><thead><tr><th>AID</th><th>来源</th><th>已关联 Agent</th><th>操作</th></tr></thead><tbody id="identities"></tbody></table></div>
9
- <form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="例如:example.agentid.cn"></label><label>Owner AID<input name="ownerAid" required placeholder="例如:owner.agentid.cn(本地归属配置)"><small class="hint">Owner 保存为本地归属配置;不代表 AUN 协议中的 Owner 关系已激活。</small></label></div><button type="submit">创建</button></form>
9
+ <form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="例如:example.agentid.cn"></label><label>Owner AID<input name="ownerAid" placeholder="例如:owner.agentid.cn"><small class="hint">留空时使用已配置的默认 Owner;未配置时需填写。Owner 保存为本地归属配置;不代表 AUN 协议中的 Owner 关系已激活。</small></label></div><button type="submit">创建</button></form>
10
10
  <form id="config-form"><h2>通道配置</h2><div class="fields"><label>默认 Agent<select name="agent_plugin"></select></label><label>允许的来源<input name="allow_from"></label><label>AUN 存储路径<input name="aun_path"></label><label>加密种子<input name="encryption_seed" type="password" autocomplete="new-password" placeholder="留空则不修改"></label></div><button type="submit">保存配置</button></form>
11
11
  </main>
12
12
  <script src="/abweb/channels/aun/app.js"></script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-aun",
3
- "version": "1.0.21",
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.21",
3
+ "version": "1.0.23",
4
4
  "type": "channel",
5
5
  "displayName": "AUN 通道",
6
6
  "entry": "dist/src-ts/plugins/aun/index.js",