mu-agent-bridge 1.0.32 → 1.0.33
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 +0 -16
- package/dist/src-ts/attachments.js +62 -0
- package/dist/src-ts/channel-sessions.js +8 -7
- package/dist/src-ts/cli-commands.js +2 -6
- package/dist/src-ts/control-server.js +0 -2
- package/dist/src-ts/daemon-process.js +2 -3
- package/dist/src-ts/dsh-agent.js +2 -4
- package/dist/src-ts/plugin-manager.js +22 -29
- package/dist/src-ts/plugins/aun/attachments.js +245 -0
- package/dist/src-ts/plugins/aun/index.js +117 -14
- package/dist/src-ts/plugins/aun/messages.js +25 -1
- package/dist/src-ts/plugins/cdp-draft.js +54 -0
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +10 -1
- package/dist/src-ts/plugins/chatgpt/index.js +16 -6
- package/dist/src-ts/plugins/doubao/cdp-client.js +72 -7
- package/dist/src-ts/plugins/doubao/index.js +2 -2
- package/dist/src-ts/plugins/dsh/index.js +0 -1
- package/dist/src-ts/plugins/feishu/index.js +8 -21
- package/dist/src-ts/plugins/feishu/public/app.js +8 -20
- package/dist/src-ts/plugins/feishu/public/index.html +1 -1
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +26 -0
- package/dist/src-ts/plugins/qwenwork/index.js +2 -1
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +227 -16
- package/dist/src-ts/plugins/workbuddy/index.js +16 -5
- package/dist/tests-ts/attachments.test.js +89 -0
- package/dist/tests-ts/aun-attachments.test.js +209 -0
- package/dist/tests-ts/aun-messages.test.js +41 -0
- package/dist/tests-ts/aun-plugin.test.js +222 -6
- package/dist/tests-ts/cdp-draft-recovery.test.js +149 -0
- package/dist/tests-ts/channel-sessions.test.js +0 -22
- package/dist/tests-ts/chatgpt-plugin.test.js +66 -6
- package/dist/tests-ts/cli-commands.test.js +0 -58
- package/dist/tests-ts/daemon-process.test.js +0 -1
- package/dist/tests-ts/doubao-plugin.test.js +143 -6
- package/dist/tests-ts/dsh-agent.test.js +0 -25
- package/dist/tests-ts/feishu-page.test.js +3 -10
- package/dist/tests-ts/feishu-plugin.test.js +1 -24
- package/dist/tests-ts/plugin-manager.test.js +61 -97
- package/dist/tests-ts/qwenwork-plugin.test.js +3 -7
- package/dist/tests-ts/workbuddy-plugin.test.js +352 -1
- package/package.json +1 -2
- package/plugins/aun/dist/src-ts/attachments.js +62 -0
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +245 -0
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +117 -14
- package/plugins/aun/dist/src-ts/plugins/aun/messages.js +25 -1
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +54 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +10 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +16 -6
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +1 -1
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +72 -7
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +2 -2
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -1
- package/plugins/dsh/dist/src-ts/dsh-agent.js +2 -4
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +0 -1
- package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +8 -21
- package/plugins/feishu/dist/src-ts/plugins/feishu/public/app.js +8 -20
- package/plugins/feishu/dist/src-ts/plugins/feishu/public/index.html +1 -1
- package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +54 -0
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +26 -0
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +2 -1
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/dist/src-ts/attachments.js +62 -0
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +227 -16
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +16 -5
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +3 -3
- package/dist/installer/mu-agent-bridge.msi +0 -0
- package/dist/src-ts/secret-input.js +0 -47
- package/dist/tests-ts/installer-assets.test.js +0 -84
package/README.md
CHANGED
|
@@ -142,19 +142,3 @@ Windows 路径时,可设置 `DSH_INSTALL_DIR`。
|
|
|
142
142
|
## AUN 身份管理
|
|
143
143
|
|
|
144
144
|
创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md);给 Codex、Claude Code 直接执行的 CLI 说明见 [使用说明](使用说明.md)。
|
|
145
|
-
|
|
146
|
-
### 飞书凭据与办公 Agent 关联
|
|
147
|
-
|
|
148
|
-
在 ABWeb 的“飞书通道 → 手动配置凭据”中填写 App ID、App Secret,并选择“关联办公 Agent”。Secret 使用密码框显示;已保存的密钥保持掩码,不通过状态接口返回。WorkBuddy、千问办公、豆包根据运行后台的机器上是否存在可执行文件检测,文件缺失或插件未就绪的选项置灰。自定义安装位置请在对应插件配置中填写 `executablePath`。
|
|
149
|
-
|
|
150
|
-
也可以使用 CLI,Secret 在命令执行后隐藏输入,不写在命令参数中:
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
mab feishu configure cli_你的应用ID --agent workbuddy
|
|
154
|
-
mab feishu agents
|
|
155
|
-
mab feishu list
|
|
156
|
-
mab feishu bind cli_你的应用ID workbuddy
|
|
157
|
-
mab feishu unbind cli_你的应用ID
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
`configure` 保存凭据及可选关联后,需要在 ABWeb 重载飞书插件以连接机器人。非交互终端请使用网页密码框;不要把 Secret 写进命令行、脚本或仓库。
|
|
@@ -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
|
+
}
|
|
@@ -22,7 +22,7 @@ class ChannelSessions {
|
|
|
22
22
|
throw new Error("通道会话文件格式无效");
|
|
23
23
|
for (const [key, value] of Object.entries(data)) {
|
|
24
24
|
const entry = value;
|
|
25
|
-
if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string"
|
|
25
|
+
if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string")
|
|
26
26
|
throw new Error("通道会话记录无效");
|
|
27
27
|
this.sessions[key] = entry;
|
|
28
28
|
}
|
|
@@ -37,13 +37,14 @@ class ChannelSessions {
|
|
|
37
37
|
async list() { await this.load(); return Object.values(this.sessions).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); }
|
|
38
38
|
async run(message, agentPlugin, call, control) {
|
|
39
39
|
const source = JSON.stringify([message.channel, message.recipientId || "", message.chatId || "", message.peerId, message.threadId || ""]);
|
|
40
|
-
const
|
|
41
|
-
const
|
|
40
|
+
const hasAttachments = Boolean(message.attachments?.length);
|
|
41
|
+
const command = hasAttachments ? undefined : message.text?.trim().match(/^\/new(?:[ \t]+([^\r\n]+))?$/);
|
|
42
|
+
const unsupported = hasAttachments ? undefined : message.text?.trim().match(unsupportedCommands);
|
|
42
43
|
if (unsupported)
|
|
43
44
|
return { text: `会话命令 ${unsupported[0].split(/[ \t]/, 1)[0]} 本期暂不支持` };
|
|
44
|
-
if (/^\/clear(?:[ \t]+[^\r\n]*)?$/.test(message.text?.trim() ?? ""))
|
|
45
|
+
if (!hasAttachments && /^\/clear(?:[ \t]+[^\r\n]*)?$/.test(message.text?.trim() ?? ""))
|
|
45
46
|
return { text: "/clear 已移除,请使用 /new 新建会话" };
|
|
46
|
-
if (message.text?.trim() === "/help")
|
|
47
|
+
if (!hasAttachments && message.text?.trim() === "/help")
|
|
47
48
|
return { text: [
|
|
48
49
|
control ? "/new [名称]:新建并切换会话" : "/new [名称]:当前 Agent 插件不支持新建会话",
|
|
49
50
|
"话题中不支持 /new;/renew 及其他会话命令当前暂不支持",
|
|
@@ -62,10 +63,10 @@ class ChannelSessions {
|
|
|
62
63
|
const sessionId = !command && entry?.agentPlugin === agentPlugin ? entry.sessionId : (0, node_crypto_1.randomUUID)();
|
|
63
64
|
const response = command
|
|
64
65
|
? await control({ operation: "new", sessionId, ...(command[1]?.trim() ? { name: command[1].trim() } : {}) })
|
|
65
|
-
: await call(sessionId
|
|
66
|
+
: await call(sessionId);
|
|
66
67
|
if (command && (typeof response.sessionId !== "string" || !response.sessionId.trim()))
|
|
67
68
|
throw new Error("插件未返回有效的新会话标识,原会话绑定保持不变");
|
|
68
|
-
const next = { source, agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString()
|
|
69
|
+
const next = { source, agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString() };
|
|
69
70
|
const save = this.writes.catch(() => { }).then(async () => {
|
|
70
71
|
const snapshot = { ...this.sessions, [source]: next };
|
|
71
72
|
await (0, promises_1.mkdir)(this.directory, { recursive: true });
|
|
@@ -5,7 +5,6 @@ exports.resolveLocalControlHost = resolveLocalControlHost;
|
|
|
5
5
|
exports.controlUrl = controlUrl;
|
|
6
6
|
exports.runPluginCommand = runPluginCommand;
|
|
7
7
|
exports.runCoreHelp = runCoreHelp;
|
|
8
|
-
const secret_input_1 = require("./secret-input");
|
|
9
8
|
const node_http_1 = require("node:http");
|
|
10
9
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
11
10
|
const PLUGIN_ID = /^[a-z][a-z0-9-]{1,63}$/;
|
|
@@ -109,7 +108,6 @@ function commandManifest(value, plugin) {
|
|
|
109
108
|
|| !Array.isArray(value.commands) || value.commands.some((command) => !object(command)
|
|
110
109
|
|| typeof command.name !== "string" || !/^[a-z][a-z0-9-]*$/.test(command.name)
|
|
111
110
|
|| typeof command.summary !== "string" || !command.summary.trim() || typeof command.usage !== "string" || !command.usage.trim()
|
|
112
|
-
|| (command.secretPrompt !== undefined && (typeof command.secretPrompt !== "string" || !command.secretPrompt.trim()))
|
|
113
111
|
|| (command.examples !== undefined && (!Array.isArray(command.examples) || command.examples.some((example) => typeof example !== "string"))))) {
|
|
114
112
|
throw new CliError("插件命令清单协议无效(需要 protocolVersion: 1)");
|
|
115
113
|
}
|
|
@@ -128,7 +126,7 @@ async function runPluginCommand(id, args, port, options = {}) {
|
|
|
128
126
|
throw new CliError(`插件“${id}”未安装;请先到 ABWeb 安装插件`, 2);
|
|
129
127
|
if (!plugin.capabilities.includes("cli"))
|
|
130
128
|
throw new CliError(`插件“${plugin.displayName}”未提供 CLI 命令;请升级对应插件`, 2);
|
|
131
|
-
if (
|
|
129
|
+
if (plugin.status !== "ready")
|
|
132
130
|
throw new CliError(`插件“${plugin.displayName}”未就绪(${plugin.status})`, 2);
|
|
133
131
|
const path = `/api/plugins/${id}/commands`;
|
|
134
132
|
const response = await requestJson(port, path, options);
|
|
@@ -148,9 +146,7 @@ async function runPluginCommand(id, args, port, options = {}) {
|
|
|
148
146
|
])].join("\n");
|
|
149
147
|
return output({ ok: true, message, data: { ...manifest, commands: selected } }, 0, json);
|
|
150
148
|
}
|
|
151
|
-
const
|
|
152
|
-
const secret = prompt ? await (options.readSecret ?? secret_input_1.readSecret)(prompt) : undefined;
|
|
153
|
-
const executed = await requestJson(port, path, options, { protocolVersion: 1, argv, ...(secret === undefined ? {} : { secret }) });
|
|
149
|
+
const executed = await requestJson(port, path, options, { protocolVersion: 1, argv });
|
|
154
150
|
const result = executed.value;
|
|
155
151
|
if (!object(result) || typeof result.ok !== "boolean" || typeof result.message !== "string"
|
|
156
152
|
|| (executed.status >= 200 && executed.status < 300 ? !result.ok : result.ok)
|
|
@@ -221,8 +221,6 @@ class ControlServer {
|
|
|
221
221
|
const result = await identity.create(body.aid, body.ownerAid);
|
|
222
222
|
return this.json(res, result.status === "等待 Owner 确认" ? 202 : result.status === "existing" ? 200 : 201, result);
|
|
223
223
|
}
|
|
224
|
-
if (method === "GET" && url.pathname === "/api/agents/availability")
|
|
225
|
-
return this.json(res, 200, { agents: await this.options.manager.availableAgents() });
|
|
226
224
|
if (method === "GET" && url.pathname === "/api/plugins")
|
|
227
225
|
return this.json(res, 200, { ok: true, plugins: this.options.manager.list() });
|
|
228
226
|
if (method === "GET" && url.pathname === "/api/plugins/catalog")
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isDaemonCommand = isDaemonCommand;
|
|
4
4
|
/** 只识别直接运行本安装 CLI 的后台进程,避免旧地址文件中的 PID 被其他程序复用。 */
|
|
5
5
|
function isDaemonCommand(commandLine, cli) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
return args[1] === normalize(cli) && ["start", "--serve"].includes(args[2]);
|
|
6
|
+
const args = [...commandLine.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => (match[1] ?? match[2]).replaceAll("\\", "/").toLowerCase());
|
|
7
|
+
return args[1] === cli.replaceAll("\\", "/").toLowerCase() && ["start", "--serve"].includes(args[2]);
|
|
9
8
|
}
|
package/dist/src-ts/dsh-agent.js
CHANGED
|
@@ -18,7 +18,7 @@ class DshAgent {
|
|
|
18
18
|
if (!request.task.trim())
|
|
19
19
|
throw new agent_1.AgentError("DSH task must not be empty");
|
|
20
20
|
const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
|
|
21
|
-
if (request.sessionId
|
|
21
|
+
if (request.sessionId)
|
|
22
22
|
args.push("--session-id", request.sessionId);
|
|
23
23
|
args.push("-");
|
|
24
24
|
const child = this.spawnProcess(this.paths.executable, args, {
|
|
@@ -29,7 +29,7 @@ class DshAgent {
|
|
|
29
29
|
child.stdin.end(request.task, "utf8");
|
|
30
30
|
let output = "";
|
|
31
31
|
let errors = "";
|
|
32
|
-
let sessionId = request.
|
|
32
|
+
let sessionId = request.sessionId;
|
|
33
33
|
let finalText = "";
|
|
34
34
|
child.stdout.setEncoding?.("utf8");
|
|
35
35
|
child.stderr.setEncoding?.("utf8");
|
|
@@ -64,8 +64,6 @@ class DshAgent {
|
|
|
64
64
|
});
|
|
65
65
|
if (!finalText)
|
|
66
66
|
throw new agent_1.AgentError("DSH returned no final text");
|
|
67
|
-
if (request.newSession && !sessionId)
|
|
68
|
-
throw new agent_1.AgentError("DSH 未返回真实新会话标识,无法确认后续续聊目标");
|
|
69
67
|
return { text: finalText, sessionId };
|
|
70
68
|
}
|
|
71
69
|
}
|
|
@@ -19,13 +19,13 @@ const plugin_catalog_1 = require("./plugin-catalog");
|
|
|
19
19
|
const log_store_1 = require("./log-store");
|
|
20
20
|
const channel_sessions_1 = require("./channel-sessions");
|
|
21
21
|
const _2020_1 = __importDefault(require("ajv/dist/2020"));
|
|
22
|
+
const attachments_1 = require("./attachments");
|
|
22
23
|
const logger = { info() { }, warn() { }, error() { } };
|
|
23
24
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
24
25
|
class PluginManager {
|
|
25
26
|
options;
|
|
26
27
|
controlUrl;
|
|
27
28
|
plugins = new Map();
|
|
28
|
-
upgrades = new Map();
|
|
29
29
|
httpRoutes = new Map();
|
|
30
30
|
root;
|
|
31
31
|
messages = [];
|
|
@@ -162,22 +162,6 @@ class PluginManager {
|
|
|
162
162
|
})]);
|
|
163
163
|
}
|
|
164
164
|
async upgradePackage(id, packageName = `agent-bridge-${id}`, version) {
|
|
165
|
-
const pending = this.upgrades.get(id);
|
|
166
|
-
if (pending) {
|
|
167
|
-
if (pending.packageName === packageName && pending.version === version)
|
|
168
|
-
return pending.task;
|
|
169
|
-
throw new agent_1.AgentError("插件正在升级,请等待完成后再请求其他版本");
|
|
170
|
-
}
|
|
171
|
-
const task = this.performUpgrade(id, packageName, version);
|
|
172
|
-
this.upgrades.set(id, { packageName, version, task });
|
|
173
|
-
try {
|
|
174
|
-
return await task;
|
|
175
|
-
}
|
|
176
|
-
finally {
|
|
177
|
-
this.upgrades.delete(id);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async performUpgrade(id, packageName, version) {
|
|
181
165
|
if (!/^agent-bridge-[a-z0-9-]+$/.test(packageName))
|
|
182
166
|
throw new agent_1.AgentError("仅支持 agent-bridge-* npm 包名");
|
|
183
167
|
if (version && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
@@ -378,11 +362,22 @@ class PluginManager {
|
|
|
378
362
|
this.recordMessage(target, "request", message.text, message.peerId);
|
|
379
363
|
try {
|
|
380
364
|
const instance = this.plugins.get(target)?.instance;
|
|
381
|
-
const control = instance?.sessionOperations?.includes("new")
|
|
365
|
+
const control = instance?.sessionOperations?.includes("new") && instance.controlSession
|
|
382
366
|
? (request) => this.controlSession(target, request)
|
|
383
367
|
: undefined;
|
|
384
|
-
const
|
|
385
|
-
await message.
|
|
368
|
+
const task = !message.text.trim() && message.attachments?.length ? "请处理收到的附件" : message.text;
|
|
369
|
+
const response = await this.sessions.run(message, target, (sessionId) => this.handle(target, { task, sessionId, ...(message.attachments ? { attachments: message.attachments } : {}) }), control);
|
|
370
|
+
const attachments = response.attachments?.length ? response.attachments : (0, attachments_1.asAttachments)(response.images);
|
|
371
|
+
(0, attachments_1.validateAttachments)(attachments);
|
|
372
|
+
if (attachments.length) {
|
|
373
|
+
if (!message.replyAttachments) {
|
|
374
|
+
await message.reply("当前通道不支持附件回传");
|
|
375
|
+
throw new agent_1.AgentError("当前通道不支持附件回传");
|
|
376
|
+
}
|
|
377
|
+
await message.replyAttachments(attachments, response.text);
|
|
378
|
+
}
|
|
379
|
+
else
|
|
380
|
+
await message.reply(response.text);
|
|
386
381
|
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
387
382
|
}
|
|
388
383
|
catch (error) {
|
|
@@ -545,17 +540,18 @@ class PluginManager {
|
|
|
545
540
|
const reason = loaded?.record.error ? `启动错误:${loaded.record.error}` : `当前状态:${loaded?.record.status ?? "未发现"}`;
|
|
546
541
|
throw new agent_1.AgentError(`插件“${name}”无法调用,插件未就绪:${processText}${reason}`);
|
|
547
542
|
}
|
|
543
|
+
(0, attachments_1.validateAttachments)(request.attachments);
|
|
544
|
+
if (request.attachments?.length && !loaded.instance.attachmentInput)
|
|
545
|
+
throw new agent_1.AgentError(`插件 ${id} 不支持接收附件`);
|
|
548
546
|
return loaded.instance.handle(request);
|
|
549
547
|
}
|
|
550
548
|
async controlSession(id, request) {
|
|
551
549
|
const loaded = this.plugins.get(id);
|
|
552
550
|
if (!loaded?.instance || loaded.record.status !== "ready")
|
|
553
551
|
throw new agent_1.AgentError(`插件 ${id} 未就绪,无法控制会话`);
|
|
554
|
-
if (!loaded.instance.sessionOperations?.includes(request.operation))
|
|
552
|
+
if (!loaded.instance.sessionOperations?.includes(request.operation) || !loaded.instance.controlSession)
|
|
555
553
|
throw new agent_1.AgentError(`插件 ${id} 不支持新建会话`);
|
|
556
|
-
const response = loaded.instance.controlSession
|
|
557
|
-
? await loaded.instance.controlSession(request)
|
|
558
|
-
: { text: `${loaded.record.displayName} 已创建新会话`, sessionId: request.sessionId };
|
|
554
|
+
const response = await loaded.instance.controlSession(request);
|
|
559
555
|
if (typeof response.sessionId !== "string" || !response.sessionId.trim())
|
|
560
556
|
throw new agent_1.AgentError("插件未返回有效的新会话标识,原会话绑定保持不变");
|
|
561
557
|
return response;
|
|
@@ -630,13 +626,10 @@ class PluginManager {
|
|
|
630
626
|
const packagedFile = (0, node_path_1.join)(record.path, record.manifest.entry);
|
|
631
627
|
const developmentFile = (0, node_path_1.resolve)(__dirname, "plugins", record.id, (0, node_path_1.basename)(record.manifest.entry));
|
|
632
628
|
const file = (0, node_fs_1.existsSync)(packagedFile) ? packagedFile : developmentFile;
|
|
633
|
-
//
|
|
629
|
+
// CommonJS is the platform's compiled format; evict the module cache for reload.
|
|
634
630
|
const req = (0, node_module_1.createRequire)(file);
|
|
635
631
|
const resolved = req.resolve(file);
|
|
636
|
-
|
|
637
|
-
for (const cached of Object.keys(req.cache))
|
|
638
|
-
if (cached === resolved || cached.startsWith(`${moduleRoot}${node_path_1.sep}`))
|
|
639
|
-
delete req.cache[cached];
|
|
632
|
+
delete req.cache[resolved];
|
|
640
633
|
const mod = req(resolved);
|
|
641
634
|
let value = mod.default ?? mod.plugin ?? mod;
|
|
642
635
|
// Entries may export a ready instance, a factory, or a class. Supporting all
|
|
@@ -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
|
+
}
|