mu-agent-bridge 1.0.12 → 1.0.14
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/dist/src-ts/channel-sessions.js +68 -0
- package/dist/src-ts/control-server.js +2 -0
- package/dist/src-ts/plugin-manager.js +75 -15
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +26 -2
- package/dist/src-ts/plugins/abweb/public/index.html +1 -1
- package/dist/src-ts/plugins/aun/index.js +2 -2
- package/dist/src-ts/plugins/doubao/cdp-client.js +5 -4
- package/dist/src-ts/plugins/doubao/index.js +2 -1
- package/dist/src-ts/plugins/dsh/index.js +1 -1
- package/dist/src-ts/plugins/feishu/index.js +82 -39
- package/dist/src-ts/plugins/feishu/public/app.js +31 -10
- package/dist/src-ts/plugins/feishu/public/index.html +2 -1
- package/dist/src-ts/plugins/feishu/public/style.css +5 -0
- package/dist/src-ts/plugins/qwenwork/index.js +1 -1
- package/dist/src-ts/plugins/workbuddy/index.js +1 -1
- package/dist/tests-ts/doubao-plugin.test.js +4 -0
- package/dist/tests-ts/feishu-page.test.js +12 -6
- package/dist/tests-ts/feishu-plugin.test.js +53 -1
- package/dist/tests-ts/plugin-manager.test.js +157 -0
- package/package.json +1 -1
- package/plugins/abweb/plugin.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChannelSessions = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
class ChannelSessions {
|
|
8
|
+
directory;
|
|
9
|
+
sessions = {};
|
|
10
|
+
loaded;
|
|
11
|
+
writes = Promise.resolve();
|
|
12
|
+
pending = new Map();
|
|
13
|
+
constructor(directory) {
|
|
14
|
+
this.directory = directory;
|
|
15
|
+
}
|
|
16
|
+
async load() {
|
|
17
|
+
this.loaded ??= (async () => {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.directory, "channel-sessions.json"), "utf8"));
|
|
20
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
21
|
+
throw new Error("通道会话文件格式无效");
|
|
22
|
+
for (const [key, value] of Object.entries(data)) {
|
|
23
|
+
const entry = value;
|
|
24
|
+
if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string")
|
|
25
|
+
throw new Error("通道会话记录无效");
|
|
26
|
+
this.sessions[key] = entry;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== "ENOENT")
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
await this.loaded;
|
|
35
|
+
}
|
|
36
|
+
async list() { await this.load(); return Object.values(this.sessions).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); }
|
|
37
|
+
async run(message, agentPlugin, call) {
|
|
38
|
+
const source = JSON.stringify([message.channel, message.recipientId || "", message.chatId || "", message.peerId, message.threadId || ""]);
|
|
39
|
+
const previous = this.pending.get(source) ?? Promise.resolve();
|
|
40
|
+
const turn = previous.catch(() => { }).then(async () => {
|
|
41
|
+
await this.load();
|
|
42
|
+
const entry = this.sessions[source];
|
|
43
|
+
const sessionId = entry?.agentPlugin === agentPlugin ? entry.sessionId : (0, node_crypto_1.randomUUID)();
|
|
44
|
+
const response = await call(sessionId);
|
|
45
|
+
const next = { source, agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString() };
|
|
46
|
+
const save = this.writes.catch(() => { }).then(async () => {
|
|
47
|
+
const snapshot = { ...this.sessions, [source]: next };
|
|
48
|
+
await (0, promises_1.mkdir)(this.directory, { recursive: true });
|
|
49
|
+
const file = (0, node_path_1.join)(this.directory, "channel-sessions.json");
|
|
50
|
+
await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify(snapshot, null, 2)}\n`, { mode: 0o600 });
|
|
51
|
+
await (0, promises_1.rename)(`${file}.tmp`, file);
|
|
52
|
+
this.sessions = snapshot;
|
|
53
|
+
});
|
|
54
|
+
this.writes = save;
|
|
55
|
+
await save;
|
|
56
|
+
return response;
|
|
57
|
+
});
|
|
58
|
+
this.pending.set(source, turn);
|
|
59
|
+
try {
|
|
60
|
+
return await turn;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (this.pending.get(source) === turn)
|
|
64
|
+
this.pending.delete(source);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.ChannelSessions = ChannelSessions;
|
|
@@ -93,6 +93,8 @@ class ControlServer {
|
|
|
93
93
|
}
|
|
94
94
|
if (method === "GET" && url.pathname === "/api/messages")
|
|
95
95
|
return this.json(res, 200, { messages: this.options.manager.recentMessages() });
|
|
96
|
+
if (method === "GET" && url.pathname === "/api/sessions")
|
|
97
|
+
return this.json(res, 200, { sessions: await this.options.manager.sessions.list() });
|
|
96
98
|
if (method === "GET" && url.pathname === "/api/logs") {
|
|
97
99
|
const page = Number(url.searchParams.get("page") ?? "1");
|
|
98
100
|
const pageSize = Number(url.searchParams.get("pageSize") ?? "100");
|
|
@@ -15,7 +15,8 @@ const plugin_manifest_1 = require("./plugin-manifest");
|
|
|
15
15
|
const version_manager_1 = require("./version-manager");
|
|
16
16
|
const plugin_catalog_1 = require("./plugin-catalog");
|
|
17
17
|
const log_store_1 = require("./log-store");
|
|
18
|
-
const
|
|
18
|
+
const channel_sessions_1 = require("./channel-sessions");
|
|
19
|
+
const _2020_1 = __importDefault(require("ajv/dist/2020"));
|
|
19
20
|
const logger = { info() { }, warn() { }, error() { } };
|
|
20
21
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
21
22
|
class PluginManager {
|
|
@@ -26,11 +27,13 @@ class PluginManager {
|
|
|
26
27
|
messages = [];
|
|
27
28
|
versions;
|
|
28
29
|
logs;
|
|
30
|
+
sessions;
|
|
29
31
|
constructor(options) {
|
|
30
32
|
this.options = options;
|
|
31
33
|
this.root = (0, node_path_1.resolve)(options.pluginsDir);
|
|
32
34
|
this.versions = options.versions ?? new version_manager_1.VersionManager({ stateDir: options.stateDir });
|
|
33
35
|
this.logs = new log_store_1.LogStore((0, node_path_1.join)(options.stateDir, "logs"));
|
|
36
|
+
this.sessions = new channel_sessions_1.ChannelSessions(options.stateDir);
|
|
34
37
|
}
|
|
35
38
|
log(level, message, fields, source = "mab") { this.logs.append({ time: new Date().toISOString(), level, source, message, ...(fields ? { fields } : {}) }); }
|
|
36
39
|
async updatePolicy() { try {
|
|
@@ -64,6 +67,7 @@ class PluginManager {
|
|
|
64
67
|
const manifest = (0, plugin_manifest_1.validateManifest)(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(candidate, meta.agentBridge.plugin), "utf8")), candidate);
|
|
65
68
|
if (manifest.id !== packageName.slice("agent-bridge-".length) || manifest.version !== meta.version)
|
|
66
69
|
throw new agent_1.AgentError("npm 包与插件清单不匹配");
|
|
70
|
+
this.validateCandidate(manifest, meta.agentBridge);
|
|
67
71
|
this.log("info", `插件下载完成:${manifest.id}`, { packageName, version: meta.version }, "插件升级");
|
|
68
72
|
return target;
|
|
69
73
|
}
|
|
@@ -97,7 +101,7 @@ class PluginManager {
|
|
|
97
101
|
}
|
|
98
102
|
return this.list();
|
|
99
103
|
}
|
|
100
|
-
list() { return [...this.plugins.values()].map(({ record }) => ({ ...record, capabilities: [...record.capabilities] })); }
|
|
104
|
+
list() { return [...this.plugins.values()].map(({ record }) => ({ ...record, capabilities: [...record.capabilities], dependencyStatus: this.dependencyStatus(record) })); }
|
|
101
105
|
packageInfo(id) {
|
|
102
106
|
const record = this.plugins.get(id)?.record;
|
|
103
107
|
if (!record)
|
|
@@ -145,6 +149,7 @@ class PluginManager {
|
|
|
145
149
|
const manifest = (0, plugin_manifest_1.validateManifest)(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(candidate, meta.agentBridge.plugin), "utf8")), candidate);
|
|
146
150
|
if (manifest.id !== id || manifest.version !== meta.version)
|
|
147
151
|
throw new agent_1.AgentError("npm 包插件 ID 或版本不匹配");
|
|
152
|
+
this.validateCandidate(manifest, meta.agentBridge);
|
|
148
153
|
await this.stop(id, "reload");
|
|
149
154
|
stopped = true;
|
|
150
155
|
await (0, promises_1.rename)(prefix, backup);
|
|
@@ -209,6 +214,7 @@ class PluginManager {
|
|
|
209
214
|
const manifest = (0, plugin_manifest_1.validateManifest)(JSON.parse(await (0, promises_1.readFile)(manifestFile, "utf8")), directory);
|
|
210
215
|
if (manifest.id !== name.slice("agent-bridge-".length) || manifest.version !== meta.version || (meta.agentBridge.type && meta.agentBridge.type !== manifest.type))
|
|
211
216
|
throw new agent_1.AgentError("npm 包与插件清单不匹配");
|
|
217
|
+
this.validateCandidate(manifest, meta.agentBridge);
|
|
212
218
|
await this.discoverDirectory(directory, name, manifestFile);
|
|
213
219
|
const record = this.plugins.get(manifest.id)?.record;
|
|
214
220
|
if (!record)
|
|
@@ -233,12 +239,19 @@ class PluginManager {
|
|
|
233
239
|
await this.discover();
|
|
234
240
|
return this.plugins.get(manifest.id).record;
|
|
235
241
|
}
|
|
236
|
-
async start(id) {
|
|
242
|
+
async start(id) { await this.startWithDependencies(id, new Set()); }
|
|
243
|
+
async startWithDependencies(id, path) {
|
|
237
244
|
const loaded = this.plugins.get(id);
|
|
238
245
|
if (!loaded)
|
|
239
246
|
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
240
247
|
if (loaded.instance && loaded.record.status === "ready")
|
|
241
248
|
return;
|
|
249
|
+
if (path.has(id)) {
|
|
250
|
+
const reason = `插件存在循环依赖:${[...path, id].join(" -> ")}`;
|
|
251
|
+
loaded.record.status = "failed";
|
|
252
|
+
loaded.record.error = reason;
|
|
253
|
+
throw new agent_1.AgentError(reason);
|
|
254
|
+
}
|
|
242
255
|
const compatibility = this.checkCompatibility(loaded.record);
|
|
243
256
|
loaded.record.compatibility = compatibility;
|
|
244
257
|
if (!compatibility.ok) {
|
|
@@ -247,6 +260,10 @@ class PluginManager {
|
|
|
247
260
|
this.log("error", `插件兼容性检查失败:${id}`, { reason: compatibility.reason }, "插件生命周期");
|
|
248
261
|
throw new agent_1.AgentError(compatibility.reason);
|
|
249
262
|
}
|
|
263
|
+
const next = new Set(path);
|
|
264
|
+
next.add(id);
|
|
265
|
+
for (const dependency of Object.keys(loaded.record.manifest.dependencies ?? {}))
|
|
266
|
+
await this.startWithDependencies(dependency, next);
|
|
250
267
|
loaded.record.status = "starting";
|
|
251
268
|
try {
|
|
252
269
|
const instance = await this.load(loaded.record);
|
|
@@ -280,7 +297,7 @@ class PluginManager {
|
|
|
280
297
|
const target = message.agentPlugin || "qwenwork";
|
|
281
298
|
this.recordMessage(target, "request", message.text, message.peerId);
|
|
282
299
|
try {
|
|
283
|
-
const response = await this.handle(target, { task: message.text, sessionId
|
|
300
|
+
const response = await this.sessions.run(message, target, (sessionId) => this.handle(target, { task: message.text, sessionId }));
|
|
284
301
|
await message.reply(response.text);
|
|
285
302
|
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
286
303
|
}
|
|
@@ -293,6 +310,7 @@ class PluginManager {
|
|
|
293
310
|
});
|
|
294
311
|
loaded.instance = instance;
|
|
295
312
|
loaded.record.status = "ready";
|
|
313
|
+
loaded.record.error = undefined;
|
|
296
314
|
this.log("info", `插件已启动:${id}`, { version: loaded.record.version }, "插件生命周期");
|
|
297
315
|
}
|
|
298
316
|
catch (error) {
|
|
@@ -327,6 +345,9 @@ class PluginManager {
|
|
|
327
345
|
const loaded = this.plugins.get(id);
|
|
328
346
|
if (!loaded)
|
|
329
347
|
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
348
|
+
const users = [...this.plugins.values()].filter(({ record }) => Object.prototype.hasOwnProperty.call(record.manifest.dependencies ?? {}, id)).map(({ record }) => record.id);
|
|
349
|
+
if (users.length)
|
|
350
|
+
throw new agent_1.AgentError(`插件 ${id} 仍被依赖:${users.join("、")}`);
|
|
330
351
|
const path = (0, node_path_1.resolve)(loaded.record.path);
|
|
331
352
|
const packageName = (0, node_path_1.basename)(path);
|
|
332
353
|
const managedPrefix = (0, node_path_1.resolve)(this.options.stateDir, "npm", packageName);
|
|
@@ -344,15 +365,18 @@ class PluginManager {
|
|
|
344
365
|
const loaded = this.plugins.get(id);
|
|
345
366
|
if (!loaded)
|
|
346
367
|
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
347
|
-
|
|
368
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
369
|
+
throw new agent_1.AgentError("插件配置必须是对象");
|
|
348
370
|
const previous = await this.config(id);
|
|
371
|
+
const merged = { ...previous, ...config };
|
|
372
|
+
await this.validateConfig(loaded.record, merged);
|
|
349
373
|
const dir = (0, node_path_1.join)(this.options.stateDir, id);
|
|
350
374
|
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
351
|
-
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(
|
|
375
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(merged, null, 2));
|
|
352
376
|
const action = await this.reloadStrategy(loaded.record);
|
|
353
377
|
try {
|
|
354
378
|
if (action === "hot" && loaded.instance && typeof loaded.instance.reloadConfig === "function")
|
|
355
|
-
await loaded.instance.reloadConfig(
|
|
379
|
+
await loaded.instance.reloadConfig(merged);
|
|
356
380
|
else if (action === "reload" && loaded.instance)
|
|
357
381
|
await this.reload(id);
|
|
358
382
|
else if (action === "hot" && loaded.instance)
|
|
@@ -362,6 +386,12 @@ class PluginManager {
|
|
|
362
386
|
}
|
|
363
387
|
catch (error) {
|
|
364
388
|
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(previous, null, 2));
|
|
389
|
+
if (action === "reload" && !loaded.instance) {
|
|
390
|
+
try {
|
|
391
|
+
await this.start(id);
|
|
392
|
+
}
|
|
393
|
+
catch { /* 保留原始错误 */ }
|
|
394
|
+
}
|
|
365
395
|
this.log("error", `插件配置重载失败:${id}`, { action, error: error instanceof Error ? error.message : String(error) }, "插件配置");
|
|
366
396
|
throw error;
|
|
367
397
|
}
|
|
@@ -388,9 +418,10 @@ class PluginManager {
|
|
|
388
418
|
return; try {
|
|
389
419
|
const schema = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(record.path, record.manifest.configSchema), "utf8"));
|
|
390
420
|
const { ["x-agent-bridge"]: _metadata, ...validationSchema } = schema;
|
|
391
|
-
const
|
|
421
|
+
const ajv = new _2020_1.default({ allErrors: true, strict: false });
|
|
422
|
+
const validator = ajv.compile(validationSchema);
|
|
392
423
|
if (!validator(config))
|
|
393
|
-
throw new agent_1.AgentError(`插件配置无效:${
|
|
424
|
+
throw new agent_1.AgentError(`插件配置无效:${ajv.errorsText(validator.errors)}`);
|
|
394
425
|
}
|
|
395
426
|
catch (error) {
|
|
396
427
|
if (error instanceof agent_1.AgentError)
|
|
@@ -570,14 +601,11 @@ class PluginManager {
|
|
|
570
601
|
if (error instanceof agent_1.AgentError)
|
|
571
602
|
throw error; /* 内置插件没有 npm 元数据 */
|
|
572
603
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
if (!(0, version_manager_1.satisfiesVersion)(coreVersion, coreConstraint))
|
|
576
|
-
throw new agent_1.AgentError(`核心版本 ${coreVersion} 不满足插件要求 ${coreConstraint}`);
|
|
577
|
-
}
|
|
604
|
+
const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
|
|
605
|
+
const error = coreConstraint && !(0, version_manager_1.satisfiesVersion)(coreVersion, coreConstraint) ? `核心版本 ${coreVersion} 不满足插件要求 ${coreConstraint}` : undefined;
|
|
578
606
|
const trustedNames = new Set((process.env.AGENT_BRIDGE_TRUSTED_PACKAGES ?? "").split(",").map((name) => name.trim()).filter(Boolean));
|
|
579
607
|
const trusted = Boolean(packageName && trustedNames.has(packageName));
|
|
580
|
-
this.plugins.set(manifest.id, { record: { id: manifest.id, version: manifest.version, type: manifest.type ?? "other", displayName: manifest.displayName ?? manifest.id, status: "discovered", capabilities: manifest.capabilities, manifest, path: directory, ...(packageName ? { packageName, publisher, trusted, autoUpdate: trusted } : {}), ...(coreConstraint ? { coreConstraint } : {}) } });
|
|
608
|
+
this.plugins.set(manifest.id, { record: { id: manifest.id, version: manifest.version, type: manifest.type ?? "other", displayName: manifest.displayName ?? manifest.id, status: error ? "failed" : "discovered", capabilities: manifest.capabilities, manifest, path: directory, ...(packageName ? { packageName, publisher, trusted, autoUpdate: trusted } : {}), ...(coreConstraint ? { coreConstraint } : {}), ...(error ? { error } : {}) } });
|
|
581
609
|
}
|
|
582
610
|
catch (error) {
|
|
583
611
|
throw new agent_1.AgentError(`invalid plugin ${label}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
@@ -596,6 +624,38 @@ class PluginManager {
|
|
|
596
624
|
}
|
|
597
625
|
return { ok: true };
|
|
598
626
|
}
|
|
627
|
+
validateCandidate(manifest, metadata) {
|
|
628
|
+
const declared = metadata?.requires?.plugins;
|
|
629
|
+
if (declared && (Object.keys(declared).length !== Object.keys(manifest.dependencies ?? {}).length || Object.entries(declared).some(([id, range]) => manifest.dependencies?.[id] !== range)))
|
|
630
|
+
throw new agent_1.AgentError("npm 包与插件清单的依赖声明冲突");
|
|
631
|
+
const compatibility = this.checkCompatibility({ id: manifest.id, version: manifest.version, type: manifest.type ?? "other", displayName: manifest.displayName ?? manifest.id, status: "discovered", capabilities: manifest.capabilities, manifest, path: "", coreConstraint: metadata?.requires?.["mu-agent-bridge"] });
|
|
632
|
+
if (!compatibility.ok)
|
|
633
|
+
throw new agent_1.AgentError(compatibility.reason);
|
|
634
|
+
for (const { record } of this.plugins.values()) {
|
|
635
|
+
const range = record.manifest.dependencies?.[manifest.id];
|
|
636
|
+
if (range && !(0, version_manager_1.satisfiesVersion)(manifest.version, range))
|
|
637
|
+
throw new agent_1.AgentError(`插件 ${record.id} 要求 ${manifest.id}@${range},候选版本为 ${manifest.version}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
dependencyStatus(record) {
|
|
641
|
+
const statuses = [];
|
|
642
|
+
for (const [id, range] of Object.entries(record.manifest.dependencies ?? {})) {
|
|
643
|
+
const dependency = this.plugins.get(id)?.record;
|
|
644
|
+
if (!dependency)
|
|
645
|
+
statuses.push(`缺少 ${id}`);
|
|
646
|
+
else if (!(0, version_manager_1.satisfiesVersion)(dependency.version, range))
|
|
647
|
+
statuses.push(`${id}@${dependency.version} 不满足 ${range}`);
|
|
648
|
+
}
|
|
649
|
+
if (this.hasCycle(record.id))
|
|
650
|
+
statuses.push("存在循环依赖");
|
|
651
|
+
return statuses;
|
|
652
|
+
}
|
|
653
|
+
hasCycle(id) {
|
|
654
|
+
const visit = (current, path) => { if (path.has(current))
|
|
655
|
+
return true; const record = this.plugins.get(current)?.record; if (!record)
|
|
656
|
+
return false; const next = new Set(path); next.add(current); return Object.keys(record.manifest.dependencies ?? {}).some((dependency) => visit(dependency, next)); };
|
|
657
|
+
return visit(id, new Set());
|
|
658
|
+
}
|
|
599
659
|
async exists(path) { try {
|
|
600
660
|
await (0, promises_1.readFile)((0, node_path_1.join)(path, "plugin.json"));
|
|
601
661
|
return true;
|
|
@@ -117,8 +117,8 @@
|
|
|
117
117
|
loading = true;
|
|
118
118
|
try {
|
|
119
119
|
error("");
|
|
120
|
-
const [health, plugins, messages, versions, policy] = await Promise.all([
|
|
121
|
-
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"),
|
|
120
|
+
const [health, plugins, messages, versions, policy, sessions] = await Promise.all([
|
|
121
|
+
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"), request("/api/sessions"),
|
|
122
122
|
]);
|
|
123
123
|
policyNode.value = policy.mode;
|
|
124
124
|
policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
|
|
@@ -130,6 +130,17 @@
|
|
|
130
130
|
renderPlugins(installed);
|
|
131
131
|
renderChannels(installed);
|
|
132
132
|
renderMessages(messages.messages ?? []);
|
|
133
|
+
const sessionTable = node("#sessions");
|
|
134
|
+
sessionTable.replaceChildren();
|
|
135
|
+
for (const session of sessions.sessions ?? []) {
|
|
136
|
+
const row = document.createElement("tr");
|
|
137
|
+
for (const value of [session.source, session.agentPlugin, session.sessionId, new Date(session.updatedAt).toLocaleString()]) {
|
|
138
|
+
const cell = document.createElement("td");
|
|
139
|
+
cell.textContent = value;
|
|
140
|
+
row.append(cell);
|
|
141
|
+
}
|
|
142
|
+
sessionTable.append(row);
|
|
143
|
+
}
|
|
133
144
|
if (refreshCatalog)
|
|
134
145
|
void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
|
|
135
146
|
}
|
|
@@ -208,6 +219,13 @@
|
|
|
208
219
|
const status = document.createElement("span");
|
|
209
220
|
status.className = `status status-${plugin.status}`;
|
|
210
221
|
status.textContent = plugin.status;
|
|
222
|
+
const dependencies = Object.entries(plugin.manifest?.dependencies ?? {});
|
|
223
|
+
if (dependencies.length) {
|
|
224
|
+
const detail = document.createElement("small");
|
|
225
|
+
detail.className = "muted plugin-id";
|
|
226
|
+
detail.textContent = `依赖:${dependencies.map(([id, range]) => `${id} ${range}`).join(";")}`;
|
|
227
|
+
name.append(detail);
|
|
228
|
+
}
|
|
211
229
|
state.append(status);
|
|
212
230
|
if (plugin.error) {
|
|
213
231
|
const detail = document.createElement("small");
|
|
@@ -215,6 +233,12 @@
|
|
|
215
233
|
detail.textContent = plugin.error;
|
|
216
234
|
state.append(detail);
|
|
217
235
|
}
|
|
236
|
+
if (plugin.dependencyStatus?.length) {
|
|
237
|
+
const detail = document.createElement("small");
|
|
238
|
+
detail.className = "error-text plugin-id";
|
|
239
|
+
detail.textContent = `依赖:${plugin.dependencyStatus.join(";")}`;
|
|
240
|
+
state.append(detail);
|
|
241
|
+
}
|
|
218
242
|
const controls = document.createElement("td");
|
|
219
243
|
controls.className = "controls";
|
|
220
244
|
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<section><details id="installed-plugins" open><summary><h2>已安装插件</h2></summary><div class="table-scroll"><table class="plugin-table"><thead><tr><th scope="col">名称</th><th scope="col">类型</th><th scope="col">版本</th><th scope="col">状态</th><th scope="col">操作</th></tr></thead><tbody id="plugins"></tbody></table></div><div class="plugin-catalog"><div class="section-heading"><h3>npm 插件目录</h3><button id="search-packages" type="button">刷新目录</button></div><input id="package-search" placeholder="搜索插件名称或功能" aria-label="搜索 npm 插件"><div id="catalog" class="catalog" aria-live="polite"></div></div><form id="install-local-form"><input id="plugin-path" required placeholder="Windows 上的插件目录路径" aria-label="本地插件目录"><button type="submit">安装本地插件</button></form></details></section>
|
|
17
17
|
<section><div class="section-heading"><h2>Channel 设置</h2></div><nav id="channel-tabs" class="tabs channel-tabs" role="tablist" aria-label="Channel 设置"></nav><div id="channel-panel" role="tabpanel" aria-live="polite"></div></section>
|
|
18
18
|
</div>
|
|
19
|
-
<div id="messages-panel" role="tabpanel" aria-labelledby="messages-tab" hidden><section><div class="section-heading"><h2>最近通信</h2></div><div class="table-scroll"><table><thead><tr><th scope="col">时间</th><th scope="col">插件</th><th scope="col">接收 AID</th><th scope="col">来源 Peer</th><th scope="col">方向</th><th scope="col">消息</th><th scope="col">错误</th></tr></thead><tbody id="messages"></tbody></table></div><p id="messages-empty" class="muted" hidden>暂无通信记录</p></section></div>
|
|
19
|
+
<div id="messages-panel" role="tabpanel" aria-labelledby="messages-tab" hidden><section><div class="section-heading"><h2>最近通信</h2></div><div class="table-scroll"><table><thead><tr><th scope="col">时间</th><th scope="col">插件</th><th scope="col">接收 AID</th><th scope="col">来源 Peer</th><th scope="col">方向</th><th scope="col">消息</th><th scope="col">错误</th></tr></thead><tbody id="messages"></tbody></table></div><p id="messages-empty" class="muted" hidden>暂无通信记录</p><h2>通道会话</h2><div class="table-scroll"><table><thead><tr><th>来源</th><th>Agent</th><th>会话 ID</th><th>最近活跃时间</th></tr></thead><tbody id="sessions"></tbody></table></div></section></div>
|
|
20
20
|
<div id="logs-panel" role="tabpanel" aria-labelledby="logs-tab" hidden><section><div class="section-heading"><h2>运行日志</h2><button id="logs-refresh" type="button">刷新日志</button></div><div class="table-scroll"><table><thead><tr><th>时间</th><th>级别</th><th>来源</th><th>消息</th><th>详细字段</th></tr></thead><tbody id="logs"></tbody></table></div><p id="logs-empty" class="muted" hidden>暂无运行日志</p><div class="section-heading"><button id="logs-prev" type="button">上一页</button><span id="logs-page" class="muted">第 1 页</span><button id="logs-next" type="button">下一页</button></div></section></div>
|
|
21
21
|
<p id="error" class="error" role="alert" hidden></p>
|
|
22
22
|
</main>
|
|
@@ -9,7 +9,7 @@ const messages_1 = require("./messages");
|
|
|
9
9
|
const evolcore_1 = require("./evolcore");
|
|
10
10
|
const local_aids_1 = require("../../local-aids");
|
|
11
11
|
const manifest = {
|
|
12
|
-
id: "aun", version: "1.0.
|
|
12
|
+
id: "aun", version: "1.0.5", type: "channel", displayName: "AUN 通道",
|
|
13
13
|
entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
|
|
14
14
|
capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health"],
|
|
15
15
|
configSchema: "config.schema.json",
|
|
@@ -373,7 +373,7 @@ class AunPlugin {
|
|
|
373
373
|
if (!message || !this.seen.add(message.messageId ? `${aid}:${message.messageId}` : ""))
|
|
374
374
|
return;
|
|
375
375
|
const channelMessage = {
|
|
376
|
-
channel: "aun", peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
|
|
376
|
+
channel: "aun", recipientId: aid, peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
|
|
377
377
|
agentPlugin: this.bindings[aid] || (typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork"),
|
|
378
378
|
reply: async (text) => {
|
|
379
379
|
if (!text.trim())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DoubaoCdpClient = void 0;
|
|
4
|
+
exports.requestBody = requestBody;
|
|
4
5
|
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
const defaults = { port: Number(process.env.DOUBAO_CDP_PORT ?? "9227"), conversationId: process.env.DOUBAO_CONVERSATION_ID ?? "759450172544514", botId: process.env.DOUBAO_BOT_ID ?? "7338286299411103781", timeoutMs: Number(process.env.DOUBAO_TIMEOUT_MS ?? "180000") };
|
|
6
7
|
function sleep(ms, signal) {
|
|
@@ -8,17 +9,17 @@ function sleep(ms, signal) {
|
|
|
8
9
|
return Promise.reject(new Error("请求已取消"));
|
|
9
10
|
return new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new Error("请求已取消")); }, { once: true }); });
|
|
10
11
|
}
|
|
11
|
-
function requestBody(text, conversationId, botId) {
|
|
12
|
+
function requestBody(text, conversationId, botId, pluginList = []) {
|
|
12
13
|
const general = { runtime_type: 0, agent_task_param: {}, agent_task_param_change: { runtime_changed: false, device_changed: false, sandbox_auth_type_changed: false }, project_id: "", need_modify_conversation: false };
|
|
13
14
|
const block = { block_type: 10000, content: { text_block: { text, icon_url: "", icon_url_dark: "", summary: "" }, pc_event_block: "" }, block_id: (0, node_crypto_1.randomUUID)(), parent_id: "", meta_info: [], append_fields: [] };
|
|
14
|
-
return JSON.stringify({ client_meta: { conversation_id: conversationId, bot_id: botId, last_section_id: "", last_message_index: null, local_permissions: [{ permission_name: "ACCESS_COARSE_LOCATION", status: 1 }, { permission_name: "ACCESS_FINE_LOCATION", status: 1 }, { permission_name: "ACCESS_BACKGROUND_LOCATION", status: 1 }] }, messages: [{ local_message_id: (0, node_crypto_1.randomUUID)(), content_block: [block], message_status: 0 }], option: { send_message_scene: "", create_time_ms: Date.now(), collect_id: "", is_audio: false, answer_with_suggest: false, agent_mode: 2, tts_switch: false, need_deep_think: 0, click_clear_context: false, is_regen: false, is_replace: false, is_from_click_option: false, is_from_click_softlink: false, disable_sse_cache: false, scene_type: 0, unique_key: (0, node_crypto_1.randomUUID)(), start_seq: 0, need_create_conversation: false, regen_query_id: [], edit_query_id: [], message_from: 0, sse_recv_event_options: { support_chunk_delta: true }, support_lazy_fetch_stream: true, is_ai_playground: false, is_old_user: true, general_task_param: general, recovery_option: { is_recovery: false, req_create_time_sec: Math.floor(Date.now() / 1000), append_sse_event_scene: 0 }, message_storage_type: 0, related_deleted_message_ids: {}, connector_info_list: [], model_config: { model_item_key: "0", model_extra_params: {}, reasoning_effort: 5 }, aggregate_params: { mention_skill_list: "[]", mention_plugin_list:
|
|
15
|
+
return JSON.stringify({ client_meta: { conversation_id: conversationId, bot_id: botId, last_section_id: "", last_message_index: null, local_permissions: [{ permission_name: "ACCESS_COARSE_LOCATION", status: 1 }, { permission_name: "ACCESS_FINE_LOCATION", status: 1 }, { permission_name: "ACCESS_BACKGROUND_LOCATION", status: 1 }] }, messages: [{ local_message_id: (0, node_crypto_1.randomUUID)(), content_block: [block], message_status: 0 }], option: { send_message_scene: "", create_time_ms: Date.now(), collect_id: "", is_audio: false, answer_with_suggest: false, agent_mode: 2, tts_switch: false, need_deep_think: 0, click_clear_context: false, is_regen: false, is_replace: false, is_from_click_option: false, is_from_click_softlink: false, disable_sse_cache: false, scene_type: 0, unique_key: (0, node_crypto_1.randomUUID)(), start_seq: 0, need_create_conversation: false, regen_query_id: [], edit_query_id: [], message_from: 0, sse_recv_event_options: { support_chunk_delta: true }, support_lazy_fetch_stream: true, is_ai_playground: false, is_old_user: true, general_task_param: general, recovery_option: { is_recovery: false, req_create_time_sec: Math.floor(Date.now() / 1000), append_sse_event_scene: 0 }, message_storage_type: 0, related_deleted_message_ids: {}, connector_info_list: [], model_config: { model_item_key: "0", model_extra_params: {}, reasoning_effort: 5 }, aggregate_params: { mention_skill_list: "[]", mention_plugin_list: JSON.stringify(pluginList), mention_ext: "[{}]", conversation_mode: "", mode_id: "1", model_item_key: "0", agent_mode: "2", reasoning_effort: "5", provider_id: "" } }, user_context: [], ext: { agent_mode: "2", use_deep_think: "0", general_task_param: JSON.stringify(general), collection_id: "", is_finish: "1", commerce_credit_config_enable: "0" } });
|
|
15
16
|
}
|
|
16
17
|
class DoubaoCdpClient {
|
|
17
18
|
socket;
|
|
18
19
|
sequence = 0;
|
|
19
20
|
pending = new Map();
|
|
20
21
|
options;
|
|
21
|
-
constructor(options = {}) { this.options = { port: options.port ?? defaults.port, conversationId: options.conversationId ?? defaults.conversationId, botId: options.botId ?? defaults.botId, timeoutMs: options.timeoutMs ?? defaults.timeoutMs, fetchImpl: options.fetchImpl }; }
|
|
22
|
+
constructor(options = {}) { this.options = { port: options.port ?? defaults.port, conversationId: options.conversationId ?? defaults.conversationId, botId: options.botId ?? defaults.botId, pluginList: options.pluginList ?? [], timeoutMs: options.timeoutMs ?? defaults.timeoutMs, fetchImpl: options.fetchImpl }; }
|
|
22
23
|
connected() { return Boolean(this.socket); }
|
|
23
24
|
async connect() {
|
|
24
25
|
if (this.socket)
|
|
@@ -38,7 +39,7 @@ class DoubaoCdpClient {
|
|
|
38
39
|
close() { this.socket?.close(); this.socket = undefined; for (const pending of this.pending.values())
|
|
39
40
|
pending.reject(new Error("豆包 CDP 已关闭")); this.pending.clear(); }
|
|
40
41
|
async direct(text, signal) {
|
|
41
|
-
await this.evaluate(buildInjection(requestBody(text, this.options.conversationId, this.options.botId)));
|
|
42
|
+
await this.evaluate(buildInjection(requestBody(text, this.options.conversationId, this.options.botId, this.options.pluginList)));
|
|
42
43
|
const deadline = Date.now() + this.options.timeoutMs;
|
|
43
44
|
while (Date.now() < deadline) {
|
|
44
45
|
await sleep(1000, signal);
|
|
@@ -4,7 +4,7 @@ exports.DoubaoPlugin = exports.doubaoManifest = void 0;
|
|
|
4
4
|
const cdp_client_1 = require("./cdp-client");
|
|
5
5
|
exports.doubaoManifest = {
|
|
6
6
|
id: "doubao",
|
|
7
|
-
version: "1.0.
|
|
7
|
+
version: "1.0.4",
|
|
8
8
|
displayName: "豆包",
|
|
9
9
|
entry: "dist/index.js",
|
|
10
10
|
apiVersion: "1",
|
|
@@ -27,6 +27,7 @@ class DoubaoPlugin {
|
|
|
27
27
|
port: typeof context.config.cdpPort === "number" ? context.config.cdpPort : undefined,
|
|
28
28
|
conversationId: typeof context.config.conversationId === "string" ? context.config.conversationId : undefined,
|
|
29
29
|
botId: typeof context.config.botId === "string" ? context.config.botId : undefined,
|
|
30
|
+
pluginList: Array.isArray(context.config.pluginList) ? context.config.pluginList.filter((value) => typeof value === "string") : undefined,
|
|
30
31
|
timeoutMs: typeof context.config.timeoutMs === "number" ? context.config.timeoutMs : undefined,
|
|
31
32
|
});
|
|
32
33
|
try {
|
|
@@ -6,7 +6,7 @@ const dsh_agent_1 = require("../../dsh-agent");
|
|
|
6
6
|
const openai_adapter_1 = require("../qwenwork/openai-adapter");
|
|
7
7
|
exports.dshManifest = {
|
|
8
8
|
id: "dsh",
|
|
9
|
-
version: "1.0.
|
|
9
|
+
version: "1.0.2",
|
|
10
10
|
displayName: "DeepSeek Harness",
|
|
11
11
|
entry: "dist/index.js",
|
|
12
12
|
apiVersion: "1",
|
|
@@ -5,35 +5,31 @@ const promises_1 = require("node:fs/promises");
|
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
const qrcode_1 = require("qrcode");
|
|
7
7
|
const client_1 = require("./client");
|
|
8
|
-
const manifest = { id: "feishu", version: "1.0.
|
|
8
|
+
const manifest = { id: "feishu", version: "1.0.4", type: "channel", displayName: "飞书通道", entry: "dist/src-ts/plugins/feishu/index.js", apiVersion: "1", process: "in-process", capabilities: ["channel", "channel.bot.setup", "channel.receive", "channel.send", "health"], configSchema: "config.schema.json", managementPage: "/abweb/channels/feishu" };
|
|
9
9
|
class FeishuPlugin {
|
|
10
10
|
options;
|
|
11
11
|
manifest = manifest;
|
|
12
12
|
context;
|
|
13
|
-
client;
|
|
14
13
|
ingress;
|
|
15
14
|
config = {};
|
|
16
15
|
bindings = {};
|
|
17
16
|
status = "degraded";
|
|
18
17
|
message = "尚未配置飞书机器人";
|
|
19
18
|
unregister = [];
|
|
19
|
+
bots = [];
|
|
20
|
+
clients = new Map();
|
|
21
|
+
botStates = new Map();
|
|
22
|
+
bindingTimes = {};
|
|
20
23
|
seen = new Set();
|
|
21
24
|
constructor(options = {}) {
|
|
22
25
|
this.options = options;
|
|
23
|
-
this.client = options.client;
|
|
24
26
|
this.ingress = options.ingress;
|
|
25
27
|
}
|
|
26
28
|
identity = {
|
|
27
|
-
list: async () => ({ evolcore: [], muAgentBridge: this.
|
|
29
|
+
list: async () => ({ evolcore: [], muAgentBridge: this.bots.filter((bot) => this.botStates.get(bot.app_id)?.status === "ready").map((bot) => bot.app_id), localMuAgentBridge: [] }),
|
|
28
30
|
create: async (aid) => { if (!aid.trim())
|
|
29
31
|
throw new Error("机器人标识不能为空"); return { aid, status: "请通过配置接口提供 app_id 和 app_secret" }; },
|
|
30
|
-
bind: async (pluginId, aid) => {
|
|
31
|
-
throw new Error("只能关联 Agent 插件"); if (aid && aid !== this.config.app_id)
|
|
32
|
-
throw new Error("飞书机器人标识不存在"); if (this.context?.pluginStatus(pluginId) !== "ready")
|
|
33
|
-
throw new Error("目标 Agent 插件未启动"); if (aid)
|
|
34
|
-
this.bindings[String(aid)] = pluginId;
|
|
35
|
-
else
|
|
36
|
-
delete this.bindings[String(this.config.app_id)]; await this.persist(); },
|
|
32
|
+
bind: async (pluginId, aid) => { await this.bindBot(aid || String(this.config.app_id || ""), aid ? pluginId : ""); },
|
|
37
33
|
};
|
|
38
34
|
async activate(context) {
|
|
39
35
|
this.context = context;
|
|
@@ -56,36 +52,52 @@ class FeishuPlugin {
|
|
|
56
52
|
}
|
|
57
53
|
this.validate();
|
|
58
54
|
this.bindings = this.config.bindings && typeof this.config.bindings === "object" ? { ...this.config.bindings } : {};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
catch (error) {
|
|
68
|
-
this.status = "degraded";
|
|
69
|
-
this.message = error instanceof Error ? error.message : String(error);
|
|
70
|
-
context.logger.warn("飞书通道连接失败", { error: this.message });
|
|
55
|
+
this.bots = Array.isArray(this.config.bots) ? this.config.bots.map((bot) => ({ ...bot })) : [];
|
|
56
|
+
if (this.config.app_id && this.config.app_secret && !this.bots.some((bot) => bot.app_id === this.config.app_id))
|
|
57
|
+
this.bots.unshift({ app_id: this.config.app_id, app_secret: this.config.app_secret });
|
|
58
|
+
const ids = new Set();
|
|
59
|
+
for (const bot of this.bots) {
|
|
60
|
+
if (!bot || typeof bot.app_id !== "string" || !bot.app_id.trim() || typeof bot.app_secret !== "string" || !bot.app_secret.trim() || ids.has(bot.app_id))
|
|
61
|
+
throw new Error("机器人凭据无效或 App ID 重复");
|
|
62
|
+
ids.add(bot.app_id);
|
|
71
63
|
}
|
|
64
|
+
this.bindingTimes = { ...(this.config.binding_times || {}) };
|
|
65
|
+
this.botStates.clear();
|
|
66
|
+
await Promise.all(this.bots.map(async (bot) => {
|
|
67
|
+
const client = this.options.clientFactory?.(bot) ?? this.options.client ?? new client_1.FeishuClient({ ...this.config, ...bot });
|
|
68
|
+
this.clients.set(bot.app_id, client);
|
|
69
|
+
try {
|
|
70
|
+
await client.start((event) => this.onMessage(event, bot.app_id));
|
|
71
|
+
this.botStates.set(bot.app_id, { status: "ready", message: "已连接" });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
client.close();
|
|
76
|
+
this.botStates.set(bot.app_id, { status: "degraded", message });
|
|
77
|
+
context.logger.warn("飞书通道连接失败", { appId: bot.app_id, error: message });
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
this.status = this.bots.length && this.bots.every((bot) => this.botStates.get(bot.app_id)?.status === "ready") ? "ready" : "degraded";
|
|
81
|
+
this.message = this.status === "ready" ? "已连接" : [...this.botStates.values()].find((state) => state.status !== "ready")?.message || "尚未配置飞书机器人";
|
|
72
82
|
}
|
|
73
|
-
async deactivate(_reason) {
|
|
83
|
+
async deactivate(_reason) { for (const client of this.clients.values())
|
|
84
|
+
client.close(); this.clients.clear(); for (const dispose of this.unregister.splice(0))
|
|
74
85
|
dispose(); this.status = "degraded"; this.context = undefined; }
|
|
75
86
|
async health() { return { status: this.status, message: this.message, details: { appId: this.config.app_id || undefined, bindings: this.bindings } }; }
|
|
76
87
|
async handle(_request) { throw new Error("飞书是入站通道插件"); }
|
|
77
|
-
async onMessage(event) {
|
|
88
|
+
async onMessage(event, appId) {
|
|
78
89
|
const allowedFrom = this.csv("allow_from");
|
|
79
90
|
const allowedChats = this.csv("allow_chats");
|
|
80
91
|
if (allowedFrom.length && !allowedFrom.includes(event.peerId) || allowedChats.length && (!event.chatId || !allowedChats.includes(event.chatId)))
|
|
81
92
|
return;
|
|
82
|
-
|
|
93
|
+
const key = JSON.stringify([appId, event.messageId]);
|
|
94
|
+
if (this.seen.has(key))
|
|
83
95
|
return;
|
|
84
|
-
this.seen.add(
|
|
96
|
+
this.seen.add(key);
|
|
85
97
|
if (this.seen.size > 10000)
|
|
86
98
|
this.seen.delete(this.seen.values().next().value);
|
|
87
|
-
const channelMessage = { channel: "feishu", peerId: event.peerId, messageId: event.messageId, text: event.text, threadId: event.threadId, agentPlugin: this.bindings[
|
|
88
|
-
await this.
|
|
99
|
+
const channelMessage = { channel: "feishu", recipientId: appId, chatId: event.chatId, peerId: event.peerId, messageId: event.messageId, text: event.text, threadId: event.threadId, agentPlugin: this.bindings[appId] ?? this.config.agent_plugin ?? "qwenwork", reply: async (text) => { if (text.trim())
|
|
100
|
+
await this.clients.get(appId)?.send(event.peerId, text, event.threadId); } };
|
|
89
101
|
try {
|
|
90
102
|
await this.ingress.publish(channelMessage);
|
|
91
103
|
}
|
|
@@ -97,23 +109,54 @@ class FeishuPlugin {
|
|
|
97
109
|
if (this.config[key] !== undefined && typeof this.config[key] !== "string")
|
|
98
110
|
throw new Error(`${key} 必须是字符串`); }
|
|
99
111
|
csv(key) { return String(this.config[key] ?? "").split(",").map((value) => value.trim()).filter(Boolean); }
|
|
100
|
-
async persist() { await (0, promises_1.mkdir)(this.context.dataDir, { recursive: true }); this.config.bindings = this.bindings; await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 }); }
|
|
101
|
-
setupStatus = async (_request, response) => { this.json(response, 200, { app_id: this.config.app_id || "", configured: Boolean(this.config.app_id && this.config.app_secret), status: this.status, message: this.message, bindings: this.bindings, agent_plugin: this.config.agent_plugin || "", allow_from: this.config.allow_from || "", allow_chats: this.config.allow_chats || "" }); };
|
|
112
|
+
async persist() { await (0, promises_1.mkdir)(this.context.dataDir, { recursive: true }); this.config.bindings = this.bindings; this.config.bots = this.bots; this.config.binding_times = this.bindingTimes; await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 }); }
|
|
113
|
+
setupStatus = async (_request, response) => { this.json(response, 200, { app_id: this.config.app_id || "", configured: Boolean(this.config.app_id && this.config.app_secret), status: this.status, message: this.message, bots: this.bots.map((bot) => ({ app_id: bot.app_id, configured: true, ...(this.botStates.get(bot.app_id) || { status: "degraded", message: "等待连接" }), pluginId: this.bindings[bot.app_id] || "", binding_time: this.bindingTimes[bot.app_id] || "" })), bindings: this.bindings, agent_plugin: this.config.agent_plugin || "", allow_from: this.config.allow_from || "", allow_chats: this.config.allow_chats || "" }); };
|
|
102
114
|
bindAgent = async (request, response) => {
|
|
103
115
|
try {
|
|
104
116
|
const body = await this.readJson(request);
|
|
105
117
|
const pluginId = typeof body.pluginId === "string" ? body.pluginId.trim() : "";
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.context?.logger.info("飞书 Agent 已关联", { appId: this.config.app_id, pluginId });
|
|
110
|
-
this.json(response, 200, { ok: true, app_id: this.config.app_id, pluginId });
|
|
118
|
+
const appId = typeof body.app_id === "string" ? body.app_id.trim() : String(this.config.app_id || "");
|
|
119
|
+
await this.bindBot(appId, pluginId);
|
|
120
|
+
this.json(response, 200, { ok: true, app_id: appId, pluginId });
|
|
111
121
|
}
|
|
112
122
|
catch (error) {
|
|
113
123
|
this.context?.logger.warn("飞书 Agent 关联失败", { error: error instanceof Error ? error.message : String(error) });
|
|
114
124
|
this.json(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
115
125
|
}
|
|
116
126
|
};
|
|
127
|
+
async bindBot(appId, pluginId) {
|
|
128
|
+
if (!this.bots.some((bot) => bot.app_id === appId))
|
|
129
|
+
throw new Error("飞书机器人标识不存在");
|
|
130
|
+
if (pluginId && (["aun", "feishu", "abweb"].includes(pluginId) || this.context?.pluginStatus(pluginId) !== "ready"))
|
|
131
|
+
throw new Error("目标 Agent 插件未启动或类型无效");
|
|
132
|
+
const previous = { ...this.bindings };
|
|
133
|
+
const times = { ...this.bindingTimes };
|
|
134
|
+
if (pluginId) {
|
|
135
|
+
this.bindings[appId] = pluginId;
|
|
136
|
+
this.bindingTimes[appId] = new Date().toISOString();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
delete this.bindings[appId];
|
|
140
|
+
delete this.bindingTimes[appId];
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await this.persist();
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
this.bindings = previous;
|
|
147
|
+
this.bindingTimes = times;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
this.context?.logger.info("飞书 Agent 关联已更新", { appId, pluginId });
|
|
151
|
+
}
|
|
152
|
+
saveBot(appId, appSecret) {
|
|
153
|
+
const index = this.bots.findIndex((bot) => bot.app_id === appId);
|
|
154
|
+
if (index < 0)
|
|
155
|
+
this.bots.push({ app_id: appId, app_secret: appSecret });
|
|
156
|
+
else
|
|
157
|
+
this.bots[index] = { ...this.bots[index], app_secret: appSecret };
|
|
158
|
+
this.config = { ...this.config, app_id: appId, app_secret: appSecret };
|
|
159
|
+
}
|
|
117
160
|
servePage = async (request, response) => {
|
|
118
161
|
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
119
162
|
const file = path.endsWith("/app.js") ? "app.js" : path.endsWith("/style.css") ? "style.css" : "index.html";
|
|
@@ -133,10 +176,10 @@ class FeishuPlugin {
|
|
|
133
176
|
const body = await this.readJson(request);
|
|
134
177
|
const appId = typeof body.app_id === "string" ? body.app_id.trim() : "";
|
|
135
178
|
const providedSecret = typeof body.app_secret === "string" ? body.app_secret.trim() : "";
|
|
136
|
-
const appSecret = providedSecret || (
|
|
179
|
+
const appSecret = providedSecret || (this.bots.find((bot) => bot.app_id === appId)?.app_secret || "");
|
|
137
180
|
if (!appId || !appSecret)
|
|
138
181
|
return this.json(response, 400, { error: "新机器人需要同时提供 App ID 和 App Secret" });
|
|
139
|
-
this.
|
|
182
|
+
this.saveBot(appId, appSecret);
|
|
140
183
|
await this.persist();
|
|
141
184
|
this.context?.logger.info("飞书机器人凭据已保存", { appId });
|
|
142
185
|
this.json(response, 200, { app_id: appId, status: "已保存,重载插件后连接" });
|
|
@@ -191,7 +234,7 @@ class FeishuPlugin {
|
|
|
191
234
|
const appId = typeof result.client_id === "string" ? result.client_id : "";
|
|
192
235
|
const appSecret = typeof result.client_secret === "string" ? result.client_secret : "";
|
|
193
236
|
if (appId && appSecret) {
|
|
194
|
-
this.
|
|
237
|
+
this.saveBot(appId, appSecret);
|
|
195
238
|
await this.persist();
|
|
196
239
|
this.context?.logger.info("飞书授权凭据已自动保存", { appId });
|
|
197
240
|
return this.json(response, 200, { status: "completed", app_id: appId, owner_open_id: user?.open_id });
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
const authQr = find("#authorization-qr");
|
|
8
8
|
const authUrl = find("#authorization-url");
|
|
9
9
|
const credentials = find("#credentials");
|
|
10
|
-
const
|
|
10
|
+
const botTable = find("#bots");
|
|
11
|
+
let bots = [];
|
|
12
|
+
let loading = false;
|
|
11
13
|
const settings = find("#settings");
|
|
12
14
|
let pollTimer;
|
|
13
15
|
let setup = 0;
|
|
@@ -23,12 +25,27 @@
|
|
|
23
25
|
return result;
|
|
24
26
|
}
|
|
25
27
|
function show(message, failed = false) { notice.textContent = message; notice.className = failed ? "error" : ""; }
|
|
26
|
-
async function load() {
|
|
28
|
+
async function load(refreshForm = true) {
|
|
29
|
+
if (loading) return;
|
|
30
|
+
loading = true;
|
|
27
31
|
try {
|
|
28
32
|
const [state, plugins] = await Promise.all([request("/api/plugins/feishu/setup/status"), request("/api/plugins")]);
|
|
29
33
|
const agents = (plugins.plugins || []).filter((plugin) => plugin.type === "agent" && plugin.status === "ready");
|
|
30
34
|
const options = agents.map((agent) => new Option(agent.displayName || agent.id, agent.id));
|
|
31
|
-
|
|
35
|
+
bots = state.bots || [];
|
|
36
|
+
botTable.replaceChildren();
|
|
37
|
+
for (const bot of [...bots].sort((a, b) => Number(Boolean(b.pluginId)) - Number(Boolean(a.pluginId)) || Number(b.status === "ready") - Number(a.status === "ready") || (Date.parse(b.binding_time) || 0) - (Date.parse(a.binding_time) || 0))) {
|
|
38
|
+
const row = document.createElement("tr");
|
|
39
|
+
for (const text of [bot.app_id, bot.status === "ready" ? "已连接" : bot.message || "未连接", bot.pluginId || "未关联", bot.binding_time ? new Date(bot.binding_time).toLocaleString() : "—"]) { const cell = document.createElement("td"); cell.textContent = text; row.append(cell); }
|
|
40
|
+
const controls = document.createElement("td");
|
|
41
|
+
const select = document.createElement("select"); select.setAttribute("aria-label", `${bot.app_id} 关联 Agent`);
|
|
42
|
+
select.append(new Option("选择 Agent", ""), ...agents.map((agent) => new Option(agent.displayName || agent.id, agent.id))); select.value = bot.pluginId || "";
|
|
43
|
+
function button(text, action) { const button = document.createElement("button"); button.type = "button"; button.textContent = text; button.onclick = async () => { button.disabled = true; try { await action(); } catch (error) { show(String(error), true); } finally { button.disabled = false; } }; return button; }
|
|
44
|
+
async function bind(pluginId) { await request("/api/plugins/feishu/bind", "POST", { app_id: bot.app_id, pluginId }); await load(false); show(`${bot.app_id} ${pluginId ? "已关联 " + pluginId : "已解除关联"}`); }
|
|
45
|
+
controls.append(select, button("关联", () => bind(select.value)), button("解除", () => bind("")), button("编辑凭据", async () => { credentials.elements.app_id.value = bot.app_id; savedAppId = bot.app_id; secretMasked = true; credentials.elements.app_secret.value = "********"; credentials.elements.app_secret.required = false; }));
|
|
46
|
+
row.append(controls); botTable.append(row);
|
|
47
|
+
}
|
|
48
|
+
if (refreshForm) {
|
|
32
49
|
settings.elements.agent_plugin.replaceChildren(new Option("默认 Agent", ""), ...options);
|
|
33
50
|
settings.elements.agent_plugin.value = state.agent_plugin || "";
|
|
34
51
|
settings.elements.allow_from.value = state.allow_from || "";
|
|
@@ -39,19 +56,21 @@
|
|
|
39
56
|
credentials.elements.app_secret.value = secretMasked ? "********" : "";
|
|
40
57
|
credentials.elements.app_secret.required = !state.configured;
|
|
41
58
|
credentials.elements.app_secret.placeholder = state.configured ? "已保存,留空使用已有凭据" : "App Secret 必填";
|
|
59
|
+
}
|
|
42
60
|
find("#connection").textContent = state.message || state.status;
|
|
43
61
|
find("#app-id").textContent = state.app_id || "未配置";
|
|
44
62
|
const bound = state.bindings?.[state.app_id] || "";
|
|
45
63
|
find("#bound-agent").textContent = bound || state.agent_plugin || "未关联";
|
|
46
|
-
|
|
64
|
+
|
|
47
65
|
show(state.configured ? state.status === "ready" ? "机器人已连接" : `凭据已保存,但连接失败:${state.message || state.status}` : "请授权或手动配置机器人凭据", state.configured && state.status !== "ready");
|
|
48
66
|
return state;
|
|
49
|
-
} catch (error) { show(String(error), true); throw error; }
|
|
67
|
+
} catch (error) { show(String(error), true); throw error; } finally { loading = false; }
|
|
50
68
|
}
|
|
51
69
|
async function reconnect() {
|
|
52
70
|
await request("/api/plugins/feishu/reload", "POST");
|
|
53
|
-
const state = await load();
|
|
54
|
-
|
|
71
|
+
const state = await load(false);
|
|
72
|
+
const bot = state.bots?.find((bot) => bot.app_id === credentials.elements.app_id.value.trim());
|
|
73
|
+
if ((bot?.status || state.status) !== "ready") throw new Error(bot?.message || state.message || "飞书尚未连接");
|
|
55
74
|
}
|
|
56
75
|
async function poll(deviceCode, interval, deadline, currentSetup) {
|
|
57
76
|
if (currentSetup !== setup) return;
|
|
@@ -99,11 +118,12 @@
|
|
|
99
118
|
} catch (error) { if (currentSetup === setup) { authState.textContent = `启动授权失败:${String(error)}`; find("#begin").disabled = false; } }
|
|
100
119
|
};
|
|
101
120
|
credentials.elements.app_id.oninput = () => {
|
|
121
|
+
|
|
102
122
|
const changed = credentials.elements.app_id.value.trim() !== savedAppId;
|
|
103
123
|
if (changed && secretMasked) { credentials.elements.app_secret.value = ""; secretMasked = false; }
|
|
104
124
|
credentials.elements.app_secret.required = changed;
|
|
105
125
|
};
|
|
106
|
-
credentials.elements.app_secret.oninput = () => { secretMasked = false;
|
|
126
|
+
credentials.elements.app_secret.oninput = () => { secretMasked = false; };
|
|
107
127
|
credentials.elements.app_secret.addEventListener("invalid", () => show("首次配置或更换机器人时,App ID 和 App Secret 都需要填写", true));
|
|
108
128
|
credentials.onsubmit = async (event) => {
|
|
109
129
|
event.preventDefault(); const button = credentials.querySelector("button"); button.disabled = true; show("正在保存凭据并连接飞书");
|
|
@@ -111,11 +131,12 @@
|
|
|
111
131
|
catch (error) { show(String(error), true); }
|
|
112
132
|
finally { button.disabled = false; }
|
|
113
133
|
};
|
|
114
|
-
|
|
134
|
+
|
|
115
135
|
settings.onsubmit = (event) => { event.preventDefault(); void request("/api/plugins/feishu/settings", "POST", { agent_plugin: settings.elements.agent_plugin.value, allow_from: settings.elements.allow_from.value, allow_chats: settings.elements.allow_chats.value }).then(load).then(() => show("配置已保存")).catch((error) => show(String(error), true)); };
|
|
116
136
|
find("#refresh").onclick = () => void load().catch(() => {});
|
|
117
|
-
window.addEventListener("pagehide", () => { setup++; clearTimeout(pollTimer); });
|
|
137
|
+
window.addEventListener("pagehide", () => { setup++; clearTimeout(pollTimer); clearInterval(refreshTimer); });
|
|
118
138
|
void load().catch(() => {});
|
|
139
|
+
const refreshTimer = setInterval(() => { if (!document.hidden) void load(false).catch(() => {}); }, 10_000);
|
|
119
140
|
try {
|
|
120
141
|
const pending = JSON.parse(sessionStorage.getItem(authorizationKey) || "null");
|
|
121
142
|
if (pending && typeof pending.deviceCode === "string" && Number.isFinite(pending.deadline) && Number.isFinite(pending.interval) && pending.interval >= 1000) {
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
<header><h1>飞书机器人</h1><button id="refresh" type="button">刷新</button></header>
|
|
7
7
|
<p id="notice" role="status"></p>
|
|
8
8
|
<dl><div><dt>连接状态</dt><dd id="connection">未配置</dd></div><div><dt>App ID</dt><dd id="app-id">未配置</dd></div><div><dt>关联 Agent</dt><dd id="bound-agent">未关联</dd></div></dl>
|
|
9
|
+
<section><h2>机器人关联</h2><div class="table-wrap"><table><thead><tr><th>App ID</th><th>连接状态</th><th>关联 Agent</th><th>最近关联时间</th><th>操作</th></tr></thead><tbody id="bots"></tbody></table></div></section>
|
|
9
10
|
<section><h2>创建设备授权</h2><div class="actions"><button id="begin" type="button">创建飞书机器人</button><a id="authorization-link" target="_blank" rel="noopener noreferrer" hidden>打开飞书创建链接</a></div><div id="authorization-details" hidden><img id="authorization-qr" width="256" height="256" alt="飞书机器人创建授权二维码"><p id="authorization-url"></p></div><p id="authorization-state" role="status"></p></section>
|
|
10
11
|
<form id="credentials"><h2>手动配置凭据</h2><div class="fields"><label>App ID<input name="app_id" required autocomplete="off"></label><label>App Secret<input name="app_secret" type="password" required autocomplete="new-password"></label></div><button type="submit">保存并连接</button></form>
|
|
11
|
-
|
|
12
|
+
|
|
12
13
|
<form id="settings"><h2>通道配置</h2><div class="fields"><label>默认 Agent<select name="agent_plugin"></select></label><label>允许的 open_id<input name="allow_from"></label><label>允许的 chat_id<input name="allow_chats"></label></div><button type="submit">保存配置</button></form>
|
|
13
14
|
</main>
|
|
14
15
|
<script src="/abweb/channels/feishu/app.js"></script>
|
|
@@ -23,3 +23,8 @@ button:hover { background: #e5f2ea; }
|
|
|
23
23
|
a { color: #176848; }
|
|
24
24
|
.error { color: #a83427; }
|
|
25
25
|
@media (max-width: 620px) { .fields, dl { grid-template-columns: 1fr; } }
|
|
26
|
+
|
|
27
|
+
.table-wrap { overflow-x: auto; }
|
|
28
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
29
|
+
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #d9dfdc; }
|
|
30
|
+
td:last-child { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
@@ -9,7 +9,7 @@ const node_path_1 = require("node:path");
|
|
|
9
9
|
const cdp_client_1 = require("./cdp-client");
|
|
10
10
|
const cli_client_1 = require("./cli-client");
|
|
11
11
|
exports.workbuddyManifest = {
|
|
12
|
-
id: "workbuddy", version: "1.1.
|
|
12
|
+
id: "workbuddy", version: "1.1.1", displayName: "WorkBuddy", type: "agent",
|
|
13
13
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
14
14
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
15
15
|
};
|
|
@@ -12,3 +12,7 @@ const cdp_client_1 = require("../src-ts/plugins/doubao/cdp-client");
|
|
|
12
12
|
await strict_1.default.rejects(client.connect(), /未找到豆包/);
|
|
13
13
|
strict_1.default.match(url, /:\d+\/json$/);
|
|
14
14
|
});
|
|
15
|
+
(0, node_test_1.default)("请求可显式携带飞书伙伴标识", () => {
|
|
16
|
+
const body = JSON.parse((0, cdp_client_1.requestBody)("查询我的飞书文档", "conversation", "bot", ["feishu"]));
|
|
17
|
+
strict_1.default.equal(body.option.aggregate_params.mention_plugin_list, '["feishu"]');
|
|
18
|
+
});
|
|
@@ -9,7 +9,8 @@ const node_path_1 = require("node:path");
|
|
|
9
9
|
const node_test_1 = __importDefault(require("node:test"));
|
|
10
10
|
const node_vm_1 = require("node:vm");
|
|
11
11
|
(0, node_test_1.default)("飞书密码掩码在刷新和重连后保留,掩码不提交为真实密钥", async () => {
|
|
12
|
-
const
|
|
12
|
+
const element = () => ({ children: [], value: "", required: false, placeholder: "", textContent: "", addEventListener() { }, setAttribute() { }, append(...items) { this.children.push(...items); }, replaceChildren(...items) { this.children = items; } });
|
|
13
|
+
const field = element;
|
|
13
14
|
const button = { disabled: false };
|
|
14
15
|
const credentials = { elements: { app_id: field(), app_secret: field() }, querySelector: () => button };
|
|
15
16
|
const nodes = {
|
|
@@ -21,20 +22,25 @@ const node_vm_1 = require("node:vm");
|
|
|
21
22
|
let status = "ready";
|
|
22
23
|
const source = await (0, promises_1.readFile)((0, node_path_1.join)(__dirname, "../src-ts/plugins/feishu/public/app.js"), "utf8");
|
|
23
24
|
(0, node_vm_1.runInNewContext)(source, {
|
|
24
|
-
document: { querySelector: (id) => nodes[id] ??=
|
|
25
|
+
document: { querySelector: (id) => nodes[id] ??= element(), createElement: element },
|
|
25
26
|
localStorage: { getItem: () => null }, sessionStorage: { getItem: () => null },
|
|
26
|
-
window: { addEventListener() { } }, AbortSignal, setTimeout, clearTimeout,
|
|
27
|
+
window: { addEventListener() { } }, AbortSignal, setTimeout, clearTimeout, setInterval() { }, clearInterval() { },
|
|
27
28
|
Option: function () { },
|
|
28
29
|
fetch: async (path, init) => {
|
|
29
|
-
if (path.endsWith("/bot"))
|
|
30
|
+
if (path.endsWith("/bot") || path.endsWith("/bind"))
|
|
30
31
|
submitted.push(JSON.parse(init.body));
|
|
31
|
-
return { ok: true, json: async () => path.endsWith("/status") ? { app_id: "cli_1", configured: true, status, message: status === "ready" ? "已连接" : "连接失败" } : { plugins: [] } };
|
|
32
|
+
return { ok: true, json: async () => path.endsWith("/status") ? { app_id: "cli_1", configured: true, status, message: status === "ready" ? "已连接" : "连接失败", bots: [{ app_id: "cli_1", configured: true, status, message: "连接失败" }, { app_id: "cli_2", configured: true, status: "ready", pluginId: "dsh", binding_time: "2026-10-10T10:00:00Z" }] } : { plugins: [] } };
|
|
32
33
|
},
|
|
33
34
|
});
|
|
34
35
|
await new Promise((resolve) => setImmediate(resolve));
|
|
36
|
+
strict_1.default.equal(nodes["#bots"].children.length, 2);
|
|
37
|
+
strict_1.default.equal(nodes["#bots"].children[0].children[0].textContent, "cli_2");
|
|
38
|
+
await nodes["#bots"].children[0].children[4].children[1].onclick();
|
|
39
|
+
strict_1.default.equal(submitted[0].app_id, "cli_2");
|
|
40
|
+
strict_1.default.equal(submitted[0].pluginId, "dsh");
|
|
35
41
|
strict_1.default.equal(credentials.elements.app_secret.value, "********");
|
|
36
42
|
await credentials.onsubmit({ preventDefault() { } });
|
|
37
|
-
strict_1.default.equal(submitted[
|
|
43
|
+
strict_1.default.equal(submitted[1].app_secret, "");
|
|
38
44
|
strict_1.default.equal(credentials.elements.app_secret.value, "********");
|
|
39
45
|
status = "degraded";
|
|
40
46
|
await credentials.onsubmit({ preventDefault() { } });
|
|
@@ -27,7 +27,7 @@ const feishu_1 = require("../src-ts/plugins/feishu");
|
|
|
27
27
|
(0, node_test_1.default)("未配置凭据时仍注册机器人创建和配置路由", async () => {
|
|
28
28
|
const routes = new Map();
|
|
29
29
|
const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "feishu-setup-"));
|
|
30
|
-
const plugin = new feishu_1.FeishuPlugin({ ingress: { publish: async () => undefined } });
|
|
30
|
+
const plugin = new feishu_1.FeishuPlugin({ client: { async start() { }, close() { } }, ingress: { publish: async () => undefined } });
|
|
31
31
|
try {
|
|
32
32
|
await plugin.activate({ dataDir: state, config: { app_id: "cli_1", app_secret: "private" }, logger: { info() { }, warn() { }, error() { } }, http: { register(route) { routes.set(`${route.method} ${route.path}`, route.handler); return () => routes.delete(`${route.method} ${route.path}`); }, unregister() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready" });
|
|
33
33
|
strict_1.default.ok(routes.has("GET /abweb/channels/feishu"));
|
|
@@ -54,6 +54,23 @@ const feishu_1 = require("../src-ts/plugins/feishu");
|
|
|
54
54
|
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
|
+
(0, node_test_1.default)("一个飞书机器人连接失败不影响另一个机器人", async () => {
|
|
58
|
+
const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "feishu-partial-"));
|
|
59
|
+
const routes = new Map();
|
|
60
|
+
const plugin = new feishu_1.FeishuPlugin({ clientFactory: (config) => ({ async start() { if (config.app_id === "bad")
|
|
61
|
+
throw new Error("凭据无效"); }, close() { } }), ingress: { publish: async () => undefined } });
|
|
62
|
+
try {
|
|
63
|
+
await plugin.activate({ dataDir: state, config: { bots: [{ app_id: "good", app_secret: "one" }, { app_id: "bad", app_secret: "two" }] }, logger: { info() { }, warn() { }, error() { } }, http: { register(route) { routes.set(route.path, route.handler); }, unregister() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready" });
|
|
64
|
+
const response = { setHeader() { }, end(value) { this.body = JSON.parse(value); } };
|
|
65
|
+
await routes.get("/api/plugins/feishu/setup/status")({}, response);
|
|
66
|
+
strict_1.default.deepEqual(response.body.bots.map((bot) => bot.status), ["ready", "degraded"]);
|
|
67
|
+
strict_1.default.deepEqual((await plugin.identity.list()).muAgentBridge, ["good"]);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await plugin.deactivate("stop");
|
|
71
|
+
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
57
74
|
(0, node_test_1.default)("飞书设备授权轮询区分等待、拒绝、过期、完成和接口错误", async () => {
|
|
58
75
|
const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "feishu-poll-"));
|
|
59
76
|
const routes = new Map();
|
|
@@ -94,3 +111,38 @@ const feishu_1 = require("../src-ts/plugins/feishu");
|
|
|
94
111
|
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
95
112
|
}
|
|
96
113
|
});
|
|
114
|
+
(0, node_test_1.default)("飞书保留旧机器人并按 App ID 独立关联、路由和回复", async () => {
|
|
115
|
+
const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "feishu-multi-"));
|
|
116
|
+
const routes = new Map();
|
|
117
|
+
const handlers = new Map();
|
|
118
|
+
const sent = [];
|
|
119
|
+
const received = [];
|
|
120
|
+
const plugin = new feishu_1.FeishuPlugin({ clientFactory: (config) => ({
|
|
121
|
+
async start(handler) { handlers.set(config.app_id, handler); }, close() { },
|
|
122
|
+
async send(_peer, text) { sent.push(`${config.app_id}:${text}`); },
|
|
123
|
+
}), ingress: { publish: async (message) => { received.push(message); await message.reply("回复"); } } });
|
|
124
|
+
const response = { statusCode: 0, setHeader() { }, end(value) { this.body = value; } };
|
|
125
|
+
try {
|
|
126
|
+
await plugin.activate({ dataDir: state, config: { app_id: "cli_1", app_secret: "first", bindings: { cli_1: "dsh" } }, logger: { info() { }, warn() { }, error() { } }, http: { register(route) { routes.set(`${route.method} ${route.path}`, route.handler); return () => routes.delete(`${route.method} ${route.path}`); }, unregister() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready" });
|
|
127
|
+
await routes.get("POST /api/plugins/feishu/bot")(node_stream_1.Readable.from([JSON.stringify({ app_id: "cli_2", app_secret: "second" })]), response);
|
|
128
|
+
const stored = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(state, "config.json"), "utf8"));
|
|
129
|
+
strict_1.default.deepEqual(stored.bots.map((bot) => bot.app_id), ["cli_1", "cli_2"]);
|
|
130
|
+
await plugin.deactivate("reload");
|
|
131
|
+
await plugin.activate({ dataDir: state, config: stored, logger: { info() { }, warn() { }, error() { } }, http: { register(route) { routes.set(`${route.method} ${route.path}`, route.handler); return () => routes.delete(`${route.method} ${route.path}`); }, unregister() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready" });
|
|
132
|
+
await routes.get("POST /api/plugins/feishu/bind")(node_stream_1.Readable.from([JSON.stringify({ app_id: "cli_2", pluginId: "doubao" })]), response);
|
|
133
|
+
await routes.get("GET /api/plugins/feishu/setup/status")({}, response);
|
|
134
|
+
strict_1.default.equal(JSON.parse(response.body).bots.length, 2);
|
|
135
|
+
strict_1.default.doesNotMatch(response.body, /first|second/);
|
|
136
|
+
await handlers.get("cli_1")({ messageId: "om_1", peerId: "ou_1", chatId: "oc_1", text: "一" });
|
|
137
|
+
await handlers.get("cli_2")({ messageId: "om_1", peerId: "ou_1", chatId: "oc_1", text: "二" });
|
|
138
|
+
strict_1.default.deepEqual(received.map((message) => [message.agentPlugin, message.recipientId]), [["dsh", "cli_1"], ["doubao", "cli_2"]]);
|
|
139
|
+
strict_1.default.deepEqual(sent, ["cli_1:回复", "cli_2:回复"]);
|
|
140
|
+
await routes.get("POST /api/plugins/feishu/bind")(node_stream_1.Readable.from([JSON.stringify({ app_id: "cli_2", pluginId: "" })]), response);
|
|
141
|
+
strict_1.default.equal(JSON.parse(response.body).ok, true);
|
|
142
|
+
strict_1.default.equal(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(state, "config.json"), "utf8")).bindings.cli_2, undefined);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
await plugin.deactivate("stop");
|
|
146
|
+
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
@@ -10,6 +10,54 @@ const node_path_1 = require("node:path");
|
|
|
10
10
|
const node_test_1 = __importDefault(require("node:test"));
|
|
11
11
|
const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
12
12
|
const version_manager_1 = require("../src-ts/version-manager");
|
|
13
|
+
const channel_sessions_1 = require("../src-ts/channel-sessions");
|
|
14
|
+
(0, node_test_1.default)("通道会话隔离、续接和重启恢复", async () => {
|
|
15
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-sessions-"));
|
|
16
|
+
try {
|
|
17
|
+
const store = new channel_sessions_1.ChannelSessions(root);
|
|
18
|
+
const seen = [];
|
|
19
|
+
const message = { channel: "feishu", recipientId: "cli_1", chatId: "group", peerId: "peer", text: "消息" };
|
|
20
|
+
const call = async (sessionId) => { seen.push(sessionId); return { text: "回复", sessionId: "agent-session" }; };
|
|
21
|
+
await store.run(message, "dsh", call);
|
|
22
|
+
await store.run(message, "dsh", call);
|
|
23
|
+
strict_1.default.equal(seen[1], "agent-session");
|
|
24
|
+
strict_1.default.notEqual(seen[0], "agent-session");
|
|
25
|
+
const resumed = new channel_sessions_1.ChannelSessions(root);
|
|
26
|
+
await resumed.run(message, "dsh", call);
|
|
27
|
+
strict_1.default.equal(seen[2], "agent-session");
|
|
28
|
+
await resumed.run({ ...message, recipientId: "cli_2" }, "dsh", call);
|
|
29
|
+
strict_1.default.notEqual(seen[3], seen[0]);
|
|
30
|
+
await resumed.run({ ...message, channel: "aun" }, "dsh", call);
|
|
31
|
+
strict_1.default.notEqual(seen[4], seen[0]);
|
|
32
|
+
await resumed.run(message, "doubao", call);
|
|
33
|
+
strict_1.default.notEqual(seen[5], "agent-session");
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
(0, node_test_1.default)("同一来源排队续接,失败不污染会话且不同来源可并发", async () => {
|
|
40
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-session-queue-"));
|
|
41
|
+
try {
|
|
42
|
+
const store = new channel_sessions_1.ChannelSessions(root);
|
|
43
|
+
const message = { channel: "aun", recipientId: "local", peerId: "peer" };
|
|
44
|
+
let release;
|
|
45
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
46
|
+
const events = [];
|
|
47
|
+
const first = store.run(message, "dsh", async () => { events.push("first"); await gate; return { text: "一", sessionId: "saved" }; });
|
|
48
|
+
const second = store.run(message, "dsh", async (id) => { events.push(id); return { text: "二" }; });
|
|
49
|
+
await store.run({ ...message, peerId: "other" }, "dsh", async () => ({ text: "其它" }));
|
|
50
|
+
strict_1.default.deepEqual(events, ["first"]);
|
|
51
|
+
release();
|
|
52
|
+
await Promise.all([first, second]);
|
|
53
|
+
strict_1.default.deepEqual(events, ["first", "saved"]);
|
|
54
|
+
await strict_1.default.rejects(store.run(message, "dsh", async () => { throw new Error("失败"); }), /失败/);
|
|
55
|
+
await store.run(message, "dsh", async (id) => { strict_1.default.equal(id, "saved"); return { text: "恢复" }; });
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
13
61
|
(0, node_test_1.default)("插件版本约束支持常用比较符", () => {
|
|
14
62
|
strict_1.default.equal((0, version_manager_1.satisfiesVersion)("1.2.0", ">=1.0.0 <2.0.0"), true);
|
|
15
63
|
strict_1.default.equal((0, version_manager_1.satisfiesVersion)("2.0.0", ">=1.0.0 <2.0.0"), false);
|
|
@@ -71,6 +119,115 @@ const version_manager_1 = require("../src-ts/version-manager");
|
|
|
71
119
|
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
72
120
|
}
|
|
73
121
|
});
|
|
122
|
+
(0, node_test_1.default)("依赖插件不能直接卸载并显示诊断", async () => {
|
|
123
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-dependency-protection-"));
|
|
124
|
+
try {
|
|
125
|
+
for (const [id, dependencies] of [["base", undefined], ["consumer", { base: ">=1.0.0" }]]) {
|
|
126
|
+
const dir = (0, node_path_1.join)(root, id, "1.0.0");
|
|
127
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
128
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id, version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: [], ...(dependencies ? { dependencies } : {}) }));
|
|
129
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), "module.exports={activate:async()=>{},deactivate:async()=>{},health:async()=>({status:'ready'}),handle:async()=>({text:'ok'})};");
|
|
130
|
+
}
|
|
131
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: root, stateDir: (0, node_path_1.join)(root, "state") });
|
|
132
|
+
await manager.discover();
|
|
133
|
+
strict_1.default.deepEqual(manager.list().find((item) => item.id === "consumer")?.dependencyStatus, []);
|
|
134
|
+
await strict_1.default.rejects(() => manager.uninstall("base"), /仍被依赖/);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
(0, node_test_1.default)("依赖循环会出现在诊断中", async () => {
|
|
141
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-cycle-"));
|
|
142
|
+
try {
|
|
143
|
+
for (const [id, dependency] of [["first", "second"], ["second", "first"]]) {
|
|
144
|
+
const dir = (0, node_path_1.join)(root, id, "1.0.0");
|
|
145
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
146
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id, version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: [], dependencies: { [dependency]: ">=1.0.0" } }));
|
|
147
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), "module.exports={activate:async()=>{},deactivate:async()=>{},health:async()=>({status:'ready'}),handle:async()=>({text:'ok'})};");
|
|
148
|
+
}
|
|
149
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: root, stateDir: (0, node_path_1.join)(root, "state") });
|
|
150
|
+
await manager.discover();
|
|
151
|
+
strict_1.default.ok(manager.list().every((item) => item.dependencyStatus?.includes("存在循环依赖")));
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
(0, node_test_1.default)("现有六个插件配置兼容且局部更新保留旧字段", async () => {
|
|
158
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-existing-schema-"));
|
|
159
|
+
try {
|
|
160
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state") });
|
|
161
|
+
await manager.discover();
|
|
162
|
+
for (const id of ["abweb", "aun", "doubao", "dsh", "qwenwork", "workbuddy"]) {
|
|
163
|
+
await manager.install((0, node_path_1.join)(process.cwd(), "plugins", id));
|
|
164
|
+
const config = id === "abweb" ? { publicPath: "/abweb" } : { aid: "existing.agentid.cn" };
|
|
165
|
+
await manager.saveConfig(id, config);
|
|
166
|
+
await manager.saveConfig(id, {});
|
|
167
|
+
strict_1.default.deepEqual(await manager.config(id), config, id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
(0, node_test_1.default)("依赖会先启动且循环依赖无法启动", async () => {
|
|
175
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-dependency-start-"));
|
|
176
|
+
try {
|
|
177
|
+
for (const [id, dependencies] of [["consumer", { provider: ">=1.0.0" }], ["provider", {}], ["cyclea", { cycleb: ">=1.0.0" }], ["cycleb", { cyclea: ">=1.0.0" }]]) {
|
|
178
|
+
const dir = (0, node_path_1.join)(root, id);
|
|
179
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
180
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id, version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: [], dependencies }));
|
|
181
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), "module.exports={activate:async()=>{},deactivate:async()=>{},health:async()=>({status:'ready'}),handle:async()=>({text:'ok'})};");
|
|
182
|
+
}
|
|
183
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: root, stateDir: (0, node_path_1.join)(root, "state") });
|
|
184
|
+
await manager.discover();
|
|
185
|
+
await manager.start("consumer");
|
|
186
|
+
strict_1.default.equal(manager.get("provider")?.record.status, "ready");
|
|
187
|
+
await strict_1.default.rejects(() => manager.start("cyclea"), /循环依赖/);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
(0, node_test_1.default)("不兼容的缓存升级不会停用原插件", async () => {
|
|
194
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-upgrade-compat-"));
|
|
195
|
+
const name = "agent-bridge-sample";
|
|
196
|
+
try {
|
|
197
|
+
for (const [folder, version, requires] of [["npm", "1.0.0", undefined], ["downloads", "2.0.0", { "mu-agent-bridge": ">=999.0.0" }]]) {
|
|
198
|
+
const dir = folder === "npm" ? (0, node_path_1.join)(root, "state", "npm", name, "node_modules", name) : (0, node_path_1.join)(root, "state", "downloads", name, version, "node_modules", name);
|
|
199
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
200
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "package.json"), JSON.stringify({ name, version, agentBridge: { plugin: "./plugin.json", ...(requires ? { requires } : {}) } }));
|
|
201
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id: "sample", version, entry: "index.js", apiVersion: "1", capabilities: [] }));
|
|
202
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), "module.exports={activate:async()=>{},deactivate:async()=>{},health:async()=>({status:'ready'}),handle:async()=>({text:'ok'})};");
|
|
203
|
+
}
|
|
204
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state") });
|
|
205
|
+
await manager.discover();
|
|
206
|
+
await manager.start("sample");
|
|
207
|
+
await strict_1.default.rejects(() => manager.upgradePackage("sample", name, "2.0.0"), /核心版本/);
|
|
208
|
+
strict_1.default.equal(manager.get("sample")?.record.version, "1.0.0");
|
|
209
|
+
strict_1.default.equal(manager.get("sample")?.record.status, "ready");
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
(0, node_test_1.default)("已安装插件核心版本不兼容时仍显示在列表中", async () => {
|
|
216
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-incompatible-installed-"));
|
|
217
|
+
try {
|
|
218
|
+
const name = "agent-bridge-newer";
|
|
219
|
+
const dir = (0, node_path_1.join)(root, "state", "npm", name, "node_modules", name);
|
|
220
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
221
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "package.json"), JSON.stringify({ name, version: "1.0.0", agentBridge: { plugin: "./plugin.json", requires: { "mu-agent-bridge": ">=999.0.0" } } }));
|
|
222
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id: "newer", version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: [] }));
|
|
223
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state") });
|
|
224
|
+
strict_1.default.equal((await manager.discover()).find((record) => record.id === "newer")?.status, "failed");
|
|
225
|
+
strict_1.default.match(manager.list().find((record) => record.id === "newer")?.error ?? "", /核心版本/);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
74
231
|
(0, node_test_1.default)("plugin manager discovers and controls a plugin lifecycle", async () => {
|
|
75
232
|
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-plugin-"));
|
|
76
233
|
try {
|
package/package.json
CHANGED