agent-bridge-aun 1.0.0 → 1.0.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 +2 -0
- package/config.schema.json +3 -0
- package/dist/src-ts/local-aids.js +26 -0
- package/dist/src-ts/plugins/aun/evolcore.js +54 -0
- package/dist/src-ts/plugins/aun/index.js +252 -50
- package/dist/src-ts/plugins/aun/public/app.js +66 -0
- package/dist/src-ts/plugins/aun/public/index.html +14 -0
- package/dist/src-ts/plugins/aun/public/style.css +21 -0
- package/package.json +1 -1
- package/plugin.json +3 -2
package/README.md
CHANGED
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
安装后在 ABWeb 的插件管理中启动 AUN 通道。配置 `aid` 和可选的 `agent_plugin`;默认 Agent 插件为 `qwenwork`。AID 创建接口为 `POST /api/plugins/aun/aid`,请求内容为 `{"aid":"新AID.agentid.cn","owner_aid":"所有者AID.agentid.cn"}`。
|
|
6
6
|
|
|
7
7
|
该插件依赖 `@agentunion/fastaun`,要求 Node.js 22.16 或更高版本。
|
|
8
|
+
|
|
9
|
+
ABWeb 的 AUN 设置仅展示已连接 AID。列表先显示已关联 Agent 的身份,同类中本工程身份在 EvolCore 身份前面;各组按最近成功关联的时间倒序排列,时间相同或旧记录没有时间时按 AID 名称排序。关联时间保存在插件配置的 `binding_times` 中,刷新或重启后保留;旧记录不补造时间,再次关联成功后开始记录。
|
package/config.schema.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "object",
|
|
3
3
|
"properties": {
|
|
4
|
+
"identities": { "type": "array", "items": { "type": "string" } },
|
|
5
|
+
"bindings": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
6
|
+
"binding_times": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
4
7
|
"aid": { "type": "string", "minLength": 1 },
|
|
5
8
|
"owner_aid": { "type": "string", "minLength": 1 },
|
|
6
9
|
"agent_plugin": { "type": "string", "minLength": 1, "default": "qwenwork" },
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listExistingAids = listExistingAids;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
async function listExistingAids() {
|
|
8
|
+
const root = (0, node_path_1.resolve)(process.env.EVOLCORE_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".evolcore"), "AIDs");
|
|
9
|
+
let names;
|
|
10
|
+
try {
|
|
11
|
+
names = await (0, promises_1.readdir)(root);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const available = await Promise.all(names.filter((name) => /^[a-zA-Z0-9.-]+$/.test(name)).map(async (name) => {
|
|
17
|
+
try {
|
|
18
|
+
await (0, promises_1.access)((0, node_path_1.join)(root, name, "private", "key.json"));
|
|
19
|
+
return name;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}));
|
|
25
|
+
return available.filter((name) => Boolean(name)).sort();
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runEc = runEc;
|
|
4
|
+
exports.configureAidForPlugin = configureAidForPlugin;
|
|
5
|
+
exports.listConnectedAids = listConnectedAids;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_util_1 = require("node:util");
|
|
8
|
+
const local_aids_1 = require("../../local-aids");
|
|
9
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
10
|
+
async function runEc(args) {
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await execFileAsync("ec", args, { shell: process.platform === "win32", timeout: 15_000, maxBuffer: 1024 * 1024 });
|
|
13
|
+
return JSON.parse(stdout);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const stdout = error.stdout;
|
|
17
|
+
if (stdout) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(stdout);
|
|
20
|
+
}
|
|
21
|
+
catch { /* 保留原始命令错误 */ }
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function configureAidForPlugin(pluginId, aid, port, run = runEc) {
|
|
27
|
+
if (!aid)
|
|
28
|
+
return;
|
|
29
|
+
if (!/^[a-z0-9_-]+$/.test(pluginId))
|
|
30
|
+
throw new Error("插件 ID 格式无效");
|
|
31
|
+
const path = pluginId === "dsh" ? "/dsh/v1" : pluginId === "qwenwork" ? "/v1" : `/api/agents/${pluginId}/v1`;
|
|
32
|
+
for (const [key, value] of [
|
|
33
|
+
["baseagents.ecagent.baseUrl", `http://127.0.0.1:${port}${path}`],
|
|
34
|
+
["baseagents.ecagent.apiKey", "mab-local"],
|
|
35
|
+
["baseagents.ecagent.model", pluginId],
|
|
36
|
+
["active_baseagent", "ecagent"],
|
|
37
|
+
]) {
|
|
38
|
+
const result = await run(["agent", "set", aid, key, value, "--format", "json"]);
|
|
39
|
+
if (!result?.ok)
|
|
40
|
+
throw new Error(`配置 AID ${aid} 失败:${key};${result?.code ?? ""} ${result?.error ?? "未知错误"}`);
|
|
41
|
+
}
|
|
42
|
+
const result = await run(["agent", "reload", aid, "--format", "json"]);
|
|
43
|
+
if (!result?.ok)
|
|
44
|
+
throw new Error(`重载 AID ${aid} 失败:${result?.code ?? ""} ${result?.error ?? "未知错误"}`);
|
|
45
|
+
}
|
|
46
|
+
async function listConnectedAids(run = runEc) {
|
|
47
|
+
const result = await run(["agent", "list", "--format", "json"]);
|
|
48
|
+
if (!result.ok || !Array.isArray(result.agents))
|
|
49
|
+
throw new Error("读取 EvolCore Agent 列表失败");
|
|
50
|
+
const candidates = result.agents.filter((agent) => agent.status === "running" && typeof agent.aid === "string" && /^[a-zA-Z0-9.-]+$/.test(agent.aid) && agent.channels?.some((channel) => channel.startsWith("aun#")));
|
|
51
|
+
const details = await Promise.all(candidates.map((agent) => run(["agent", "show", agent.aid, "--format", "json"]).catch(() => null)));
|
|
52
|
+
const local = new Set(await (0, local_aids_1.listExistingAids)());
|
|
53
|
+
return details.filter((detail) => detail?.ok && detail.status === "running" && detail.connection?.status === "connected" && local.has(detail.aid)).map((detail) => detail.aid).sort();
|
|
54
|
+
}
|
|
@@ -6,11 +6,14 @@ const node_module_1 = require("node:module");
|
|
|
6
6
|
const node_path_1 = require("node:path");
|
|
7
7
|
const node_url_1 = require("node:url");
|
|
8
8
|
const messages_1 = require("./messages");
|
|
9
|
+
const evolcore_1 = require("./evolcore");
|
|
10
|
+
const local_aids_1 = require("../../local-aids");
|
|
9
11
|
const manifest = {
|
|
10
|
-
id: "aun", version: "1.0.
|
|
12
|
+
id: "aun", version: "1.0.3", type: "channel", displayName: "AUN 通道",
|
|
11
13
|
entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
|
|
12
14
|
capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health"],
|
|
13
15
|
configSchema: "config.schema.json",
|
|
16
|
+
managementPage: "/abweb/channels/aun",
|
|
14
17
|
};
|
|
15
18
|
async function loadSdk() {
|
|
16
19
|
const resolver = (0, node_module_1.createRequire)(__filename);
|
|
@@ -28,23 +31,152 @@ async function loadSdk() {
|
|
|
28
31
|
}
|
|
29
32
|
const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
|
|
30
33
|
class AunPlugin {
|
|
34
|
+
options;
|
|
31
35
|
manifest = manifest;
|
|
32
36
|
context;
|
|
33
37
|
sdk;
|
|
34
38
|
store;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
clients = new Map();
|
|
40
|
+
identities = new Set();
|
|
41
|
+
legacyCreatedAids = new Set();
|
|
42
|
+
bindings = {};
|
|
43
|
+
bindingTimes = {};
|
|
44
|
+
creating = false;
|
|
45
|
+
unregister = [];
|
|
38
46
|
ingress;
|
|
39
47
|
aid = "";
|
|
40
48
|
config = {};
|
|
41
49
|
status = "degraded";
|
|
42
50
|
healthMessage = "尚未配置 AID";
|
|
43
51
|
seen = new messages_1.DuplicateTracker();
|
|
44
|
-
constructor(options = {}) {
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
this.options = options;
|
|
54
|
+
this.sdk = options.sdk;
|
|
55
|
+
this.ingress = options.ingress;
|
|
56
|
+
}
|
|
57
|
+
identity = {
|
|
58
|
+
list: async () => {
|
|
59
|
+
const connected = await (this.options.connectedAids ?? evolcore_1.listConnectedAids)().catch((error) => {
|
|
60
|
+
this.context?.logger.warn("查询 EvolCore 已连接身份失败", { error: safeError(error) });
|
|
61
|
+
return [];
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
evolcore: connected.filter((aid) => !this.identities.has(aid) && !this.legacyCreatedAids.has(aid)),
|
|
65
|
+
muAgentBridge: [...new Set([...this.clients.keys(), ...connected.filter((aid) => this.legacyCreatedAids.has(aid))])].sort(),
|
|
66
|
+
localMuAgentBridge: [...this.identities].filter((aid) => !this.clients.has(aid)).sort(),
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
create: async (aid, owner) => {
|
|
70
|
+
if (!aidPattern.test(aid) || !aidPattern.test(owner))
|
|
71
|
+
throw new Error("需要有效的 AID 和 Owner AID");
|
|
72
|
+
if (this.creating)
|
|
73
|
+
throw new Error("正在创建身份,请稍后再试");
|
|
74
|
+
this.creating = true;
|
|
75
|
+
try {
|
|
76
|
+
if (!this.identities.has(aid)) {
|
|
77
|
+
const result = await this.store.register({ aid, type: "agent", owner });
|
|
78
|
+
if (!result.ok)
|
|
79
|
+
throw new Error(redact(result.error?.message ?? result.error?.code ?? "AID 注册失败"));
|
|
80
|
+
const ownership = result.data?.ownership_status ?? result.data?.state ?? result.data?.operation_state;
|
|
81
|
+
if (ownership && ownership !== "active")
|
|
82
|
+
return { aid, status: "等待 Owner 确认", operation_id: result.data?.operation_id };
|
|
83
|
+
const loaded = this.store.load(aid);
|
|
84
|
+
if (!loaded.ok || !loaded.data)
|
|
85
|
+
throw new Error(loaded.error?.message ?? "注册后未能加载本地身份");
|
|
86
|
+
this.identities.add(aid);
|
|
87
|
+
this.config.aid = this.aid = aid;
|
|
88
|
+
this.config.owner_aid = owner;
|
|
89
|
+
this.config.aun_path = this.path();
|
|
90
|
+
await this.persist();
|
|
91
|
+
this.context?.logger.info("AUN 身份已创建", { aid, owner });
|
|
92
|
+
}
|
|
93
|
+
if (!this.clients.has(aid))
|
|
94
|
+
await this.connectAid(aid);
|
|
95
|
+
return { aid, status: "已创建", connection: this.clients.has(aid) ? "ready" : "degraded" };
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
this.creating = false;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
bind: async (pluginId, aid, port) => {
|
|
102
|
+
if (pluginId === "aun" || pluginId === "abweb")
|
|
103
|
+
throw new Error("只能关联 Agent 插件");
|
|
104
|
+
const previous = this.bindings;
|
|
105
|
+
const previousTimes = this.bindingTimes;
|
|
106
|
+
this.bindings = { ...previous };
|
|
107
|
+
this.bindingTimes = { ...previousTimes };
|
|
108
|
+
try {
|
|
109
|
+
if (aid) {
|
|
110
|
+
if (!aidPattern.test(aid))
|
|
111
|
+
throw new Error("AID 格式无效");
|
|
112
|
+
if (this.context?.pluginStatus(pluginId) !== "ready")
|
|
113
|
+
throw new Error("目标 Agent 插件未启动");
|
|
114
|
+
if (this.identities.has(aid)) {
|
|
115
|
+
if (!this.clients.has(aid))
|
|
116
|
+
await this.connectAid(aid);
|
|
117
|
+
if (!this.clients.has(aid))
|
|
118
|
+
throw new Error("AID 当前未连接");
|
|
119
|
+
for (const [identity, target] of Object.entries(this.bindings))
|
|
120
|
+
if (target === pluginId) {
|
|
121
|
+
delete this.bindings[identity];
|
|
122
|
+
delete this.bindingTimes[identity];
|
|
123
|
+
}
|
|
124
|
+
this.bindings[aid] = pluginId;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
if (!(await (0, evolcore_1.listConnectedAids)()).includes(aid))
|
|
128
|
+
throw new Error("AID 当前未连接");
|
|
129
|
+
await (0, evolcore_1.configureAidForPlugin)(pluginId, aid, port);
|
|
130
|
+
for (const [identity, target] of Object.entries(this.bindings))
|
|
131
|
+
if (target === pluginId) {
|
|
132
|
+
delete this.bindings[identity];
|
|
133
|
+
delete this.bindingTimes[identity];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
for (const [identity, target] of Object.entries(this.bindings))
|
|
139
|
+
if (target === pluginId) {
|
|
140
|
+
delete this.bindings[identity];
|
|
141
|
+
delete this.bindingTimes[identity];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (aid)
|
|
145
|
+
this.bindingTimes[aid] = new Date().toISOString();
|
|
146
|
+
await this.persist();
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.bindings = previous;
|
|
150
|
+
this.bindingTimes = previousTimes;
|
|
151
|
+
this.config.bindings = previous;
|
|
152
|
+
this.config.binding_times = previousTimes;
|
|
153
|
+
this.context?.logger.error("AUN 身份关联失败", { aid, pluginId, error: safeError(error) });
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
this.context?.logger.info("AUN 身份关联已更新", { aid, pluginId });
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
async persist() {
|
|
160
|
+
this.config.identities = [...this.identities].sort();
|
|
161
|
+
this.config.bindings = this.bindings;
|
|
162
|
+
this.config.binding_times = this.bindingTimes;
|
|
163
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 });
|
|
164
|
+
}
|
|
45
165
|
async activate(context) {
|
|
46
166
|
this.context = context;
|
|
47
167
|
this.config = { ...context.config };
|
|
168
|
+
this.legacyCreatedAids.clear();
|
|
169
|
+
try {
|
|
170
|
+
const created = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(context.dataDir, "..", "created-aids.json"), "utf8"));
|
|
171
|
+
if (Array.isArray(created))
|
|
172
|
+
for (const aid of created)
|
|
173
|
+
if (typeof aid === "string" && aidPattern.test(aid))
|
|
174
|
+
this.legacyCreatedAids.add(aid);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error.code !== "ENOENT")
|
|
178
|
+
context.logger.warn("读取旧版身份创建记录失败", { error: safeError(error) });
|
|
179
|
+
}
|
|
48
180
|
for (const key of ["aid", "owner_aid", "aun_path", "encryption_seed", "root_ca_path", "allow_from", "agent_plugin"]) {
|
|
49
181
|
if (this.config[key] !== undefined && typeof this.config[key] !== "string")
|
|
50
182
|
throw new Error(`${key} 必须是字符串`);
|
|
@@ -63,21 +195,45 @@ class AunPlugin {
|
|
|
63
195
|
this.sdk ??= await loadSdk();
|
|
64
196
|
await (0, promises_1.mkdir)(this.path(), { recursive: true });
|
|
65
197
|
this.store = this.newStore();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.
|
|
198
|
+
for (const [method, path, handler] of [
|
|
199
|
+
["POST", "/api/plugins/aun/aid", this.createAid],
|
|
200
|
+
["GET", "/api/plugins/aun/identities", this.listIdentities],
|
|
201
|
+
["POST", "/api/plugins/aun/bind", this.bindAgent],
|
|
202
|
+
["POST", "/api/plugins/aun/settings", this.saveSettings],
|
|
203
|
+
...(["", "/app.js", "/style.css"].map((suffix) => ["GET", `/abweb/channels/aun${suffix}`, this.servePage])),
|
|
204
|
+
]) {
|
|
205
|
+
const dispose = context.http.register({ method, path, handler });
|
|
206
|
+
if (typeof dispose === "function")
|
|
207
|
+
this.unregister.push(dispose);
|
|
208
|
+
}
|
|
69
209
|
this.aid = typeof this.config.aid === "string" ? this.config.aid.trim() : "";
|
|
210
|
+
this.bindings = this.config.bindings && typeof this.config.bindings === "object" && !Array.isArray(this.config.bindings) ? { ...this.config.bindings } : {};
|
|
211
|
+
this.bindingTimes = this.config.binding_times && typeof this.config.binding_times === "object" && !Array.isArray(this.config.binding_times) ? { ...this.config.binding_times } : {};
|
|
212
|
+
for (const aid of Array.isArray(this.config.identities) ? this.config.identities : []) {
|
|
213
|
+
if (typeof aid === "string" && aidPattern.test(aid))
|
|
214
|
+
this.identities.add(aid);
|
|
215
|
+
}
|
|
70
216
|
if (this.aid)
|
|
71
|
-
|
|
217
|
+
this.identities.add(this.aid);
|
|
218
|
+
for (const aid of this.identities)
|
|
219
|
+
await this.connectAid(aid);
|
|
72
220
|
}
|
|
73
221
|
async connectAid(aid) {
|
|
74
222
|
try {
|
|
75
223
|
const loaded = this.store.load(aid);
|
|
76
224
|
if (!loaded.ok || !loaded.data)
|
|
77
225
|
throw new Error(loaded.error?.message ?? "本地密钥库中不存在该 AID");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
226
|
+
const client = new this.sdk.AUNClient(loaded.data.aid);
|
|
227
|
+
const subscription = client.on("message.received", (payload) => this.onMessage(payload, aid));
|
|
228
|
+
try {
|
|
229
|
+
await client.connect();
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
subscription.unsubscribe();
|
|
233
|
+
await client.close();
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
this.clients.set(aid, { client, subscription });
|
|
81
237
|
this.status = "ready";
|
|
82
238
|
this.healthMessage = "已连接";
|
|
83
239
|
}
|
|
@@ -85,23 +241,24 @@ class AunPlugin {
|
|
|
85
241
|
this.status = "degraded";
|
|
86
242
|
this.healthMessage = safeError(error);
|
|
87
243
|
this.context?.logger.warn("AUN 通道连接失败", { error: this.healthMessage });
|
|
88
|
-
await this.closeClient();
|
|
89
244
|
}
|
|
90
245
|
}
|
|
91
246
|
async deactivate(_reason) {
|
|
92
247
|
this.status = "degraded";
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
|
|
248
|
+
for (const { client, subscription } of this.clients.values()) {
|
|
249
|
+
subscription.unsubscribe();
|
|
250
|
+
await client.close();
|
|
251
|
+
}
|
|
252
|
+
this.clients.clear();
|
|
96
253
|
await this.store?.close();
|
|
97
254
|
this.store = undefined;
|
|
98
|
-
this.unregister
|
|
99
|
-
|
|
255
|
+
for (const dispose of this.unregister.splice(0))
|
|
256
|
+
dispose();
|
|
100
257
|
this.context?.http.unregister("aun");
|
|
101
258
|
this.context = undefined;
|
|
102
259
|
}
|
|
103
260
|
async health() {
|
|
104
|
-
return { status: this.status, message: this.healthMessage, details: { aid: this.aid || undefined, connected: this.
|
|
261
|
+
return { status: this.clients.size ? "ready" : this.status, message: this.healthMessage, details: { aid: this.aid || undefined, connected: this.clients.size > 0, aids: [...this.clients.keys()], identities: [...this.identities], bindings: this.bindings } };
|
|
105
262
|
}
|
|
106
263
|
async handle(_request) { throw new Error("AUN 是入站通道插件"); }
|
|
107
264
|
path() {
|
|
@@ -128,45 +285,96 @@ class AunPlugin {
|
|
|
128
285
|
const aid = input && typeof input === "object" && typeof input.aid === "string" ? input.aid.trim() : "";
|
|
129
286
|
if (!aidPattern.test(aid))
|
|
130
287
|
return this.json(response, 400, { error: "AID 格式无效" });
|
|
131
|
-
if (this.aid && aid !== this.aid)
|
|
132
|
-
return this.json(response, 409, { error: "创建其他身份前请先停止当前 AID" });
|
|
133
|
-
if (aid === this.aid)
|
|
134
|
-
return this.json(response, 200, { aid, status: "existing", connection: this.status });
|
|
135
288
|
try {
|
|
136
289
|
const body = input;
|
|
137
|
-
const
|
|
138
|
-
|
|
290
|
+
const requestedOwner = body.owner_aid ?? body.ownerAid;
|
|
291
|
+
const owner = typeof requestedOwner === "string" && requestedOwner.trim()
|
|
292
|
+
? requestedOwner.trim()
|
|
139
293
|
: typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
|
|
140
294
|
if (!owner)
|
|
141
295
|
return this.json(response, 400, { error: "创建 AID 时必须提供 Owner AID" });
|
|
142
296
|
if (!aidPattern.test(owner))
|
|
143
297
|
return this.json(response, 400, { error: "Owner AID 格式无效" });
|
|
144
|
-
const result = await this.
|
|
145
|
-
|
|
146
|
-
return this.json(response, 502, { error: result.error?.code ?? "注册失败", message: redact(result.error?.message ?? "AID 注册失败") });
|
|
147
|
-
if (result.data?.ownership_status !== "active")
|
|
148
|
-
return this.json(response, 202, { aid, owner, status: "等待 Owner 确认", operation_id: result.data?.operation_id });
|
|
149
|
-
this.config.aid = aid;
|
|
150
|
-
this.config.owner_aid = owner;
|
|
151
|
-
this.config.aun_path = this.path();
|
|
152
|
-
await (0, promises_1.mkdir)(this.context.dataDir, { recursive: true });
|
|
153
|
-
await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 });
|
|
154
|
-
this.aid = aid;
|
|
155
|
-
await this.connectAid(aid);
|
|
156
|
-
return this.json(response, 201, { aid, status: "已创建", connection: this.status });
|
|
298
|
+
const result = await this.identity.create(aid, owner);
|
|
299
|
+
return this.json(response, result.status === "等待 Owner 确认" ? 202 : 201, result);
|
|
157
300
|
}
|
|
158
301
|
catch (error) {
|
|
159
302
|
this.context?.logger.warn("AUN AID 注册失败", { error: safeError(error) });
|
|
160
303
|
return this.json(response, 502, { error: safeError(error) });
|
|
161
304
|
}
|
|
162
305
|
};
|
|
163
|
-
async
|
|
164
|
-
const
|
|
165
|
-
|
|
306
|
+
listIdentities = async (_request, response) => {
|
|
307
|
+
const groups = await this.identity.list();
|
|
308
|
+
const connected = new Set([...groups.evolcore, ...groups.muAgentBridge]);
|
|
309
|
+
const localAids = (await (0, local_aids_1.listExistingAids)()).filter((aid) => !connected.has(aid));
|
|
310
|
+
this.json(response, 200, { groups, localAids, bindings: this.bindings, bindingTimes: this.bindingTimes });
|
|
311
|
+
};
|
|
312
|
+
saveSettings = async (request, response) => {
|
|
313
|
+
try {
|
|
314
|
+
let raw = "";
|
|
315
|
+
for await (const chunk of request) {
|
|
316
|
+
raw += chunk;
|
|
317
|
+
if (raw.length > 65536)
|
|
318
|
+
throw new Error("请求内容过大");
|
|
319
|
+
}
|
|
320
|
+
const body = JSON.parse(raw || "{}");
|
|
321
|
+
for (const key of ["agent_plugin", "allow_from", "aun_path"])
|
|
322
|
+
if (typeof body[key] !== "string")
|
|
323
|
+
return this.json(response, 400, { error: `${key} 必须是字符串` });
|
|
324
|
+
if (body.encryption_seed !== undefined && typeof body.encryption_seed !== "string")
|
|
325
|
+
return this.json(response, 400, { error: "encryption_seed 必须是字符串" });
|
|
326
|
+
this.config = { ...this.config, agent_plugin: body.agent_plugin, allow_from: body.allow_from, aun_path: body.aun_path, ...(body.encryption_seed ? { encryption_seed: body.encryption_seed } : {}) };
|
|
327
|
+
await this.persist();
|
|
328
|
+
this.context?.logger.info("AUN 通道配置已保存");
|
|
329
|
+
this.json(response, 200, { ok: true });
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
this.context?.logger.warn("AUN 通道配置失败", { error: safeError(error) });
|
|
333
|
+
this.json(response, 400, { error: safeError(error) });
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
bindAgent = async (request, response) => {
|
|
337
|
+
try {
|
|
338
|
+
let raw = "";
|
|
339
|
+
for await (const chunk of request) {
|
|
340
|
+
raw += chunk;
|
|
341
|
+
if (raw.length > 65536)
|
|
342
|
+
throw new Error("请求内容过大");
|
|
343
|
+
}
|
|
344
|
+
const body = JSON.parse(raw || "{}");
|
|
345
|
+
const aid = typeof body.aid === "string" ? body.aid.trim() : "";
|
|
346
|
+
const pluginId = typeof body.pluginId === "string" ? body.pluginId.trim() : "";
|
|
347
|
+
const port = Number(body.port);
|
|
348
|
+
if (!aidPattern.test(aid) || !/^[a-z][a-z0-9-]{1,63}$/.test(pluginId) || !Number.isInteger(port) || port < 1 || port > 65535)
|
|
349
|
+
return this.json(response, 400, { error: "关联参数无效" });
|
|
350
|
+
await this.identity.bind(pluginId, aid, port);
|
|
351
|
+
this.json(response, 200, { ok: true });
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
this.json(response, 400, { error: safeError(error) });
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
servePage = async (request, response) => {
|
|
358
|
+
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
359
|
+
const file = path.endsWith("/app.js") ? "app.js" : path.endsWith("/style.css") ? "style.css" : "index.html";
|
|
360
|
+
try {
|
|
361
|
+
const content = await (0, promises_1.readFile)((0, node_path_1.join)(__dirname, "public", file));
|
|
362
|
+
response.statusCode = 200;
|
|
363
|
+
response.setHeader("content-type", file.endsWith(".js") ? "text/javascript; charset=utf-8" : file.endsWith(".css") ? "text/css; charset=utf-8" : "text/html; charset=utf-8");
|
|
364
|
+
response.setHeader("cache-control", "no-store");
|
|
365
|
+
response.end(content);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
this.json(response, 503, { error: "AUN 设置页面资源不可用" });
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
async onMessage(payload, aid) {
|
|
372
|
+
const message = (0, messages_1.normalizeAunMessage)(payload, aid, typeof this.config.allow_from === "string" ? this.config.allow_from : undefined);
|
|
373
|
+
if (!message || !this.seen.add(message.messageId ? `${aid}:${message.messageId}` : ""))
|
|
166
374
|
return;
|
|
167
375
|
const channelMessage = {
|
|
168
376
|
channel: "aun", peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
|
|
169
|
-
agentPlugin: typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork",
|
|
377
|
+
agentPlugin: this.bindings[aid] || (typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork"),
|
|
170
378
|
reply: async (text) => {
|
|
171
379
|
if (!text.trim())
|
|
172
380
|
return;
|
|
@@ -175,12 +383,12 @@ class AunPlugin {
|
|
|
175
383
|
payload.thread_id = message.threadId;
|
|
176
384
|
const params = { to: message.peerId, payload, encrypt: true };
|
|
177
385
|
try {
|
|
178
|
-
await this.client.call("message.send", params);
|
|
386
|
+
await this.clients.get(aid).client.call("message.send", params);
|
|
179
387
|
}
|
|
180
388
|
catch (error) {
|
|
181
389
|
if (!this.isE2eeError(error))
|
|
182
390
|
throw error;
|
|
183
|
-
await this.client.call("message.send", { ...params, encrypt: false });
|
|
391
|
+
await this.clients.get(aid).client.call("message.send", { ...params, encrypt: false });
|
|
184
392
|
}
|
|
185
393
|
},
|
|
186
394
|
};
|
|
@@ -196,12 +404,6 @@ class AunPlugin {
|
|
|
196
404
|
return [sdk.E2EEError, sdk.E2EEDecryptFailedError, sdk.E2EEDegradedError].some((type) => !!type && error instanceof type)
|
|
197
405
|
|| (error instanceof Error && /^(E2EE|E2EE[A-Za-z]+Error)/.test(error.name));
|
|
198
406
|
}
|
|
199
|
-
async closeClient() {
|
|
200
|
-
const client = this.client;
|
|
201
|
-
this.client = undefined;
|
|
202
|
-
if (client)
|
|
203
|
-
await client.close();
|
|
204
|
-
}
|
|
205
407
|
json(response, status, value) {
|
|
206
408
|
response.statusCode = status;
|
|
207
409
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const find = (selector) => document.querySelector(selector);
|
|
3
|
+
const status = find("#status");
|
|
4
|
+
const list = find("#identities");
|
|
5
|
+
const createForm = find("#create-form");
|
|
6
|
+
const configForm = find("#config-form");
|
|
7
|
+
let agents = [];
|
|
8
|
+
let loading = false;
|
|
9
|
+
|
|
10
|
+
async function request(path, method = "GET", body) {
|
|
11
|
+
const token = localStorage.getItem("mab-login-token");
|
|
12
|
+
const response = await fetch(path, { method, headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body ? { "content-type": "application/json" } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
13
|
+
const result = await response.json();
|
|
14
|
+
if (!response.ok) throw new Error(result.error || `请求失败:${response.status}`);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function show(message, failed = false) { status.textContent = message; status.className = failed ? "error" : ""; }
|
|
18
|
+
async function load(refreshForm = true) {
|
|
19
|
+
if (loading) return;
|
|
20
|
+
loading = true;
|
|
21
|
+
try {
|
|
22
|
+
const [identities, plugins, config] = await Promise.all([request("/api/plugins/aun/identities"), request("/api/plugins"), request("/api/plugins/aun/config")]);
|
|
23
|
+
agents = (plugins.plugins || []).filter((plugin) => plugin.type === "agent");
|
|
24
|
+
if (refreshForm) createForm.elements.ownerAid.value = config.owner_aid || "";
|
|
25
|
+
await Promise.all(agents.map(async (agent) => { agent.configAid = (await request(`/api/plugins/${encodeURIComponent(agent.id)}/config`)).aid || ""; }));
|
|
26
|
+
const target = configForm.elements.agent_plugin;
|
|
27
|
+
if (refreshForm) {
|
|
28
|
+
target.replaceChildren(new Option("默认 Agent", ""), ...agents.map((agent) => new Option(agent.displayName || agent.id, agent.id)));
|
|
29
|
+
target.value = config.agent_plugin || "";
|
|
30
|
+
for (const key of ["allow_from", "aun_path"]) configForm.elements[key].value = config[key] || "";
|
|
31
|
+
configForm.elements.encryption_seed.value = "";
|
|
32
|
+
}
|
|
33
|
+
list.replaceChildren();
|
|
34
|
+
const entries = [["本工程 · 已连接", identities.groups.muAgentBridge || []], ["EvolCore · 已连接", identities.groups.evolcore || []]]
|
|
35
|
+
.flatMap(([label, values], group) => values.map((aid) => ({ aid, label, group, bound: identities.bindings?.[aid] || agents.find((agent) => agent.id && agent.configAid === aid)?.id || "", time: Date.parse(identities.bindingTimes?.[aid] || "") || 0 })));
|
|
36
|
+
entries.sort((a, b) => Number(Boolean(b.bound)) - Number(Boolean(a.bound)) || a.group - b.group || b.time - a.time || a.aid.localeCompare(b.aid));
|
|
37
|
+
for (const { aid, label, bound } of entries) {
|
|
38
|
+
const row = document.createElement("tr");
|
|
39
|
+
for (const value of [aid, label, bound || "未关联"]) { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); }
|
|
40
|
+
const cell = document.createElement("td"); const select = document.createElement("select"); select.setAttribute("aria-label", `${aid} 关联 Agent`);
|
|
41
|
+
select.append(new Option("选择 Agent", ""), ...agents.map((agent) => new Option(agent.displayName || agent.id, agent.id)));
|
|
42
|
+
select.value = bound; select.onchange = () => { if (select.value) void bind(aid, select.value); };
|
|
43
|
+
cell.append(select); row.append(cell); list.append(row);
|
|
44
|
+
}
|
|
45
|
+
if (!list.children.length) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = 4; cell.textContent = "暂无已连接 AID"; row.append(cell); list.append(row); }
|
|
46
|
+
show(`${(identities.groups.evolcore || []).length + (identities.groups.muAgentBridge || []).length} 个已连接 AID`);
|
|
47
|
+
} catch (error) { show(String(error), true); }
|
|
48
|
+
finally { loading = false; }
|
|
49
|
+
}
|
|
50
|
+
async function bind(aid, pluginId) {
|
|
51
|
+
try {
|
|
52
|
+
await request(`/api/plugins/${encodeURIComponent(pluginId)}/aid`, "PATCH", { aid });
|
|
53
|
+
await load(); show(`${aid} 已关联 ${pluginId}`);
|
|
54
|
+
} catch (error) { show(`关联失败:${String(error)}`, true); }
|
|
55
|
+
}
|
|
56
|
+
createForm.onsubmit = (event) => { event.preventDefault(); const aid = createForm.elements.aid.value.trim(); const owner = createForm.elements.ownerAid.value.trim();
|
|
57
|
+
void request("/api/plugins/aun/aid", "POST", { aid, ownerAid: owner }).then((result) => load().then(() => show(result.status === "等待 Owner 确认" ? `${aid} 等待 Owner 确认` : `${aid} 已创建`))).catch((error) => show(String(error), true));
|
|
58
|
+
};
|
|
59
|
+
configForm.onsubmit = (event) => { event.preventDefault(); const changes = { agent_plugin: configForm.elements.agent_plugin.value, allow_from: configForm.elements.allow_from.value, aun_path: configForm.elements.aun_path.value };
|
|
60
|
+
if (configForm.elements.encryption_seed.value) changes.encryption_seed = configForm.elements.encryption_seed.value;
|
|
61
|
+
void request("/api/plugins/aun/settings", "POST", changes).then(() => request("/api/plugins/aun/reload", "POST")).then(() => load()).then(() => show("配置已保存")).catch((error) => show(String(error), true));
|
|
62
|
+
};
|
|
63
|
+
find("#refresh").onclick = () => void load();
|
|
64
|
+
void load();
|
|
65
|
+
setInterval(() => { if (!document.hidden) void load(false); }, 10_000);
|
|
66
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/abweb/channels/aun/style.css"><title>AUN 设置</title></head>
|
|
4
|
+
<body>
|
|
5
|
+
<main>
|
|
6
|
+
<header><h1>AUN 身份</h1><button id="refresh" type="button">刷新</button></header>
|
|
7
|
+
<p id="status" role="status"></p>
|
|
8
|
+
<div class="table-scroll"><table><thead><tr><th>AID</th><th>来源</th><th>已关联 Agent</th><th>操作</th></tr></thead><tbody id="identities"></tbody></table></div>
|
|
9
|
+
<form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="new.agentid.cn"></label><label>Owner AID<input name="ownerAid" required></label></div><button type="submit">创建</button></form>
|
|
10
|
+
<form id="config-form"><h2>通道配置</h2><div class="fields"><label>默认 Agent<select name="agent_plugin"></select></label><label>允许的来源<input name="allow_from"></label><label>AUN 存储路径<input name="aun_path"></label><label>加密种子<input name="encryption_seed" type="password" autocomplete="new-password" placeholder="留空则不修改"></label></div><button type="submit">保存配置</button></form>
|
|
11
|
+
</main>
|
|
12
|
+
<script src="/abweb/channels/aun/app.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
:root { font-family: "Segoe UI", "Microsoft YaHei", sans-serif; color: #182027; background: #fff; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin: 0; }
|
|
4
|
+
main { padding: 8px 2px 20px; }
|
|
5
|
+
header { display: flex; align-items: center; justify-content: space-between; }
|
|
6
|
+
h1 { font-size: 17px; margin: 0; }
|
|
7
|
+
h2 { font-size: 15px; margin: 0 0 12px; }
|
|
8
|
+
p { min-height: 20px; color: #54665d; font-size: 13px; }
|
|
9
|
+
form { padding: 18px 0; border-top: 1px solid #d9dfdc; }
|
|
10
|
+
.fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-bottom: 12px; }
|
|
11
|
+
label { display: grid; gap: 5px; font-size: 13px; }
|
|
12
|
+
input, select, button { min-height: 34px; padding: 6px 9px; border: 1px solid #bfc9c5; border-radius: 5px; background: white; color: inherit; font: inherit; }
|
|
13
|
+
button { cursor: pointer; }
|
|
14
|
+
button:hover { background: #e5f2ea; }
|
|
15
|
+
.table-scroll { overflow-x: auto; margin: 10px 0 20px; }
|
|
16
|
+
table { width: 100%; min-width: 650px; border-collapse: collapse; font-size: 13px; }
|
|
17
|
+
th, td { padding: 9px 8px; text-align: left; border-bottom: 1px solid #dce3df; }
|
|
18
|
+
th { background: #edf1ef; color: #566761; }
|
|
19
|
+
td select { max-width: 190px; }
|
|
20
|
+
.error { color: #a83427; }
|
|
21
|
+
@media (max-width: 620px) { .fields { grid-template-columns: 1fr; } }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bridge-aun",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "mu-agent-bridge 的 AUN 通道插件。",
|
|
5
5
|
"keywords": ["mu-agent-bridge-plugin", "mu-agent-bridge-channel"],
|
|
6
6
|
"repository": { "type": "git", "url": "https://codeup.aliyun.com/6808da56fb3da14b899afd54/agent_bridge.git", "directory": "packages/aun" },
|
package/plugin.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "aun",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "channel",
|
|
5
5
|
"displayName": "AUN 通道",
|
|
6
6
|
"entry": "dist/src-ts/plugins/aun/index.js",
|
|
7
7
|
"apiVersion": "1",
|
|
8
8
|
"process": "in-process",
|
|
9
9
|
"capabilities": ["channel", "channel.identity.create", "channel.receive", "channel.send", "health"],
|
|
10
|
-
"configSchema": "config.schema.json"
|
|
10
|
+
"configSchema": "config.schema.json",
|
|
11
|
+
"managementPage": "/abweb/channels/aun"
|
|
11
12
|
}
|