mu-agent-bridge 1.0.33 → 1.0.35
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 +2 -0
- package/dist/src-ts/channel-sessions.js +3 -3
- package/dist/src-ts/dsh-agent.js +141 -46
- package/dist/src-ts/headless-account.patch.yml +8 -1
- package/dist/src-ts/plugin-manager.js +38 -8
- package/dist/src-ts/plugins/aun/attachments.js +44 -13
- package/dist/src-ts/plugins/aun/index.js +5 -6
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +270 -9
- package/dist/src-ts/plugins/chatgpt/index.js +6 -4
- package/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/dist/src-ts/plugins/doubao/attachments.js +42 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +302 -45
- package/dist/src-ts/plugins/doubao/index.js +37 -24
- package/dist/src-ts/plugins/dsh/index.js +3 -1
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +263 -10
- package/dist/src-ts/plugins/qwenwork/index.js +31 -3
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +139 -25
- package/dist/src-ts/plugins/workbuddy/index.js +43 -9
- package/dist/tests-ts/aun-attachments.test.js +41 -3
- package/dist/tests-ts/aun-plugin.test.js +28 -5
- package/dist/tests-ts/bundled-package.test.js +3 -1
- package/dist/tests-ts/channel-sessions.test.js +22 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
- package/dist/tests-ts/desktop-launch.test.js +41 -0
- package/dist/tests-ts/doubao-plugin.test.js +428 -2
- package/dist/tests-ts/dsh-agent.test.js +199 -0
- package/dist/tests-ts/plugin-manager.test.js +100 -1
- package/dist/tests-ts/qwenwork-images.test.js +166 -0
- package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
- package/dist/tests-ts/workbuddy-plugin.test.js +283 -9
- package/package.json +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +270 -9
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +6 -4
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +1 -1
- package/plugins/doubao/config.schema.json +3 -0
- package/plugins/doubao/dist/src-ts/attachments.js +62 -0
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +302 -45
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +37 -24
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -1
- package/plugins/dsh/dist/src-ts/attachments.js +62 -0
- package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
- package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +3 -1
- package/plugins/dsh/package.json +5 -2
- package/plugins/dsh/plugin.json +1 -1
- package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
- package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +263 -10
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +31 -3
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/config.schema.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +139 -25
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +43 -9
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +3 -3
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npm run start
|
|
|
14
14
|
|
|
15
15
|
默认监听 `http://127.0.0.1:26107`,管理页面为 `http://127.0.0.1:26107/abweb/`,管理 API 为 `/api/health`、`/api/plugins`,千问办公的
|
|
16
16
|
OpenAI 兼容 API 为 `/v1/models` 和 `/v1/chat/completions`。管理页面位于 `/abweb`。
|
|
17
|
+
千问办公支持通过 AUN 输入文件、图片和视频附件,并从本轮“已展示最终文件”收集实际输出附件。输入附件文件名会追加本轮唯一后缀并保留扩展名,以便失败时只清理桥接自己的附件。插件等待生成结束后再回传,输出保留文件名和 MIME 类型;桌面客户端不支持的媒体能力以实际回复为准。
|
|
18
|
+
|
|
17
19
|
千问办公插件使用 CDP `9228`;没有运行千问办公时,daemon 仍会启动,qwenwork 会显示为失败状态,
|
|
18
20
|
可以通过管理 API 重载。
|
|
19
21
|
|
|
@@ -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" || (entry.newSession !== undefined && typeof entry.newSession !== "boolean"))
|
|
26
26
|
throw new Error("通道会话记录无效");
|
|
27
27
|
this.sessions[key] = entry;
|
|
28
28
|
}
|
|
@@ -63,10 +63,10 @@ class ChannelSessions {
|
|
|
63
63
|
const sessionId = !command && entry?.agentPlugin === agentPlugin ? entry.sessionId : (0, node_crypto_1.randomUUID)();
|
|
64
64
|
const response = command
|
|
65
65
|
? await control({ operation: "new", sessionId, ...(command[1]?.trim() ? { name: command[1].trim() } : {}) })
|
|
66
|
-
: await call(sessionId);
|
|
66
|
+
: await call(sessionId, !entry || entry.agentPlugin !== agentPlugin || entry.newSession === true);
|
|
67
67
|
if (command && (typeof response.sessionId !== "string" || !response.sessionId.trim()))
|
|
68
68
|
throw new Error("插件未返回有效的新会话标识,原会话绑定保持不变");
|
|
69
|
-
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(), ...(command ? { newSession: true } : {}) };
|
|
70
70
|
const save = this.writes.catch(() => { }).then(async () => {
|
|
71
71
|
const snapshot = { ...this.sessions, [source]: next };
|
|
72
72
|
await (0, promises_1.mkdir)(this.directory, { recursive: true });
|
package/dist/src-ts/dsh-agent.js
CHANGED
|
@@ -2,8 +2,64 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DshAgent = void 0;
|
|
4
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const node_os_1 = require("node:os");
|
|
5
10
|
const agent_1 = require("./agent");
|
|
11
|
+
const attachments_1 = require("./attachments");
|
|
6
12
|
const dsh_paths_1 = require("./dsh-paths");
|
|
13
|
+
const outputMimeTypes = {
|
|
14
|
+
".csv": "text/csv", ".html": "text/html", ".json": "application/json", ".md": "text/markdown",
|
|
15
|
+
".pdf": "application/pdf", ".txt": "text/plain", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
16
|
+
".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp",
|
|
17
|
+
};
|
|
18
|
+
function digest(data) { return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex"); }
|
|
19
|
+
function outputContentType(filename, data) {
|
|
20
|
+
return (0, attachments_1.detectImageContentType)(data) ?? outputMimeTypes[(0, node_path_1.extname)(filename).toLowerCase()] ?? "application/octet-stream";
|
|
21
|
+
}
|
|
22
|
+
async function readPresentedFiles(directory, paths, cwd, inputs, returnPaths) {
|
|
23
|
+
if (paths.length > attachments_1.MAX_ATTACHMENTS)
|
|
24
|
+
throw new Error("单条消息最多包含 8 个附件");
|
|
25
|
+
const result = [];
|
|
26
|
+
const root = (0, node_path_1.resolve)(directory);
|
|
27
|
+
if (await (0, promises_1.realpath)(directory) !== root || !(await (0, promises_1.lstat)(directory)).isDirectory())
|
|
28
|
+
throw new Error("DSH 输出目录已被替换");
|
|
29
|
+
let total = 0;
|
|
30
|
+
for (const value of paths) {
|
|
31
|
+
const target = (0, node_path_1.resolve)(cwd, value);
|
|
32
|
+
if ((0, node_path_1.dirname)(target) !== root || (0, node_path_1.dirname)(await (0, promises_1.realpath)(target)) !== root)
|
|
33
|
+
throw new Error("DSH 回传附件路径超出输出目录");
|
|
34
|
+
if (!(await (0, promises_1.lstat)(target)).isFile())
|
|
35
|
+
throw new Error("DSH 回传附件不是普通文件");
|
|
36
|
+
const handle = await (0, promises_1.open)(target, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0));
|
|
37
|
+
let data;
|
|
38
|
+
try {
|
|
39
|
+
const info = await handle.stat();
|
|
40
|
+
if (!info.isFile() || info.size < 1 || info.size > attachments_1.MAX_ATTACHMENT_BYTES)
|
|
41
|
+
throw new Error("DSH 回传附件大小无效,单个附件不能超过 32 MiB");
|
|
42
|
+
total += info.size;
|
|
43
|
+
if (total > attachments_1.MAX_TOTAL_ATTACHMENT_BYTES)
|
|
44
|
+
throw new Error("单条消息附件总大小不能超过 64 MiB");
|
|
45
|
+
const buffer = Buffer.alloc(info.size + 1);
|
|
46
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
47
|
+
if (bytesRead !== info.size)
|
|
48
|
+
throw new Error("DSH 回传附件大小发生变化");
|
|
49
|
+
data = new Uint8Array(buffer.subarray(0, bytesRead));
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await handle.close();
|
|
53
|
+
}
|
|
54
|
+
const source = inputs[returnPaths.indexOf(target)];
|
|
55
|
+
const original = source && digest(source.data) === digest(data) ? source : inputs.find((input) => input.filename === (0, node_path_1.basename)(target) && digest(input.data) === digest(data));
|
|
56
|
+
const filename = (0, attachments_1.attachmentFilename)(original?.filename ?? (0, node_path_1.basename)(target));
|
|
57
|
+
const contentType = original?.contentType ?? outputContentType(filename, data);
|
|
58
|
+
result.push({ kind: contentType.startsWith("image/") ? "image" : "file", data, filename, contentType });
|
|
59
|
+
}
|
|
60
|
+
(0, attachments_1.validateAttachments)(result);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
7
63
|
class DshAgent {
|
|
8
64
|
name = "dsh";
|
|
9
65
|
paths;
|
|
@@ -17,54 +73,93 @@ class DshAgent {
|
|
|
17
73
|
async send(request) {
|
|
18
74
|
if (!request.task.trim())
|
|
19
75
|
throw new agent_1.AgentError("DSH task must not be empty");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
76
|
+
(0, attachments_1.validateAttachments)(request.attachments);
|
|
77
|
+
const workDirectory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-dsh-"));
|
|
78
|
+
let failed = false;
|
|
79
|
+
try {
|
|
80
|
+
const outputDirectory = (0, node_path_1.join)(workDirectory, "output");
|
|
81
|
+
await (0, promises_1.mkdir)(outputDirectory, { recursive: true });
|
|
82
|
+
for (const [index, item] of (request.attachments ?? []).entries())
|
|
83
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(workDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), item.data, { mode: 0o600 });
|
|
84
|
+
const inputPaths = (request.attachments ?? []).map((item, index) => ({ path: (0, node_path_1.join)(workDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), returnPath: (0, node_path_1.join)(outputDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), filename: item.filename, contentType: item.contentType }));
|
|
85
|
+
for (const [index, item] of (request.attachments ?? []).entries())
|
|
86
|
+
await (0, promises_1.writeFile)(inputPaths[index].returnPath, item.data, { mode: 0o600 });
|
|
87
|
+
const task = `${request.task}\n\n请使用真实文件工具读取输入附件;生成的回传文件必须写入 outputDirectory 的直接子文件,并调用 present 工具声明文件。原附件已准备 returnPath 副本,原样回传时直接 present 此路径,无需 shell 复制。图片或视频无法理解时请明确说明,仍可原样回传。\n附件传输:\n${JSON.stringify({ inputs: inputPaths, outputDirectory })}`;
|
|
88
|
+
const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
|
|
89
|
+
if (request.sessionId && !request.newSession)
|
|
90
|
+
args.push("--session-id", request.sessionId);
|
|
91
|
+
args.push("-");
|
|
92
|
+
const child = this.spawnProcess(this.paths.executable, args, {
|
|
93
|
+
cwd: request.cwd,
|
|
94
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
|
95
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
96
|
+
});
|
|
97
|
+
let output = "";
|
|
98
|
+
let errors = "";
|
|
99
|
+
let sessionId = request.newSession ? undefined : request.sessionId;
|
|
100
|
+
let sessionCwd = request.cwd ?? process.cwd();
|
|
101
|
+
let finalText = "";
|
|
102
|
+
const presented = new Set();
|
|
103
|
+
const presentCalls = new Map();
|
|
104
|
+
child.stdout.setEncoding?.("utf8");
|
|
105
|
+
child.stderr.setEncoding?.("utf8");
|
|
106
|
+
child.stdout.on("data", (chunk) => {
|
|
107
|
+
output += chunk;
|
|
108
|
+
for (const line of output.split(/\r?\n/).slice(0, -1)) {
|
|
109
|
+
try {
|
|
110
|
+
const event = JSON.parse(line);
|
|
111
|
+
if (event.type === "session" && event.sessionId)
|
|
112
|
+
sessionId = event.sessionId;
|
|
113
|
+
if (event.type === "session" && event.cwd)
|
|
114
|
+
sessionCwd = event.cwd;
|
|
115
|
+
if (event.type === "final" && event.text)
|
|
116
|
+
finalText = event.text.trim();
|
|
117
|
+
if (event.type === "tool_call" && event.tool === "present" && event.callId)
|
|
118
|
+
presentCalls.set(event.callId, (event.input?.files ?? []).map((file) => file.path).filter((path) => typeof path === "string"));
|
|
119
|
+
if (event.type === "tool_result" && event.status === "completed" && event.callId)
|
|
120
|
+
for (const path of presentCalls.get(event.callId) ?? [])
|
|
121
|
+
presented.add(path);
|
|
122
|
+
}
|
|
123
|
+
catch { /* 忽略诊断输出和未完成的 JSON 行。 */ }
|
|
45
124
|
}
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
reject(new agent_1.AgentError(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
125
|
+
output = output.split(/\r?\n/).at(-1) ?? "";
|
|
126
|
+
});
|
|
127
|
+
child.stderr.on("data", (chunk) => { errors += chunk; });
|
|
128
|
+
await new Promise((resolve, reject) => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
child.kill();
|
|
131
|
+
reject(new agent_1.AgentError(`DSH task timed out after ${request.timeoutMs ?? this.defaultTimeoutMs}ms`));
|
|
132
|
+
}, request.timeoutMs ?? this.defaultTimeoutMs);
|
|
133
|
+
child.once("error", (error) => { clearTimeout(timer); reject(new agent_1.AgentError("failed to start DSH", { cause: error })); });
|
|
134
|
+
child.once("close", (code) => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
if (code !== 0 && !finalText)
|
|
137
|
+
reject(new agent_1.AgentError(errors.trim() || `DSH exited with code ${code}`));
|
|
138
|
+
else
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
141
|
+
child.stdin.end(task, "utf8");
|
|
63
142
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
143
|
+
if (request.newSession && !sessionId)
|
|
144
|
+
throw new agent_1.AgentError("DSH 未返回真实新会话标识,无法确认后续续聊目标");
|
|
145
|
+
const attachments = await readPresentedFiles(outputDirectory, [...presented], sessionCwd, request.attachments ?? [], inputPaths.map((item) => item.returnPath));
|
|
146
|
+
if (!finalText && !attachments.length)
|
|
147
|
+
throw new agent_1.AgentError("DSH 未返回正文或附件");
|
|
148
|
+
return { text: finalText, sessionId, ...(attachments?.length ? { attachments } : {}) };
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
failed = true;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
try {
|
|
156
|
+
await (0, promises_1.rm)(workDirectory, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (!failed)
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
68
163
|
}
|
|
69
164
|
}
|
|
70
165
|
exports.DshAgent = DshAgent;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 复用 DeepSeek Harness 桌面端登录账号的模型。
|
|
2
2
|
- id: agent-default-model
|
|
3
3
|
config:
|
|
4
4
|
provider: deepseek-account
|
|
5
5
|
model: deepseek-chat
|
|
6
|
+
|
|
7
|
+
# headless 默认未挂载交付工具,显式启用真实文件交付事件。
|
|
8
|
+
- insert:
|
|
9
|
+
- id: tool-present
|
|
10
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
11
|
+
config:
|
|
12
|
+
maxFiles: 8
|
|
@@ -26,6 +26,7 @@ class PluginManager {
|
|
|
26
26
|
options;
|
|
27
27
|
controlUrl;
|
|
28
28
|
plugins = new Map();
|
|
29
|
+
upgrades = new Map();
|
|
29
30
|
httpRoutes = new Map();
|
|
30
31
|
root;
|
|
31
32
|
messages = [];
|
|
@@ -162,6 +163,22 @@ class PluginManager {
|
|
|
162
163
|
})]);
|
|
163
164
|
}
|
|
164
165
|
async upgradePackage(id, packageName = `agent-bridge-${id}`, version) {
|
|
166
|
+
const pending = this.upgrades.get(id);
|
|
167
|
+
if (pending) {
|
|
168
|
+
if (pending.packageName === packageName && pending.version === version)
|
|
169
|
+
return pending.task;
|
|
170
|
+
throw new agent_1.AgentError("插件正在升级,请等待完成后再请求其他版本");
|
|
171
|
+
}
|
|
172
|
+
const task = this.performUpgrade(id, packageName, version);
|
|
173
|
+
this.upgrades.set(id, { packageName, version, task });
|
|
174
|
+
try {
|
|
175
|
+
return await task;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
this.upgrades.delete(id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async performUpgrade(id, packageName, version) {
|
|
165
182
|
if (!/^agent-bridge-[a-z0-9-]+$/.test(packageName))
|
|
166
183
|
throw new agent_1.AgentError("仅支持 agent-bridge-* npm 包名");
|
|
167
184
|
if (version && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
@@ -362,11 +379,19 @@ class PluginManager {
|
|
|
362
379
|
this.recordMessage(target, "request", message.text, message.peerId);
|
|
363
380
|
try {
|
|
364
381
|
const instance = this.plugins.get(target)?.instance;
|
|
365
|
-
const control = instance?.sessionOperations?.includes("new")
|
|
382
|
+
const control = instance?.sessionOperations?.includes("new")
|
|
366
383
|
? (request) => this.controlSession(target, request)
|
|
367
384
|
: undefined;
|
|
368
385
|
const task = !message.text.trim() && message.attachments?.length ? "请处理收到的附件" : message.text;
|
|
369
|
-
const
|
|
386
|
+
const forwarded = new Set();
|
|
387
|
+
const onMessage = async (text) => {
|
|
388
|
+
if (!text.trim() || forwarded.has(text))
|
|
389
|
+
return;
|
|
390
|
+
await message.reply(text);
|
|
391
|
+
forwarded.add(text);
|
|
392
|
+
this.recordMessage(target, "reply", text, message.peerId);
|
|
393
|
+
};
|
|
394
|
+
const response = await this.sessions.run(message, target, (sessionId, newSession) => this.handle(target, { task, sessionId, onMessage, ...(newSession ? { newSession: true } : {}), ...(message.attachments ? { attachments: message.attachments } : {}) }), control);
|
|
370
395
|
const attachments = response.attachments?.length ? response.attachments : (0, attachments_1.asAttachments)(response.images);
|
|
371
396
|
(0, attachments_1.validateAttachments)(attachments);
|
|
372
397
|
if (attachments.length) {
|
|
@@ -375,10 +400,10 @@ class PluginManager {
|
|
|
375
400
|
throw new agent_1.AgentError("当前通道不支持附件回传");
|
|
376
401
|
}
|
|
377
402
|
await message.replyAttachments(attachments, response.text);
|
|
403
|
+
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
378
404
|
}
|
|
379
405
|
else
|
|
380
|
-
await
|
|
381
|
-
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
406
|
+
await onMessage(response.text);
|
|
382
407
|
}
|
|
383
408
|
catch (error) {
|
|
384
409
|
this.recordMessage(target, "error", error instanceof Error ? error.message : String(error), message.peerId);
|
|
@@ -549,9 +574,11 @@ class PluginManager {
|
|
|
549
574
|
const loaded = this.plugins.get(id);
|
|
550
575
|
if (!loaded?.instance || loaded.record.status !== "ready")
|
|
551
576
|
throw new agent_1.AgentError(`插件 ${id} 未就绪,无法控制会话`);
|
|
552
|
-
if (!loaded.instance.sessionOperations?.includes(request.operation)
|
|
577
|
+
if (!loaded.instance.sessionOperations?.includes(request.operation))
|
|
553
578
|
throw new agent_1.AgentError(`插件 ${id} 不支持新建会话`);
|
|
554
|
-
const response =
|
|
579
|
+
const response = loaded.instance.controlSession
|
|
580
|
+
? await loaded.instance.controlSession(request)
|
|
581
|
+
: { text: `${loaded.record.displayName} 已创建新会话`, sessionId: request.sessionId };
|
|
555
582
|
if (typeof response.sessionId !== "string" || !response.sessionId.trim())
|
|
556
583
|
throw new agent_1.AgentError("插件未返回有效的新会话标识,原会话绑定保持不变");
|
|
557
584
|
return response;
|
|
@@ -626,10 +653,13 @@ class PluginManager {
|
|
|
626
653
|
const packagedFile = (0, node_path_1.join)(record.path, record.manifest.entry);
|
|
627
654
|
const developmentFile = (0, node_path_1.resolve)(__dirname, "plugins", record.id, (0, node_path_1.basename)(record.manifest.entry));
|
|
628
655
|
const file = (0, node_fs_1.existsSync)(packagedFile) ? packagedFile : developmentFile;
|
|
629
|
-
//
|
|
656
|
+
// 同时刷新插件内部依赖,避免升级后仍运行旧版 CDP 客户端。
|
|
630
657
|
const req = (0, node_module_1.createRequire)(file);
|
|
631
658
|
const resolved = req.resolve(file);
|
|
632
|
-
|
|
659
|
+
const moduleRoot = (0, node_fs_1.existsSync)(packagedFile) ? (0, node_path_1.resolve)(record.path) : (0, node_path_1.dirname)(file);
|
|
660
|
+
for (const cached of Object.keys(req.cache))
|
|
661
|
+
if (cached === resolved || cached.startsWith(`${moduleRoot}${node_path_1.sep}`))
|
|
662
|
+
delete req.cache[cached];
|
|
633
663
|
const mod = req(resolved);
|
|
634
664
|
let value = mod.default ?? mod.plugin ?? mod;
|
|
635
665
|
// Entries may export a ready instance, a factory, or a class. Supporting all
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AunDownloadUrlError = void 0;
|
|
3
4
|
exports.parseAunAttachment = parseAunAttachment;
|
|
4
5
|
exports.downloadAunAttachments = downloadAunAttachments;
|
|
5
6
|
exports.downloadAunAttachment = downloadAunAttachment;
|
|
6
7
|
exports.uploadAunAttachments = uploadAunAttachments;
|
|
7
8
|
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_util_1 = require("node:util");
|
|
8
10
|
const attachments_1 = require("../../attachments");
|
|
9
11
|
const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
|
|
10
12
|
const mimePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
|
|
@@ -12,6 +14,21 @@ const imageTypes = new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
|
12
14
|
function digest(data) { return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex"); }
|
|
13
15
|
function validKey(key) { return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(key) && !key.split("/").some((part) => part === "." || part === ".."); }
|
|
14
16
|
function validHash(value) { return /^[a-f0-9]{64}$/i.test(value); }
|
|
17
|
+
function normalizedMime(value) {
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
return;
|
|
20
|
+
if (typeof value !== "string" || /[\u0000-\u001f\u007f]/.test(value))
|
|
21
|
+
throw new Error("AUN 附件 MIME 类型无效");
|
|
22
|
+
if (!value.trim())
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
const type = new node_util_1.MIMEType(value).essence;
|
|
26
|
+
if (mimePattern.test(type))
|
|
27
|
+
return type;
|
|
28
|
+
}
|
|
29
|
+
catch { /* 统一报告无效类型。 */ }
|
|
30
|
+
throw new Error("AUN 附件 MIME 类型无效");
|
|
31
|
+
}
|
|
15
32
|
function validateReference(ref) {
|
|
16
33
|
if (!aidPattern.test(ref.ownerAid) || !/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/.test(ref.bucket) || !validKey(ref.objectKey))
|
|
17
34
|
throw new Error("AUN 附件引用路径无效");
|
|
@@ -43,11 +60,9 @@ function parseAunAttachment(value) {
|
|
|
43
60
|
const bucket = item.bucket === undefined ? "default" : typeof item.bucket === "string" ? item.bucket : "";
|
|
44
61
|
const filename = (0, attachments_1.attachmentFilename)(typeof item.filename === "string" ? item.filename : objectKey);
|
|
45
62
|
const ref = { ownerAid, bucket, objectKey, filename };
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
ref.contentType = item.content_type.toLowerCase();
|
|
50
|
-
}
|
|
63
|
+
const contentType = normalizedMime(item.content_type);
|
|
64
|
+
if (contentType)
|
|
65
|
+
ref.contentType = contentType;
|
|
51
66
|
if (item.size_bytes !== undefined) {
|
|
52
67
|
if (typeof item.size_bytes !== "number")
|
|
53
68
|
throw new Error("AUN 附件大小无效");
|
|
@@ -81,10 +96,8 @@ function checkedMetadata(meta, ref) {
|
|
|
81
96
|
if ([meta.owner, meta.owner_aid].some((owner) => owner && owner !== ref.ownerAid) || meta.bucket && meta.bucket !== ref.bucket
|
|
82
97
|
|| [meta.path, meta.object_key].some((path) => path && path !== `/${ref.objectKey}` && path !== ref.objectKey))
|
|
83
98
|
throw new Error("AUN 附件元数据引用不一致");
|
|
84
|
-
const rawType = metadataValue(meta, "contentType", "content_type");
|
|
85
|
-
|
|
86
|
-
throw new Error("AUN 附件元数据 MIME 类型无效");
|
|
87
|
-
const contentType = typeof rawType === "string" && rawType ? rawType.toLowerCase() : ref.contentType ?? "application/octet-stream";
|
|
99
|
+
const rawType = normalizedMime(metadataValue(meta, "contentType", "content_type"));
|
|
100
|
+
const contentType = rawType ?? ref.contentType ?? "application/octet-stream";
|
|
88
101
|
if (ref.contentType && rawType && ref.contentType.toLowerCase() !== contentType)
|
|
89
102
|
throw new Error("AUN 附件 MIME 类型校验失败");
|
|
90
103
|
const sha256 = typeof meta.sha256 === "string" && meta.sha256 ? meta.sha256.toLowerCase() : undefined;
|
|
@@ -162,9 +175,20 @@ async function downloadAunAttachments(client, refs) {
|
|
|
162
175
|
(0, attachments_1.validateAttachments)(result);
|
|
163
176
|
return result;
|
|
164
177
|
}
|
|
178
|
+
class AunDownloadUrlError extends Error {
|
|
179
|
+
diagnostic;
|
|
180
|
+
constructor(url, reason) {
|
|
181
|
+
super("AUN 下载地址无效");
|
|
182
|
+
this.name = "AunDownloadUrlError";
|
|
183
|
+
this.diagnostic = { protocol: url.protocol, hostname: url.hostname, port: url.port, reason };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.AunDownloadUrlError = AunDownloadUrlError;
|
|
165
187
|
function checkedDownloadUrl(url) {
|
|
166
|
-
|
|
167
|
-
|
|
188
|
+
const storagePort = url.port === "9470" && /^storage\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i.test(url.hostname);
|
|
189
|
+
const reason = url.protocol !== "https:" ? "协议" : url.username || url.password ? "凭据" : url.port && !storagePort ? "端口" : 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(":") ? "主机名" : undefined;
|
|
190
|
+
if (reason)
|
|
191
|
+
throw new AunDownloadUrlError(url, reason);
|
|
168
192
|
return url;
|
|
169
193
|
}
|
|
170
194
|
async function readDownloadTicket(value, expectedSize) {
|
|
@@ -224,9 +248,16 @@ async function uploadAunAttachments(client, ownerAid, peerAid, items) {
|
|
|
224
248
|
const filename = (0, attachments_1.attachmentFilename)(item.filename);
|
|
225
249
|
const objectKey = `shared/${(0, node_crypto_1.randomUUID)()}/${filename.replace(/[^A-Za-z0-9._-]/g, "_")}`;
|
|
226
250
|
const sha256 = digest(item.data);
|
|
227
|
-
const
|
|
228
|
-
|
|
251
|
+
const contentType = item.contentType.toLowerCase();
|
|
252
|
+
// 视频播放器不携带 AID 凭据,沿用 EvolCore 的标准可读对象上传方式。
|
|
253
|
+
const video = contentType.startsWith("video/");
|
|
254
|
+
const ref = { ownerAid, bucket: "default", objectKey, filename, contentType, sizeBytes: item.data.byteLength, sha256 };
|
|
255
|
+
const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType, public: video, overwrite: video });
|
|
229
256
|
checkedMetadata({ ...uploaded, size: uploaded.size ?? uploaded.size_bytes ?? item.data.byteLength }, ref);
|
|
257
|
+
if (video) {
|
|
258
|
+
result.push({ owner_aid: ownerAid, object_key: objectKey, filename, content_type: contentType, size_bytes: item.data.byteLength, sha256 });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
230
261
|
const share = await client.storage.lowlevel.createShareLink({ owner: ownerAid, bucket: "default", objectKey, allowedAids: [peerAid], expireInSeconds: 86400 });
|
|
231
262
|
if (share.owner_aid && share.owner_aid !== ownerAid || share.bucket && share.bucket !== "default" || share.object_key && share.object_key !== objectKey)
|
|
232
263
|
throw new Error("AUN 附件分享对象不一致");
|
|
@@ -11,7 +11,7 @@ const node_url_1 = require("node:url");
|
|
|
11
11
|
const messages_1 = require("./messages");
|
|
12
12
|
const attachments_1 = require("./attachments");
|
|
13
13
|
const manifest = {
|
|
14
|
-
id: "aun", version: "1.0.
|
|
14
|
+
id: "aun", version: "1.0.30", type: "channel", displayName: "AUN 通道",
|
|
15
15
|
entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
|
|
16
16
|
capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
|
|
17
17
|
configSchema: "config.schema.json",
|
|
@@ -783,7 +783,7 @@ class AunPlugin {
|
|
|
783
783
|
}
|
|
784
784
|
}
|
|
785
785
|
catch (error) {
|
|
786
|
-
this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error) });
|
|
786
|
+
this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error), ...(error instanceof attachments_1.AunDownloadUrlError ? { downloadUrl: error.diagnostic } : {}) });
|
|
787
787
|
try {
|
|
788
788
|
await this.sendText(aid, message.peerId, `附件接收失败:${safeError(error)}`, message.threadId);
|
|
789
789
|
}
|
|
@@ -817,15 +817,14 @@ class AunPlugin {
|
|
|
817
817
|
}
|
|
818
818
|
const groups = new Map();
|
|
819
819
|
for (const [index, item] of items.entries()) {
|
|
820
|
-
const kind = item.kind;
|
|
821
|
-
const ref = refs[index];
|
|
820
|
+
const kind = item.kind === "file" && item.contentType.toLowerCase().startsWith("video/") ? "video" : item.kind;
|
|
822
821
|
const group = groups.get(kind) ?? [];
|
|
823
|
-
group.push(
|
|
822
|
+
group.push(refs[index]);
|
|
824
823
|
groups.set(kind, group);
|
|
825
824
|
}
|
|
826
825
|
groupsCount = groups.size;
|
|
827
826
|
for (const [kind, group] of groups) {
|
|
828
|
-
const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : "文件已生成。"), attachments: group };
|
|
827
|
+
const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : kind === "video" ? "视频已生成。" : "文件已生成。"), attachments: group };
|
|
829
828
|
if (message.threadId)
|
|
830
829
|
payload.thread_id = message.threadId;
|
|
831
830
|
if (message.messageId)
|