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.
@@ -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 ajv_1 = __importDefault(require("ajv"));
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: `${message.channel}:${message.peerId}` });
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
- await this.validateConfig(loaded.record, config);
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(config, null, 2));
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(config);
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 validator = new ajv_1.default({ allErrors: true }).compile(validationSchema);
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(`插件配置无效:${new ajv_1.default({ allErrors: true }).errorsText(validator.errors)}`);
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
- if (coreConstraint) {
574
- const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
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;
@@ -5,7 +5,7 @@ const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const MANIFEST = {
7
7
  id: "abweb",
8
- version: "1.0.3",
8
+ version: "1.0.5",
9
9
  displayName: "Agent Bridge Web",
10
10
  entry: "dist/index.js",
11
11
  apiVersion: "1",
@@ -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.3", type: "channel", displayName: "AUN 通道",
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: "[]", 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
+ 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.2",
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.1",
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.2", 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" };
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.config.app_id ? [String(this.config.app_id)] : [], localMuAgentBridge: [] }),
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) => { if (pluginId === "feishu" || pluginId === "abweb")
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
- if (!this.config.app_id || !this.config.app_secret)
60
- return;
61
- this.client = this.options.client ?? new client_1.FeishuClient(this.config);
62
- try {
63
- await this.client.start((event) => this.onMessage(event));
64
- this.status = "ready";
65
- this.message = "已连接";
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) { this.client?.close(); for (const dispose of this.unregister.splice(0))
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
- if (this.seen.has(event.messageId))
93
+ const key = JSON.stringify([appId, event.messageId]);
94
+ if (this.seen.has(key))
83
95
  return;
84
- this.seen.add(event.messageId);
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[String(this.config.app_id)] ?? this.config.agent_plugin ?? "qwenwork", reply: async (text) => { if (text.trim())
88
- await this.client?.send(event.peerId, text, event.threadId); } };
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
- if (!this.config.app_id || !pluginId)
107
- return this.json(response, 400, { error: "请先配置机器人并选择 Agent" });
108
- await this.identity.bind(pluginId, String(this.config.app_id), 0);
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 || (appId === this.config.app_id ? this.config.app_secret : "");
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.config = { ...this.config, app_id: appId, app_secret: appSecret };
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.config = { ...this.config, app_id: appId, app_secret: appSecret };
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 binding = find("#binding");
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
- binding.elements.pluginId.replaceChildren(new Option("选择 Agent", ""), ...options.map((option) => option.cloneNode(true)));
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
- binding.elements.pluginId.value = bound;
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
- if (state.status !== "ready") throw new Error(state.message || "飞书尚未连接");
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
- binding.onsubmit = (event) => { event.preventDefault(); void request("/api/plugins/feishu/bind", "POST", { pluginId: binding.elements.pluginId.value }).then(load).then(() => show("Agent 已关联")).catch((error) => show(String(error), true)); };
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
- <form id="binding"><h2>关联 Agent</h2><div class="fields"><label>Agent<select name="pluginId" required></select></label></div><button type="submit">关联</button></form>
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; }
@@ -6,7 +6,7 @@ const openai_adapter_1 = require("./openai-adapter");
6
6
  const cdp_client_1 = require("./cdp-client");
7
7
  exports.qwenworkManifest = {
8
8
  id: "qwenwork",
9
- version: "1.0.1",
9
+ version: "1.0.2",
10
10
  displayName: "千问办公",
11
11
  entry: "dist/index.js",
12
12
  apiVersion: "1",
@@ -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.0", displayName: "WorkBuddy", type: "agent",
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 field = () => ({ value: "", required: false, placeholder: "", addEventListener() { }, replaceChildren() { } });
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[0].app_secret, "");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "abweb",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "type": "management",
5
5
  "displayName": "Agent Bridge Web",
6
6
  "entry": "dist/index.js",