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.
|
|
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
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
855
|
+
if (!replyAttempted) {
|
|
856
|
+
const action = attachments.length > 0 ? "附件处理失败" : newSessionCommand.test(message.text.trim()) ? "新建会话失败" : "消息处理失败";
|
|
769
857
|
try {
|
|
770
|
-
await channelMessage.reply(
|
|
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(/
|
|
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
|
-
|
|
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