agent-bridge-aun 1.0.44 → 1.0.45-sdk0531.3
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 +24 -1
- package/dist/src-ts/plugins/aun/directory.js +50 -0
- package/dist/src-ts/plugins/aun/fs-commands.js +121 -0
- package/dist/src-ts/plugins/aun/fs-transfer.js +90 -0
- package/dist/src-ts/plugins/aun/fs.js +416 -0
- package/dist/src-ts/plugins/aun/index.js +404 -10
- package/dist/src-ts/plugins/aun/owner-directory.js +174 -0
- package/dist/src-ts/plugins/aun/ownership.js +43 -0
- package/dist/src-ts/plugins/aun/peer-messaging.js +224 -0
- package/dist/src-ts/plugins/aun/public/app.js +8 -3
- package/package.json +4 -3
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -6,12 +6,35 @@
|
|
|
6
6
|
|
|
7
7
|
安装后在 ABWeb 的插件管理中启动 AUN 通道。收到的 AID 消息仅投递给用户在 AUN 身份页中明确关联的 Agent;未关联的 AID 消息会被忽略。旧配置中的 `agent_plugin` 字段不再作为默认路由。配置字段 `owner_aid` 是可选字符串,用作默认 Owner。AID 创建接口为 `POST /api/plugins/aun/aid`,请求内容为 `{"aid":"新AID.agentid.cn","owner_aid":"所有者AID.agentid.cn"}`。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
创建调用 SDK 0.5.31 的 `register(aid, { type: "agent", owner })`。注册、连接成功与 Owner 双边共识是不同状态;Owner 声明仍可能为 `single_sided/pending`,不能把“已创建”解释为归属已激活。创建后尝试把新 AID 加入 Owner 名录,默认 `allow_pa_use: true`;只有本地拥有 Owner 私钥且 Storage 可用时才能同步,失败通过 `directorySync` 单独返回。Gateway 信任根使用随插件提供并校验指纹的 AUN 根证书,不从 Gateway 下载未经验证的 CA 链。
|
|
10
10
|
|
|
11
11
|
核心支持插件命令后,可运行 `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 <aid>`。关联或解绑只修改指定 AID,不会影响同一 Agent 下的其它 AID,也不会写入 Agent 全局 AID 配置。`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 查询会在终端显示当前值。
|
|
12
12
|
|
|
13
13
|
该插件依赖 `@agentunion/fastaun`,要求 Node.js 22.16 或更高版本。
|
|
14
14
|
|
|
15
|
+
## 原生文件命令
|
|
16
|
+
|
|
17
|
+
`mab fs` 与 `mab aun fs` 共用 AUN 插件当前连接,不要求安装或运行 EvolCore。`--as` 只能选择后台已连接的身份;只有一个连接时可省略,多连接时必须指定。远端后端根据经过验证的名片类型选择,不依赖 Owner 名录同步。
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
mab fs stat 209530905.agentid.cn:/.well-known/ability/index.json --as devproj.agentid.cn --json
|
|
21
|
+
mab fs cat 209530905.agentid.cn:/.well-known/ability/index.json --as devproj.agentid.cn --authoritative --json
|
|
22
|
+
mab fs cp ./说明.txt devproj.agentid.cn:/docs/说明.txt --as devproj.agentid.cn
|
|
23
|
+
mab fs cp devproj.agentid.cn:/docs/说明.txt ./下载.txt --as devproj.agentid.cn
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
命令包括 `ls`、`stat`、`lstat`、`cat`、`cp`、`mv`、`rm`、`mkdir`、`ln -s`、`chmod`、`setfacl`、`getfacl`、`token`、`find`、`df`、`link`、`mount`、`approve`、`reject` 和 `umount`(也接受 `unmount`)。SDK 后端没有对应能力时返回 `UNSUPPORTED`。群空间当前不支持正文读取、软链、可见性修改、令牌、分享链接或挂载审批;个人空间不支持跨 AID 移动,跨 personal/group 的复制或移动也会拒绝。`stat` 默认不跟随末级软链,`-L` 才跟随。
|
|
27
|
+
|
|
28
|
+
文本读取最多 256 KiB,二进制摘要输入最多 64 KiB,列表最多 512 项;超限明确失败。普通 Storage 读取按服务内联上限分块,并在读取前后核对大小、版本和已有哈希。本机文件上传和下载使用独立的管理身份校验及二进制传输接口,单文件最多 32 MiB、传输最多 60 秒,不把本机路径交给后台读写。下载先在目标目录暂存,校验大小和 SHA-256 后提交;覆盖已有文件必须显式加 `--overwrite`。`--authoritative` 使用个人 Storage 的权威对象读取,不以缓存参数替代,也不适用于群空间。本机认证管理入口可取得新签发的 token 和分享链接,插件不把这些值写入运行日志。
|
|
29
|
+
|
|
30
|
+
## AUN 私聊中的兼容入口
|
|
31
|
+
|
|
32
|
+
私聊可以发送一条完整、单行的 `ec fs cat <AID:/路径>` (可选 `--format json`)。插件直接解析参数,不执行 shell,不安装或替换本机 `ec`。普通聊天提及、引用、代码块和多条命令不会触发执行;消息里的 `--as` 不能选择宿主身份。
|
|
33
|
+
|
|
34
|
+
兼容入口只读取能够匿名公开访问的文件,不借用宿主对私有对象的权限。公开正文走有大小限制的匿名 HTTPS 读取;`stat`、`lstat`、目录、配额和 ACL 没有可证明的匿名视图,当前返回 `UNSUPPORTED`。私聊兼容入口不接受 `--token` 或 `--authoritative`。文件写入、删除、权限、令牌及挂载变更会提示需要 Owner 在本机明确操作,不接受远端文本自报授权。回复保留原私聊话题。
|
|
35
|
+
|
|
36
|
+
文件可读只证明 FS 发布可访问。还需核对 L1 的 `publisher`、`capabilities` 和每个精确 `detail_ref` 对应的 L3;在线发现及真实 Ability 调用分别验收,不能以索引可读代替执行成功。
|
|
37
|
+
|
|
15
38
|
附件回复按图片、视频和普通文件分组发送,并保留原话题和请求引用。图片使用不带普通说明正文的 `type: "image"` 消息,说明和下载链接另发加密文本,兼容 AUN 网页的图片卡片显示规则;MIME 类型为 `video/*` 的附件使用 `type: "video"`,普通文件使用 `type: "file"`,二者正文均附下载链接。三类附件参考 EvolCore 的标准附件格式,由插件直接通过 AUN SDK 上传可公开读取的对象,发送不带受限分享 URL 的标准对象引用,便于接收端取得下载票据及播放器分段读取。持有对象地址者可读取附件;旧版本已发送的私有分享不会自动改为公开。发送失败或状态未确认时仍向原 Peer 提供下载链接,不自动重发附件。图片已发送而说明失败时明确报告说明发送阶段,不误报 FS 上传失败。附件链路不调用 `ec` 命令,也不依赖 EvolCore 运行。
|
|
16
39
|
|
|
17
40
|
ABWeb 的 AUN 设置只展示已连接 AID,并按来源分组。成功注册并记录在插件 `created_aids` 中的 AID 属于本工程;共享密钥库中其余 AID 属于 EvolCore。旧版 `created-aids.json` 记录继续兼容。来源只用于展示,连接和关联由 AUN 插件直接管理。列表先显示已关联 Agent 的身份,各组按最近成功关联的时间倒序排列,时间相同或旧记录没有时间时按 AID 名称排序。关联时间保存在插件配置的 `binding_times` 中,刷新或重启后保留;旧记录不补造时间,再次关联成功后开始记录。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUN_DIRECTORY_USAGE = void 0;
|
|
4
|
+
exports.parseDirectoryCommand = parseDirectoryCommand;
|
|
5
|
+
exports.queryOwnerDirectory = queryOwnerDirectory;
|
|
6
|
+
const fs_1 = require("./fs");
|
|
7
|
+
exports.AUN_DIRECTORY_USAGE = "mab directory list [--page <页码>] [--limit <1-100>] [--self <已连接 AID>] [--format json]\n mab directory show <AID或名称> [--self <已连接 AID>] [--format json]";
|
|
8
|
+
function parseDirectoryCommand(args) {
|
|
9
|
+
const action = args[0] === "info" ? "show" : args[0];
|
|
10
|
+
if (action !== "list" && action !== "show")
|
|
11
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", exports.AUN_DIRECTORY_USAGE);
|
|
12
|
+
const flags = new Map();
|
|
13
|
+
const positional = [];
|
|
14
|
+
for (let i = 1; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg.startsWith("--")) {
|
|
17
|
+
if (!["--self", "--page", "--limit"].includes(arg) || flags.has(arg) || !args[i + 1] || args[i + 1].startsWith("--"))
|
|
18
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", `名录参数无效:${arg}`);
|
|
19
|
+
flags.set(arg, args[++i]);
|
|
20
|
+
}
|
|
21
|
+
else
|
|
22
|
+
positional.push(arg);
|
|
23
|
+
}
|
|
24
|
+
const page = flags.get("--page") ?? "1", limit = flags.get("--limit") ?? "20";
|
|
25
|
+
if (!/^[1-9]\d*$/.test(page) || !Number.isSafeInteger(Number(page)) || !/^[1-9]\d*$/.test(limit) || Number(limit) > 100
|
|
26
|
+
|| action === "list" && positional.length || action === "show" && (!positional.length || flags.has("--page") || flags.has("--limit")))
|
|
27
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", "名录命令参数无效,页码从 1 开始,每页最多 100 条;show 需要 AID 或名称");
|
|
28
|
+
return { action, selfAid: flags.get("--self"), page: Number(page), limit: Number(limit), target: positional.join(" ") || undefined };
|
|
29
|
+
}
|
|
30
|
+
function queryOwnerDirectory(directory, query, selfAid, ownerAid) {
|
|
31
|
+
const blocked = new Set(directory.blocked_aids.map((item) => item.target_id));
|
|
32
|
+
const entries = [];
|
|
33
|
+
for (const entry of [directory.owner, directory.personal_assistant, ...directory.entries]) {
|
|
34
|
+
if (entry && typeof entry.aid === "string" && !blocked.has(entry.aid) && !entries.some(item => item.aid === entry.aid))
|
|
35
|
+
entries.push(entry);
|
|
36
|
+
}
|
|
37
|
+
const base = { selfAid, ownerAid, path: "/data/directories/v2/contacts.json" };
|
|
38
|
+
if (query.action === "show") {
|
|
39
|
+
const target = query.target.replace(/^@/, "").toLowerCase();
|
|
40
|
+
const matches = entries.filter(entry => entry.aid.toLowerCase() === target || typeof entry.name === "string" && entry.name.toLowerCase() === target);
|
|
41
|
+
if (!matches.length)
|
|
42
|
+
throw new fs_1.AunFsError("DIRECTORY_NOT_FOUND", `Owner 名录中未找到联系人:${query.target}`);
|
|
43
|
+
return { message: `名录详情:\n${JSON.stringify(matches, null, 2)}`, data: { ...base, entries: matches, ...(matches.length === 1 ? { entry: matches[0] } : {}) } };
|
|
44
|
+
}
|
|
45
|
+
const offset = (query.page - 1) * query.limit;
|
|
46
|
+
if (!Number.isSafeInteger(offset))
|
|
47
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", "名录页码超出范围");
|
|
48
|
+
const page = entries.slice(offset, offset + query.limit);
|
|
49
|
+
return { message: `Owner 名录(第 ${query.page} 页,共 ${entries.length} 项):\n${page.map(entry => `${entry.aid} ${entry.name ?? ""}`).join("\n")}`, data: { ...base, entries: page, total: entries.length, page: query.page, limit: query.limit, hasMore: offset + page.length < entries.length } };
|
|
50
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUN_FS_READ_COMMANDS = exports.AUN_FS_USAGE = void 0;
|
|
4
|
+
exports.parseAunFsCommand = parseAunFsCommand;
|
|
5
|
+
const fs_1 = require("./fs");
|
|
6
|
+
exports.AUN_FS_USAGE = "用法:mab fs <ls|stat|lstat|cat|cp|mv|rm|mkdir|ln|chmod|setfacl|getfacl|token|find|df|link|mount|approve|reject|umount|unmount> [参数...] [--as <aid>] [--json]";
|
|
7
|
+
exports.AUN_FS_READ_COMMANDS = new Set(["ls", "stat", "lstat", "cat", "find", "df"]);
|
|
8
|
+
function parseAunFsCommand(argv) {
|
|
9
|
+
const [command, ...args] = argv;
|
|
10
|
+
const fields = {
|
|
11
|
+
"--as": "asAid", "--page": "page", "--page-size": "pageSize", "--name": "name", "--type": "nodeType", "--size": "size", "--mtime": "mtime",
|
|
12
|
+
"--token": "token", "--expires-at": "expiresAt", "--max-reads": "maxReads", "--allowed-aids": "allowedAids", "--expire-in-seconds": "expireInSeconds",
|
|
13
|
+
"--source": "source", "--volume": "volumeId", "--source-bucket": "sourceBucket", "--mount-id": "mountId", "--request-id": "requestId", "--allow-roles": "allowRoles", "--max-uses": "maxUses",
|
|
14
|
+
};
|
|
15
|
+
const flags = { "-r": "recursive", "--recursive": "recursive", "-p": "parents", "--parents": "parents", "-L": "followSymlinks", "--follow": "followSymlinks", "--follow-symlinks": "followSymlinks", "--overwrite": "overwrite", "--force": "overwrite", "--authoritative": "authoritative", "--readonly": "readonly", "--require-approval": "requireApproval" };
|
|
16
|
+
const allowed = {
|
|
17
|
+
ls: ["page", "token"], stat: ["followSymlinks", "token"], lstat: ["token"], cat: ["authoritative", "token"], cp: ["recursive", "followSymlinks", "overwrite"], mv: ["overwrite"], rm: ["recursive"], mkdir: ["parents"], ln: ["overwrite"],
|
|
18
|
+
chmod: ["allowRoles"], setfacl: ["expiresAt", "maxUses"], getfacl: [], token: ["token", "expiresAt", "maxReads", "allowedAids"], find: ["name", "nodeType", "size", "mtime", "page", "pageSize", "token"], df: [], link: ["expireInSeconds", "allowedAids"],
|
|
19
|
+
mount: ["source", "volumeId", "sourceBucket", "readonly", "requireApproval", "expiresAt"], approve: ["mountId", "requestId"], reject: ["mountId", "requestId"], umount: [], unmount: [],
|
|
20
|
+
};
|
|
21
|
+
if (!allowed[command])
|
|
22
|
+
throw new fs_1.AunFsError("UNKNOWN_COMMAND", `未知 FS 子命令:${command ?? ""}`);
|
|
23
|
+
const request = { command };
|
|
24
|
+
const positional = [];
|
|
25
|
+
const used = new Set();
|
|
26
|
+
let asAid;
|
|
27
|
+
let acl;
|
|
28
|
+
let aclRemove = false;
|
|
29
|
+
let symbolic = false;
|
|
30
|
+
for (let index = 0; index < args.length; index++) {
|
|
31
|
+
const arg = args[index];
|
|
32
|
+
if (command === "ln" && arg === "-s" && !symbolic) {
|
|
33
|
+
symbolic = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (command === "setfacl" && ["-m", "-x"].includes(arg) && acl === undefined) {
|
|
37
|
+
aclRemove = arg === "-x";
|
|
38
|
+
acl = args[++index];
|
|
39
|
+
if (!acl)
|
|
40
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", `${arg} 需要参数`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!arg.startsWith("-") || command === "chmod" && ["+r", "o-r"].includes(arg)) {
|
|
44
|
+
positional.push(arg);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const field = fields[arg] ?? flags[arg];
|
|
48
|
+
if (!field || field !== "asAid" && !allowed[command].includes(field) || used.has(field))
|
|
49
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", `不支持的 FS 参数:${arg}`);
|
|
50
|
+
used.add(field);
|
|
51
|
+
let value = true;
|
|
52
|
+
if (fields[arg]) {
|
|
53
|
+
value = args[++index];
|
|
54
|
+
if (typeof value !== "string" || !value || value.startsWith("--"))
|
|
55
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", `${arg} 需要参数`);
|
|
56
|
+
if (["page", "pageSize", "expiresAt", "maxReads", "maxUses", "expireInSeconds"].includes(field)) {
|
|
57
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1)
|
|
58
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", `${arg} 需要正整数`);
|
|
59
|
+
value = Number(value);
|
|
60
|
+
}
|
|
61
|
+
else if (["allowRoles", "allowedAids"].includes(field)) {
|
|
62
|
+
value = value.split(",");
|
|
63
|
+
if (value.some(item => !item || item.trim() !== item))
|
|
64
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", `${arg} 需要逗号分隔的非空值`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (field === "asAid")
|
|
68
|
+
asAid = String(value);
|
|
69
|
+
else
|
|
70
|
+
Object.assign(request, { [field]: value });
|
|
71
|
+
}
|
|
72
|
+
if (["cp", "mv"].includes(command)) {
|
|
73
|
+
if (positional.length !== 2)
|
|
74
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", `${command} 需要来源和目标路径`);
|
|
75
|
+
[request.source, request.destination] = positional;
|
|
76
|
+
request.path = request.destination;
|
|
77
|
+
}
|
|
78
|
+
else if (command === "mount") {
|
|
79
|
+
if (positional.length === 2 && !request.source && !request.volumeId) {
|
|
80
|
+
[request.source, request.destination] = positional;
|
|
81
|
+
request.path = request.destination;
|
|
82
|
+
}
|
|
83
|
+
else if (positional.length === 1 && !(request.source && request.volumeId))
|
|
84
|
+
request.path = positional[0];
|
|
85
|
+
else
|
|
86
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", "mount 需要目标路径和唯一的 --source/--volume,或来源与目标路径");
|
|
87
|
+
}
|
|
88
|
+
else if (command === "ln") {
|
|
89
|
+
if (!symbolic || positional.length !== 2)
|
|
90
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", "ln 需要 -s、目标和软链路径");
|
|
91
|
+
[request.target, request.path] = positional;
|
|
92
|
+
}
|
|
93
|
+
else if (command === "chmod") {
|
|
94
|
+
if (positional.length !== 2 || !["public", "private", "+r", "o-r"].includes(positional[0]))
|
|
95
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", "chmod 需要 public/private 和目标路径");
|
|
96
|
+
request.visibility = ["public", "+r"].includes(positional[0]) ? "public" : "private";
|
|
97
|
+
request.path = positional[1];
|
|
98
|
+
}
|
|
99
|
+
else if (command === "token") {
|
|
100
|
+
if (positional.length !== 2 || !["issue", "revoke", "ls"].includes(positional[0]) || positional[0] === "revoke" && !request.token)
|
|
101
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", "token 需要 issue/revoke/ls、路径;revoke 还需要 --token");
|
|
102
|
+
[request.action, request.path] = positional;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (positional.length !== 1)
|
|
106
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", `${command} 需要一个远端路径`);
|
|
107
|
+
request.path = positional[0];
|
|
108
|
+
if (command === "df" && /^[^:]+:$/.test(request.path))
|
|
109
|
+
request.path += "/";
|
|
110
|
+
}
|
|
111
|
+
if (command === "setfacl") {
|
|
112
|
+
const match = acl?.match(aclRemove ? /^(aid:[^:]+|role:(?:member|admin))$/ : /^(aid:[^:]+|role:(?:member|admin)):([^:]+)$/);
|
|
113
|
+
if (!match)
|
|
114
|
+
throw new fs_1.AunFsError("INVALID_REQUEST", "setfacl 需要 -m aid:<AID>:<权限>/role:member:<权限>/role:admin:<权限>,或 -x <授权对象>");
|
|
115
|
+
request.action = aclRemove ? "remove" : "set";
|
|
116
|
+
request.granteeAid = match[1].startsWith("aid:") ? match[1].slice(4) : match[1];
|
|
117
|
+
if (!aclRemove)
|
|
118
|
+
request.perms = match[2];
|
|
119
|
+
}
|
|
120
|
+
return { request, ...(asAid ? { asAid } : {}) };
|
|
121
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FS_TRANSFER_PATH = exports.FS_TRANSFER_TIMEOUT_MS = exports.MAX_FS_TRANSFER_BYTES = void 0;
|
|
4
|
+
exports.createFsTransferHandler = createFsTransferHandler;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const promises_1 = require("node:stream/promises");
|
|
7
|
+
const fs_1 = require("./fs");
|
|
8
|
+
exports.MAX_FS_TRANSFER_BYTES = 32 * 1024 * 1024;
|
|
9
|
+
exports.FS_TRANSFER_TIMEOUT_MS = 60_000;
|
|
10
|
+
exports.FS_TRANSFER_PATH = "/api/plugins/aun/fs-transfer";
|
|
11
|
+
function json(response, status, value) {
|
|
12
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
13
|
+
response.end(JSON.stringify(value));
|
|
14
|
+
}
|
|
15
|
+
function createFsTransferHandler(backend, actor) {
|
|
16
|
+
return async (request, response) => {
|
|
17
|
+
if (!actor(request)) {
|
|
18
|
+
json(response, 403, { ok: false, message: "文件传输需要本机或已认证的管理身份" });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let timer;
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
response.once("close", () => { cancelled = true; });
|
|
24
|
+
try {
|
|
25
|
+
await Promise.race([
|
|
26
|
+
(async () => {
|
|
27
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
28
|
+
if (url.pathname !== exports.FS_TRANSFER_PATH || !["GET", "PUT"].includes(request.method ?? ""))
|
|
29
|
+
throw new Error("文件传输接口或方法无效");
|
|
30
|
+
if ([...url.searchParams.keys()].some(key => !["path", "as", "overwrite"].includes(key)) || url.searchParams.getAll("path").length !== 1 || url.searchParams.getAll("as").length > 1
|
|
31
|
+
|| url.searchParams.getAll("overwrite").length > 1 || url.searchParams.has("overwrite") && url.searchParams.get("overwrite") !== "true")
|
|
32
|
+
throw new Error("文件传输参数无效");
|
|
33
|
+
const target = url.searchParams.get("path");
|
|
34
|
+
const asAid = url.searchParams.get("as") ?? undefined;
|
|
35
|
+
(0, fs_1.parseAunFsPath)(target);
|
|
36
|
+
if (asAid !== undefined)
|
|
37
|
+
(0, fs_1.parseAunFsPath)(`${asAid}:/identity`);
|
|
38
|
+
if (request.method === "GET") {
|
|
39
|
+
const bytes = await backend.download(target, asAid);
|
|
40
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength > exports.MAX_FS_TRANSFER_BYTES)
|
|
41
|
+
throw new Error("文件传输不能超过 32 MiB");
|
|
42
|
+
if (cancelled || response.destroyed)
|
|
43
|
+
return;
|
|
44
|
+
response.writeHead(200, { "content-type": "application/octet-stream", "cache-control": "no-store", "content-length": bytes.byteLength, "x-content-sha256": (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex") });
|
|
45
|
+
response.end(bytes);
|
|
46
|
+
await (0, promises_1.finished)(response, { cleanup: true });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const rawSize = request.headers["content-length"];
|
|
50
|
+
const expectedHash = request.headers["x-content-sha256"];
|
|
51
|
+
if (request.headers["content-type"] !== "application/octet-stream" || typeof rawSize !== "string" || !/^\d+$/.test(rawSize)
|
|
52
|
+
|| !Number.isSafeInteger(Number(rawSize)) || Number(rawSize) > exports.MAX_FS_TRANSFER_BYTES || typeof expectedHash !== "string" || !/^[a-f0-9]{64}$/.test(expectedHash))
|
|
53
|
+
throw new Error("上传需要有效的大小、SHA256 和二进制内容类型,文件不能超过 32 MiB");
|
|
54
|
+
const chunks = [];
|
|
55
|
+
let size = 0;
|
|
56
|
+
const hash = (0, node_crypto_1.createHash)("sha256");
|
|
57
|
+
for await (const chunk of request) {
|
|
58
|
+
const bytes = Buffer.from(chunk);
|
|
59
|
+
size += bytes.length;
|
|
60
|
+
if (cancelled || size > Number(rawSize) || size > exports.MAX_FS_TRANSFER_BYTES)
|
|
61
|
+
throw new Error("上传文件大小校验失败或传输已结束");
|
|
62
|
+
chunks.push(bytes);
|
|
63
|
+
hash.update(bytes);
|
|
64
|
+
}
|
|
65
|
+
const sha256 = hash.digest("hex");
|
|
66
|
+
if (size !== Number(rawSize) || sha256 !== expectedHash)
|
|
67
|
+
throw new Error("上传文件大小或哈希校验失败");
|
|
68
|
+
if (cancelled || response.destroyed)
|
|
69
|
+
return;
|
|
70
|
+
await backend.upload(target, Buffer.concat(chunks, size), asAid, { overwrite: url.searchParams.get("overwrite") === "true" });
|
|
71
|
+
if (!cancelled && !response.destroyed)
|
|
72
|
+
json(response, 200, { ok: true, message: `文件已上传(${size} 字节)`, data: { command: "cp", backend: "personal", path: target, direction: "upload", size, sha256 } });
|
|
73
|
+
})(),
|
|
74
|
+
new Promise((_resolve, reject) => { timer = setTimeout(() => { cancelled = true; request.destroy(); reject(new Error("文件传输超时;上传状态可能未知,请先核对远端文件")); }, exports.FS_TRANSFER_TIMEOUT_MS); }),
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!response.destroyed) {
|
|
79
|
+
if (response.headersSent)
|
|
80
|
+
response.destroy();
|
|
81
|
+
else
|
|
82
|
+
json(response, 400, { ok: false, message: error instanceof Error ? error.message : "文件传输失败", ...(error.code ? { data: { code: error.code } } : {}) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
if (timer)
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|