mu-agent-bridge 1.0.30 → 1.0.31

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.
Files changed (73) hide show
  1. package/README.md +17 -1
  2. package/dist/installer/mu-agent-bridge.msi +0 -0
  3. package/dist/src-ts/agent-availability.js +14 -0
  4. package/dist/src-ts/channel-sessions.js +28 -5
  5. package/dist/src-ts/cli-commands.js +6 -2
  6. package/dist/src-ts/control-server.js +13 -2
  7. package/dist/src-ts/daemon-process.js +3 -2
  8. package/dist/src-ts/dsh-agent.js +4 -2
  9. package/dist/src-ts/plugin-manager.js +36 -3
  10. package/dist/src-ts/plugins/aun/index.js +52 -7
  11. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  12. package/dist/src-ts/plugins/chatgpt/cdp-client.js +31 -2
  13. package/dist/src-ts/plugins/chatgpt/index.js +37 -1
  14. package/dist/src-ts/plugins/chatgpt/session-store.js +6 -2
  15. package/dist/src-ts/plugins/desktop-launch.js +13 -2
  16. package/dist/src-ts/plugins/doubao/cdp-client.js +29 -3
  17. package/dist/src-ts/plugins/dsh/index.js +1 -0
  18. package/dist/src-ts/plugins/feishu/index.js +35 -10
  19. package/dist/src-ts/plugins/feishu/public/app.js +20 -8
  20. package/dist/src-ts/plugins/feishu/public/index.html +1 -1
  21. package/dist/src-ts/plugins/qwenwork/cdp-client.js +105 -31
  22. package/dist/src-ts/plugins/qwenwork/index.js +200 -7
  23. package/dist/src-ts/plugins/qwenwork/openai-adapter.js +12 -2
  24. package/dist/src-ts/plugins/workbuddy/cdp-client.js +10 -4
  25. package/dist/src-ts/plugins/workbuddy/index.js +139 -7
  26. package/dist/src-ts/secret-input.js +47 -0
  27. package/dist/tests-ts/agent-availability.test.js +27 -0
  28. package/dist/tests-ts/aun-default-owner-page.test.js +81 -0
  29. package/dist/tests-ts/aun-plugin.test.js +152 -8
  30. package/dist/tests-ts/channel-sessions.test.js +189 -0
  31. package/dist/tests-ts/chatgpt-plugin.test.js +104 -0
  32. package/dist/tests-ts/cli-commands.test.js +58 -0
  33. package/dist/tests-ts/control-server.test.js +51 -0
  34. package/dist/tests-ts/daemon-process.test.js +1 -0
  35. package/dist/tests-ts/desktop-launch.test.js +40 -0
  36. package/dist/tests-ts/doubao-plugin.test.js +10 -0
  37. package/dist/tests-ts/dsh-agent.test.js +25 -0
  38. package/dist/tests-ts/feishu-page.test.js +10 -3
  39. package/dist/tests-ts/feishu-plugin.test.js +62 -1
  40. package/dist/tests-ts/installer-assets.test.js +84 -0
  41. package/dist/tests-ts/plugin-manager.test.js +135 -2
  42. package/dist/tests-ts/qwenwork-plugin.test.js +423 -0
  43. package/dist/tests-ts/qwenwork-sse.test.js +46 -0
  44. package/dist/tests-ts/workbuddy-plugin.test.js +278 -3
  45. package/package.json +2 -1
  46. package/plugins/aun/config.schema.json +1 -0
  47. package/plugins/aun/dist/src-ts/plugins/aun/index.js +52 -7
  48. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  49. package/plugins/aun/package.json +1 -1
  50. package/plugins/aun/plugin.json +1 -1
  51. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +31 -2
  52. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +37 -1
  53. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/session-store.js +6 -2
  54. package/plugins/chatgpt/package.json +1 -1
  55. package/plugins/chatgpt/plugin.json +1 -1
  56. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +13 -2
  57. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +29 -3
  58. package/plugins/dsh/dist/src-ts/dsh-agent.js +4 -2
  59. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +1 -0
  60. package/plugins/dsh/dist/src-ts/plugins/qwenwork/openai-adapter.js +12 -2
  61. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +35 -10
  62. package/plugins/feishu/dist/src-ts/plugins/feishu/public/app.js +20 -8
  63. package/plugins/feishu/dist/src-ts/plugins/feishu/public/index.html +1 -1
  64. package/plugins/qwenwork/config.schema.json +3 -0
  65. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +13 -2
  66. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +105 -31
  67. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +200 -7
  68. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/openai-adapter.js +12 -2
  69. package/plugins/qwenwork/package.json +1 -1
  70. package/plugins/qwenwork/plugin.json +2 -2
  71. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +10 -4
  72. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +139 -7
  73. package/plugins/workbuddy/plugin.json +2 -2
package/README.md CHANGED
@@ -141,4 +141,20 @@ Windows 路径时,可设置 `DSH_INSTALL_DIR`。
141
141
 
142
142
  ## AUN 身份管理
143
143
 
144
- 创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md)。
144
+ 创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md);给 Codex、Claude Code 直接执行的 CLI 说明见 [使用说明](使用说明.md)。
145
+
146
+ ### 飞书凭据与办公 Agent 关联
147
+
148
+ 在 ABWeb 的“飞书通道 → 手动配置凭据”中填写 App ID、App Secret,并选择“关联办公 Agent”。Secret 使用密码框显示;已保存的密钥保持掩码,不通过状态接口返回。WorkBuddy、千问办公、豆包根据运行后台的机器上是否存在可执行文件检测,文件缺失或插件未就绪的选项置灰。自定义安装位置请在对应插件配置中填写 `executablePath`。
149
+
150
+ 也可以使用 CLI,Secret 在命令执行后隐藏输入,不写在命令参数中:
151
+
152
+ ```bash
153
+ mab feishu configure cli_你的应用ID --agent workbuddy
154
+ mab feishu agents
155
+ mab feishu list
156
+ mab feishu bind cli_你的应用ID workbuddy
157
+ mab feishu unbind cli_你的应用ID
158
+ ```
159
+
160
+ `configure` 保存凭据及可选关联后,需要在 ABWeb 重载飞书插件以连接机器人。非交互终端请使用网页密码框;不要把 Secret 写进命令行、脚本或仓库。
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentAvailability = agentAvailability;
4
+ const desktop_launch_1 = require("./plugins/desktop-launch");
5
+ const workbuddy_1 = require("./plugins/workbuddy");
6
+ async function agentAvailability(id, config, status) {
7
+ const configured = typeof config.executablePath === "string" ? config.executablePath : undefined;
8
+ const executable = id === "workbuddy" ? (0, workbuddy_1.findExecutable)(configured)
9
+ : id === "qwenwork" ? await (0, desktop_launch_1.findWindowsExecutable)("QwenWorkCN.exe", configured)
10
+ : id === "doubao" ? await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", configured) : true;
11
+ if (!executable)
12
+ return { available: false, reason: "未找到可执行文件" };
13
+ return { available: status === "ready", reason: status === "ready" ? "" : "插件未就绪" };
14
+ }
@@ -4,6 +4,7 @@ exports.ChannelSessions = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const promises_1 = require("node:fs/promises");
6
6
  const node_path_1 = require("node:path");
7
+ const unsupportedCommands = /^(?:\/(?:s|session|slist|name|rename|del|status|stop|pause|resume|compact|fork|rewind|rw|repair|renew))(?:[ \t]+[^\r\n]*)?$/;
7
8
  class ChannelSessions {
8
9
  directory;
9
10
  sessions = {};
@@ -21,7 +22,7 @@ class ChannelSessions {
21
22
  throw new Error("通道会话文件格式无效");
22
23
  for (const [key, value] of Object.entries(data)) {
23
24
  const entry = value;
24
- if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string")
25
+ if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string" || (entry.newSession !== undefined && typeof entry.newSession !== "boolean"))
25
26
  throw new Error("通道会话记录无效");
26
27
  this.sessions[key] = entry;
27
28
  }
@@ -34,15 +35,37 @@ class ChannelSessions {
34
35
  await this.loaded;
35
36
  }
36
37
  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
+ async run(message, agentPlugin, call, control) {
38
39
  const source = JSON.stringify([message.channel, message.recipientId || "", message.chatId || "", message.peerId, message.threadId || ""]);
40
+ const command = message.text?.trim().match(/^\/new(?:[ \t]+([^\r\n]+))?$/);
41
+ const unsupported = message.text?.trim().match(unsupportedCommands);
42
+ if (unsupported)
43
+ return { text: `会话命令 ${unsupported[0].split(/[ \t]/, 1)[0]} 本期暂不支持` };
44
+ if (/^\/clear(?:[ \t]+[^\r\n]*)?$/.test(message.text?.trim() ?? ""))
45
+ return { text: "/clear 已移除,请使用 /new 新建会话" };
46
+ if (message.text?.trim() === "/help")
47
+ return { text: [
48
+ control ? "/new [名称]:新建并切换会话" : "/new [名称]:当前 Agent 插件不支持新建会话",
49
+ "话题中不支持 /new;/renew 及其他会话命令当前暂不支持",
50
+ "/clear 已移除,请使用 /new",
51
+ ].join("\n") };
52
+ if (command && message.threadId)
53
+ return { text: "话题中不支持 /new,请使用 /renew 轮换话题会话(当前尚未支持)" };
54
+ if (command && !control)
55
+ return { text: "当前 Agent 插件不支持新建会话" };
56
+ if (command && this.pending.has(source))
57
+ throw new Error("当前会话正在执行任务,请等待完成后再新建会话");
39
58
  const previous = this.pending.get(source) ?? Promise.resolve();
40
59
  const turn = previous.catch(() => { }).then(async () => {
41
60
  await this.load();
42
61
  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() };
62
+ const sessionId = !command && entry?.agentPlugin === agentPlugin ? entry.sessionId : (0, node_crypto_1.randomUUID)();
63
+ const response = command
64
+ ? await control({ operation: "new", sessionId, ...(command[1]?.trim() ? { name: command[1].trim() } : {}) })
65
+ : await call(sessionId, !entry || entry.agentPlugin !== agentPlugin || entry.newSession === true);
66
+ if (command && (typeof response.sessionId !== "string" || !response.sessionId.trim()))
67
+ throw new Error("插件未返回有效的新会话标识,原会话绑定保持不变");
68
+ const next = { source, agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString(), ...(command ? { newSession: true } : {}) };
46
69
  const save = this.writes.catch(() => { }).then(async () => {
47
70
  const snapshot = { ...this.sessions, [source]: next };
48
71
  await (0, promises_1.mkdir)(this.directory, { recursive: true });
@@ -5,6 +5,7 @@ exports.resolveLocalControlHost = resolveLocalControlHost;
5
5
  exports.controlUrl = controlUrl;
6
6
  exports.runPluginCommand = runPluginCommand;
7
7
  exports.runCoreHelp = runCoreHelp;
8
+ const secret_input_1 = require("./secret-input");
8
9
  const node_http_1 = require("node:http");
9
10
  const MAX_BODY_BYTES = 1024 * 1024;
10
11
  const PLUGIN_ID = /^[a-z][a-z0-9-]{1,63}$/;
@@ -108,6 +109,7 @@ function commandManifest(value, plugin) {
108
109
  || !Array.isArray(value.commands) || value.commands.some((command) => !object(command)
109
110
  || typeof command.name !== "string" || !/^[a-z][a-z0-9-]*$/.test(command.name)
110
111
  || typeof command.summary !== "string" || !command.summary.trim() || typeof command.usage !== "string" || !command.usage.trim()
112
+ || (command.secretPrompt !== undefined && (typeof command.secretPrompt !== "string" || !command.secretPrompt.trim()))
111
113
  || (command.examples !== undefined && (!Array.isArray(command.examples) || command.examples.some((example) => typeof example !== "string"))))) {
112
114
  throw new CliError("插件命令清单协议无效(需要 protocolVersion: 1)");
113
115
  }
@@ -126,7 +128,7 @@ async function runPluginCommand(id, args, port, options = {}) {
126
128
  throw new CliError(`插件“${id}”未安装;请先到 ABWeb 安装插件`, 2);
127
129
  if (!plugin.capabilities.includes("cli"))
128
130
  throw new CliError(`插件“${plugin.displayName}”未提供 CLI 命令;请升级对应插件`, 2);
129
- if (plugin.status !== "ready")
131
+ if (!["ready", "degraded"].includes(plugin.status))
130
132
  throw new CliError(`插件“${plugin.displayName}”未就绪(${plugin.status})`, 2);
131
133
  const path = `/api/plugins/${id}/commands`;
132
134
  const response = await requestJson(port, path, options);
@@ -146,7 +148,9 @@ async function runPluginCommand(id, args, port, options = {}) {
146
148
  ])].join("\n");
147
149
  return output({ ok: true, message, data: { ...manifest, commands: selected } }, 0, json);
148
150
  }
149
- const executed = await requestJson(port, path, options, { protocolVersion: 1, argv });
151
+ const prompt = manifest.commands.find((command) => command.name === argv[0])?.secretPrompt;
152
+ const secret = prompt ? await (options.readSecret ?? secret_input_1.readSecret)(prompt) : undefined;
153
+ const executed = await requestJson(port, path, options, { protocolVersion: 1, argv, ...(secret === undefined ? {} : { secret }) });
150
154
  const result = executed.value;
151
155
  if (!object(result) || typeof result.ok !== "boolean" || typeof result.message !== "string"
152
156
  || (executed.status >= 200 && executed.status < 300 ? !result.ok : result.ok)
@@ -132,12 +132,21 @@ class ControlServer {
132
132
  return this.json(res, 200, { ok: true, token, expiresAt });
133
133
  }
134
134
  const authorization = req.headers.authorization;
135
- const authorized = authorization === `Bearer ${this.options.token}` || (authorization?.startsWith("Bearer ") && (this.pairedTokens.get((0, node_crypto_1.createHash)("sha256").update(authorization.slice(7)).digest("hex")) ?? 0) > Date.now());
135
+ const authorized = (Boolean(this.options.token) && authorization === `Bearer ${this.options.token}`) || (authorization?.startsWith("Bearer ") && (this.pairedTokens.get((0, node_crypto_1.createHash)("sha256").update(authorization.slice(7)).digest("hex")) ?? 0) > Date.now());
136
136
  if (this.options.token && !isLoopback(req.socket.remoteAddress) && !authorized && !(method === "GET" && url.pathname.startsWith("/abweb")))
137
137
  return this.json(res, 401, { error: "unauthorized" });
138
138
  if (parts[0] === "api" && parts[1] === "agents" && parts[2] && parts[3] === "v1") {
139
139
  const id = decodeURIComponent(parts[2]);
140
- const adapter = new openai_adapter_1.OpenAIAdapter(async (task) => (await this.options.manager.handle(id, { task })).text, { model: id, prefix: `/api/agents/${encodeURIComponent(id)}/v1`, onExchange: (direction, text, peer, aid) => this.options.manager.recordMessage(id, direction, text, peer, aid) });
140
+ if (req.headers["x-evolcore-session-id"] !== undefined && !authorized && !isLoopback(req.socket.remoteAddress))
141
+ return this.json(res, 401, { error: "会话身份仅允许已认证或本机调用方使用" });
142
+ const adapter = new openai_adapter_1.OpenAIAdapter(async (task, _signal, context) => {
143
+ if (!context)
144
+ return (await this.options.manager.handle(id, { task })).text;
145
+ // 客户端标识只在认证调用方或本机信任范围内生效,不能直接恢复其他入口的会话。
146
+ const caller = authorized ? (0, node_crypto_1.createHash)("sha256").update(authorization).digest("hex") : "loopback";
147
+ const sessionId = `openai:${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify([caller, id, context.agentAid ?? "", context.peerId ?? "", context.sessionId])).digest("hex")}`;
148
+ return (await this.options.manager.handle(id, { task, sessionId })).text;
149
+ }, { model: id, prefix: `/api/agents/${encodeURIComponent(id)}/v1`, onExchange: (direction, text, peer, aid) => this.options.manager.recordMessage(id, direction, text, peer, aid) });
141
150
  await adapter.handle(req, res);
142
151
  return;
143
152
  }
@@ -212,6 +221,8 @@ class ControlServer {
212
221
  const result = await identity.create(body.aid, body.ownerAid);
213
222
  return this.json(res, result.status === "等待 Owner 确认" ? 202 : result.status === "existing" ? 200 : 201, result);
214
223
  }
224
+ if (method === "GET" && url.pathname === "/api/agents/availability")
225
+ return this.json(res, 200, { agents: await this.options.manager.availableAgents() });
215
226
  if (method === "GET" && url.pathname === "/api/plugins")
216
227
  return this.json(res, 200, { ok: true, plugins: this.options.manager.list() });
217
228
  if (method === "GET" && url.pathname === "/api/plugins/catalog")
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isDaemonCommand = isDaemonCommand;
4
4
  /** 只识别直接运行本安装 CLI 的后台进程,避免旧地址文件中的 PID 被其他程序复用。 */
5
5
  function isDaemonCommand(commandLine, cli) {
6
- const args = [...commandLine.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => (match[1] ?? match[2]).replaceAll("\\", "/").toLowerCase());
7
- return args[1] === cli.replaceAll("\\", "/").toLowerCase() && ["start", "--serve"].includes(args[2]);
6
+ const normalize = (value) => value.replace(/[\\/]+/g, "/").toLowerCase();
7
+ const args = [...commandLine.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => normalize(match[1] ?? match[2]));
8
+ return args[1] === normalize(cli) && ["start", "--serve"].includes(args[2]);
8
9
  }
@@ -18,7 +18,7 @@ class DshAgent {
18
18
  if (!request.task.trim())
19
19
  throw new agent_1.AgentError("DSH task must not be empty");
20
20
  const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
21
- if (request.sessionId)
21
+ if (request.sessionId && !request.newSession)
22
22
  args.push("--session-id", request.sessionId);
23
23
  args.push("-");
24
24
  const child = this.spawnProcess(this.paths.executable, args, {
@@ -29,7 +29,7 @@ class DshAgent {
29
29
  child.stdin.end(request.task, "utf8");
30
30
  let output = "";
31
31
  let errors = "";
32
- let sessionId = request.sessionId;
32
+ let sessionId = request.newSession ? undefined : request.sessionId;
33
33
  let finalText = "";
34
34
  child.stdout.setEncoding?.("utf8");
35
35
  child.stderr.setEncoding?.("utf8");
@@ -64,6 +64,8 @@ class DshAgent {
64
64
  });
65
65
  if (!finalText)
66
66
  throw new agent_1.AgentError("DSH returned no final text");
67
+ if (request.newSession && !sessionId)
68
+ throw new agent_1.AgentError("DSH 未返回真实新会话标识,无法确认后续续聊目标");
67
69
  return { text: finalText, sessionId };
68
70
  }
69
71
  }
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PluginManager = void 0;
7
+ const agent_availability_1 = require("./agent-availability");
7
8
  const promises_1 = require("node:fs/promises");
8
9
  const node_fs_1 = require("node:fs");
9
10
  const node_module_1 = require("node:module");
@@ -232,6 +233,8 @@ class PluginManager {
232
233
  get(id) { return this.plugins.get(id); }
233
234
  recentMessages(id) { return this.messages.filter((item) => !id || item.pluginId === id).slice(-200).reverse(); }
234
235
  recordMessage(pluginId, direction, text, peer, agentAid) {
236
+ if (direction === "error" && isNoisyAgentError(text))
237
+ return;
235
238
  this.messages.push({ pluginId, direction, text: text.slice(0, 10000), time: new Date().toISOString(), ...(peer ? { peer } : {}), ...(agentAid ? { agentAid } : {}) });
236
239
  if (this.messages.length > 200)
237
240
  this.messages.shift();
@@ -357,7 +360,11 @@ class PluginManager {
357
360
  const target = message.agentPlugin || "qwenwork";
358
361
  this.recordMessage(target, "request", message.text, message.peerId);
359
362
  try {
360
- const response = await this.sessions.run(message, target, (sessionId) => this.handle(target, { task: message.text, sessionId }));
363
+ const instance = this.plugins.get(target)?.instance;
364
+ const control = instance?.sessionOperations?.includes("new")
365
+ ? (request) => this.controlSession(target, request)
366
+ : undefined;
367
+ const response = await this.sessions.run(message, target, (sessionId, newSession) => this.handle(target, { task: message.text, sessionId, ...(newSession ? { newSession: true } : {}) }), control);
361
368
  await message.reply(response.text);
362
369
  this.recordMessage(target, "reply", response.text, message.peerId);
363
370
  }
@@ -503,6 +510,12 @@ class PluginManager {
503
510
  throw error;
504
511
  throw new agent_1.AgentError(`插件配置校验失败:${error instanceof Error ? error.message : String(error)}`);
505
512
  } }
513
+ async availableAgents() {
514
+ return Promise.all(this.list().filter((plugin) => plugin.type === "agent").map(async (plugin) => ({
515
+ id: plugin.id, displayName: plugin.displayName, status: plugin.status,
516
+ ...await (0, agent_availability_1.agentAvailability)(plugin.id, await this.config(plugin.id), plugin.status),
517
+ })));
518
+ }
506
519
  async health(id) { const loaded = this.plugins.get(id); if (!loaded)
507
520
  throw new agent_1.AgentError(`unknown plugin: ${id}`); if (!loaded.instance)
508
521
  return { status: loaded.record.status }; return loaded.instance.health(); }
@@ -517,6 +530,19 @@ class PluginManager {
517
530
  }
518
531
  return loaded.instance.handle(request);
519
532
  }
533
+ async controlSession(id, request) {
534
+ const loaded = this.plugins.get(id);
535
+ if (!loaded?.instance || loaded.record.status !== "ready")
536
+ throw new agent_1.AgentError(`插件 ${id} 未就绪,无法控制会话`);
537
+ if (!loaded.instance.sessionOperations?.includes(request.operation))
538
+ throw new agent_1.AgentError(`插件 ${id} 不支持新建会话`);
539
+ const response = loaded.instance.controlSession
540
+ ? await loaded.instance.controlSession(request)
541
+ : { text: `${loaded.record.displayName} 已创建新会话`, sessionId: request.sessionId };
542
+ if (typeof response.sessionId !== "string" || !response.sessionId.trim())
543
+ throw new agent_1.AgentError("插件未返回有效的新会话标识,原会话绑定保持不变");
544
+ return response;
545
+ }
520
546
  async dispatchHttp(request, response) {
521
547
  const method = (request.method ?? "GET").toUpperCase();
522
548
  const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
@@ -587,10 +613,13 @@ class PluginManager {
587
613
  const packagedFile = (0, node_path_1.join)(record.path, record.manifest.entry);
588
614
  const developmentFile = (0, node_path_1.resolve)(__dirname, "plugins", record.id, (0, node_path_1.basename)(record.manifest.entry));
589
615
  const file = (0, node_fs_1.existsSync)(packagedFile) ? packagedFile : developmentFile;
590
- // CommonJS is the platform's compiled format; evict the module cache for reload.
616
+ // 同时刷新插件内部依赖,避免升级后仍运行旧版 CDP 客户端。
591
617
  const req = (0, node_module_1.createRequire)(file);
592
618
  const resolved = req.resolve(file);
593
- delete req.cache[resolved];
619
+ const moduleRoot = (0, node_fs_1.existsSync)(packagedFile) ? (0, node_path_1.resolve)(record.path) : (0, node_path_1.dirname)(file);
620
+ for (const cached of Object.keys(req.cache))
621
+ if (cached === resolved || cached.startsWith(`${moduleRoot}${node_path_1.sep}`))
622
+ delete req.cache[cached];
594
623
  const mod = req(resolved);
595
624
  let value = mod.default ?? mod.plugin ?? mod;
596
625
  // Entries may export a ready instance, a factory, or a class. Supporting all
@@ -819,6 +848,10 @@ class PluginManager {
819
848
  } }
820
849
  }
821
850
  exports.PluginManager = PluginManager;
851
+ function isNoisyAgentError(text) {
852
+ const value = text.trim();
853
+ return /^(?:千问生成接口不可用(?:,已触发后台恢复)?|千问办公 UI 超时未返回回复|(?:API|接口)\s*(?:暂时)?不可用[。.!!]?)$/i.test(value);
854
+ }
822
855
  function isUpdatePolicy(value) { return Boolean(value && typeof value === "object" && ["auto-install", "download-only", "manual"].includes(value.mode)); }
823
856
  function pluginProcessName(id) {
824
857
  return { qwenwork: "QwenWorkCN.exe", doubao: "Doubao.exe", workbuddy: "WorkBuddy.exe", chatgpt: "ChatGPT/Codex" }[id];
@@ -10,12 +10,13 @@ const node_os_1 = require("node:os");
10
10
  const node_url_1 = require("node:url");
11
11
  const messages_1 = require("./messages");
12
12
  const manifest = {
13
- id: "aun", version: "1.0.21", type: "channel", displayName: "AUN 通道",
13
+ id: "aun", version: "1.0.22", type: "channel", displayName: "AUN 通道",
14
14
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
15
15
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
16
16
  configSchema: "config.schema.json",
17
17
  managementPage: "/abweb/channels/aun",
18
18
  };
19
+ const newSessionCommand = /^\/new(?:[ \t]+[^\r\n]+)?$/;
19
20
  async function loadSdk() {
20
21
  const resolver = (0, node_module_1.createRequire)(__filename);
21
22
  const dynamicImport = new Function("url", "return import(url)");
@@ -36,7 +37,8 @@ const cliCommands = [
36
37
  { name: "status", summary: "查看 AUN 通道状态", usage: "mab aun status" },
37
38
  { name: "list", summary: "查看已连接 AID 和 Agent 关联", usage: "mab aun list" },
38
39
  { name: "agents", summary: "列出当前已就绪的 Agent 插件", usage: "mab aun agents" },
39
- { name: "create", summary: "创建并连接 AID,可选关联 Agent", usage: "mab aun create <aid> --owner <owner-aid> [--agent <plugin-id>]", examples: ["mab aun create new.agentid.cn --owner owner.agentid.cn --agent qwenwork"] },
40
+ { name: "default-owner", summary: "查询或设置默认 Owner AID", usage: "mab aun default-owner [set <owner-aid>]", examples: ["mab aun default-owner", "mab aun default-owner set owner.agentid.cn"] },
41
+ { name: "create", summary: "创建并连接 AID,可选关联 Agent", usage: "mab aun create <aid> [--owner <owner-aid>] [--agent <plugin-id>]", examples: ["mab aun create new.agentid.cn --owner owner.agentid.cn --agent qwenwork"] },
40
42
  { name: "bind", summary: "将 AID 关联到 Agent", usage: "mab aun bind <aid> <plugin-id>", examples: ["mab aun bind new.agentid.cn qwenwork"] },
41
43
  { name: "unbind", summary: "解除 Agent 与 AID 的关联", usage: "mab aun unbind <plugin-id>", examples: ["mab aun unbind qwenwork"] },
42
44
  ];
@@ -58,6 +60,7 @@ class AunPlugin {
58
60
  clients = new Map();
59
61
  identities = new Set();
60
62
  legacyCreatedAids = new Set();
63
+ createdAids = new Set();
61
64
  bindings = {};
62
65
  bindingTimes = {};
63
66
  creating = false;
@@ -75,10 +78,11 @@ class AunPlugin {
75
78
  }
76
79
  identity = {
77
80
  list: async () => {
81
+ const own = new Set([...this.createdAids, ...this.legacyCreatedAids]);
78
82
  return {
79
- evolcore: [],
80
- muAgentBridge: [...this.clients.keys()].sort(),
81
- localMuAgentBridge: [...this.identities].filter((aid) => !this.clients.has(aid)).sort(),
83
+ evolcore: [...this.clients.keys()].filter((aid) => !own.has(aid)).sort(),
84
+ muAgentBridge: [...this.clients.keys()].filter((aid) => own.has(aid)).sort(),
85
+ localMuAgentBridge: [...own].filter((aid) => !this.clients.has(aid)).sort(),
82
86
  };
83
87
  },
84
88
  create: async (aid, owner) => {
@@ -95,6 +99,8 @@ class AunPlugin {
95
99
  const result = await this.store.register(aid);
96
100
  if (!result.ok)
97
101
  throw new Error(redact(result.error?.message ?? result.error?.code ?? "AID 注册失败"));
102
+ this.createdAids.add(aid);
103
+ await this.persist();
98
104
  }
99
105
  const loaded = this.store.load(aid);
100
106
  if (!loaded.ok || !loaded.data)
@@ -180,6 +186,7 @@ class AunPlugin {
180
186
  };
181
187
  async persist() {
182
188
  this.config.identities = [...this.identities].sort();
189
+ this.config.created_aids = [...new Set([...this.createdAids, ...this.legacyCreatedAids])].sort();
183
190
  this.config.bindings = this.bindings;
184
191
  this.config.binding_times = this.bindingTimes;
185
192
  await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 });
@@ -188,6 +195,7 @@ class AunPlugin {
188
195
  this.context = context;
189
196
  this.config = { ...context.config };
190
197
  this.legacyCreatedAids.clear();
198
+ this.createdAids.clear();
191
199
  try {
192
200
  const created = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(context.dataDir, "..", "created-aids.json"), "utf8"));
193
201
  if (Array.isArray(created))
@@ -199,6 +207,10 @@ class AunPlugin {
199
207
  if (error.code !== "ENOENT")
200
208
  context.logger.warn("读取旧版身份创建记录失败", { error: safeError(error) });
201
209
  }
210
+ for (const aid of Array.isArray(this.config.created_aids) ? this.config.created_aids : []) {
211
+ if (typeof aid === "string" && aidPattern.test(aid))
212
+ this.createdAids.add(aid);
213
+ }
202
214
  for (const key of ["aid", "owner_aid", "aun_path", "encryption_seed", "root_ca_path", "allow_from", "agent_plugin"]) {
203
215
  if (this.config[key] !== undefined && typeof this.config[key] !== "string")
204
216
  throw new Error(`${key} 必须是字符串`);
@@ -487,6 +499,8 @@ class AunPlugin {
487
499
  const agents = await this.agents();
488
500
  return { message: agents.length ? `当前已就绪的 Agent 插件:\n${agents.map((agent) => `${agent.id}(${agent.displayName})`).join("\n")}` : "暂无已就绪的 Agent 插件", data: agents };
489
501
  }
502
+ if (command === "default-owner")
503
+ return this.defaultOwnerCommand(args);
490
504
  if (command === "create")
491
505
  return this.createCommand(args);
492
506
  if (command === "bind" && args.length === 2) {
@@ -522,10 +536,29 @@ class AunPlugin {
522
536
  }
523
537
  return { message: `已解除 ${pluginId} 的 AID 关联`, data: { agent: pluginId, unboundAids } };
524
538
  }
525
- if (!["status", "list", "agents", "create", "bind", "unbind"].includes(command ?? ""))
539
+ if (!["status", "list", "agents", "default-owner", "create", "bind", "unbind"].includes(command ?? ""))
526
540
  throw new AunCommandError(`未知 AUN 命令:${command ?? ""}`);
527
541
  throw new AunCommandError(`AUN 命令参数无效:${command}`);
528
542
  }
543
+ async defaultOwnerCommand(args) {
544
+ const owner = typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
545
+ if (!args.length)
546
+ return { message: owner ? `默认 Owner:${owner}` : "未配置默认 Owner", data: { owner_aid: owner || undefined } };
547
+ if (args.length !== 2 || args[0] !== "set")
548
+ throw new AunCommandError("AUN 默认 Owner 命令参数无效");
549
+ const nextOwner = args[1].trim();
550
+ if (!aidPattern.test(nextOwner))
551
+ throw new AunCommandError("Owner AID 格式无效");
552
+ this.config.owner_aid = nextOwner;
553
+ try {
554
+ await this.persist();
555
+ }
556
+ catch (error) {
557
+ this.config.owner_aid = owner;
558
+ throw error;
559
+ }
560
+ return { message: "默认 Owner 已保存", data: { configured: true } };
561
+ }
529
562
  async createCommand(args) {
530
563
  const aid = args[0] ?? "";
531
564
  if (!aidPattern.test(aid))
@@ -544,8 +577,10 @@ class AunPlugin {
544
577
  else
545
578
  throw new AunCommandError(`不支持的创建参数:${option}`);
546
579
  }
580
+ if (!owner)
581
+ owner = typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
547
582
  if (!owner || !aidPattern.test(owner))
548
- throw new AunCommandError("需要有效的 --owner AID");
583
+ throw new AunCommandError("需要有效的 --owner AID,或先设置默认 Owner");
549
584
  if (agent) {
550
585
  this.validatePluginId(agent);
551
586
  await this.requireAgent(agent);
@@ -703,12 +738,14 @@ class AunPlugin {
703
738
  const message = (0, messages_1.normalizeAunMessage)(payload, aid, typeof this.config.allow_from === "string" ? this.config.allow_from : undefined);
704
739
  if (!message || !this.seen.add(message.messageId ? `${aid}:${message.messageId}` : ""))
705
740
  return;
741
+ let replyAttempted = false;
706
742
  const channelMessage = {
707
743
  channel: "aun", recipientId: aid, peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
708
744
  agentPlugin: this.bindings[aid] || (typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork"),
709
745
  reply: async (text) => {
710
746
  if (!text.trim())
711
747
  return;
748
+ replyAttempted = true;
712
749
  const payload = { type: "text", text };
713
750
  if (message.threadId)
714
751
  payload.thread_id = message.threadId;
@@ -728,6 +765,14 @@ class AunPlugin {
728
765
  }
729
766
  catch (error) {
730
767
  this.context?.logger.warn("AUN 消息处理失败", { error: safeError(error) });
768
+ if (!replyAttempted && newSessionCommand.test(message.text.trim())) {
769
+ try {
770
+ await channelMessage.reply(`新建会话失败:${safeError(error)}`);
771
+ }
772
+ catch (replyError) {
773
+ this.context?.logger.warn("AUN 错误回复失败", { error: safeError(replyError) });
774
+ }
775
+ }
731
776
  }
732
777
  }
733
778
  isE2eeError(error) {
@@ -6,7 +6,7 @@
6
6
  <header><h1>AUN 身份</h1><button id="refresh" type="button">刷新</button></header>
7
7
  <p id="status" role="status"></p>
8
8
  <div class="table-scroll"><table><thead><tr><th>AID</th><th>来源</th><th>已关联 Agent</th><th>操作</th></tr></thead><tbody id="identities"></tbody></table></div>
9
- <form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="例如:example.agentid.cn"></label><label>Owner AID<input name="ownerAid" required placeholder="例如:owner.agentid.cn(本地归属配置)"><small class="hint">Owner 保存为本地归属配置;不代表 AUN 协议中的 Owner 关系已激活。</small></label></div><button type="submit">创建</button></form>
9
+ <form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="例如:example.agentid.cn"></label><label>Owner AID<input name="ownerAid" placeholder="例如:owner.agentid.cn"><small class="hint">留空时使用已配置的默认 Owner;未配置时需填写。Owner 保存为本地归属配置;不代表 AUN 协议中的 Owner 关系已激活。</small></label></div><button type="submit">创建</button></form>
10
10
  <form id="config-form"><h2>通道配置</h2><div class="fields"><label>默认 Agent<select name="agent_plugin"></select></label><label>允许的来源<input name="allow_from"></label><label>AUN 存储路径<input name="aun_path"></label><label>加密种子<input name="encryption_seed" type="password" autocomplete="new-password" placeholder="留空则不修改"></label></div><button type="submit">保存配置</button></form>
11
11
  </main>
12
12
  <script src="/abweb/channels/aun/app.js"></script>
@@ -13,6 +13,7 @@ const sendButtonSelector = 'button[aria-label="Send"], button[aria-label="发送
13
13
  const stopButtonSelector = 'button[aria-label="Stop"], button[aria-label="停止"]';
14
14
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
15
  const sidebarSelector = "[data-app-action-sidebar-thread-id]";
16
+ const newSessionSelector = 'button,[role="button"]';
16
17
  class ChatgptCdpClient {
17
18
  socket;
18
19
  tunnel;
@@ -45,7 +46,7 @@ class ChatgptCdpClient {
45
46
  }
46
47
  async activeSession() {
47
48
  const entry = await this.evaluate(`(() => {
48
- const item = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
49
+ const item = document.querySelector('[data-app-action-sidebar-thread-selected="true"], [data-app-action-sidebar-thread-active="true"]');
49
50
  return item ? {
50
51
  id: item.getAttribute('data-app-action-sidebar-thread-id'),
51
52
  host: item.getAttribute('data-app-action-sidebar-thread-host-id'),
@@ -72,10 +73,38 @@ class ChatgptCdpClient {
72
73
  throw new Error("ChatGPT 侧栏中找不到目标会话");
73
74
  await this.waitFor(`(() => {
74
75
  const item = ${locate};
75
- return item?.getAttribute('data-app-action-sidebar-thread-selected') === 'true'
76
+ return (item?.getAttribute('data-app-action-sidebar-thread-selected') === 'true' || item?.getAttribute('data-app-action-sidebar-thread-active') === 'true')
76
77
  && Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}));
77
78
  })()`, "ChatGPT 会话切换未确认,未发送消息");
78
79
  }
80
+ async createSession(_name) {
81
+ const before = await this.activeSession();
82
+ const state = await this.evaluate(`JSON.stringify({ editor: document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText ?? null, turns: document.querySelectorAll(${JSON.stringify(exports.turnSelector)}).length, generating: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) })`);
83
+ const initial = JSON.parse(String(state));
84
+ if (initial.editor?.trim() || initial.generating)
85
+ throw new Error("ChatGPT 当前有未发送内容或正在生成回复,不能新建会话");
86
+ const clicked = await this.evaluate(`(() => {
87
+ const labels = /^(?:新聊天|新对话|new chat|new conversation)$/i;
88
+ const item = [...document.querySelectorAll(${JSON.stringify(newSessionSelector)})].find((node) => {
89
+ const label = [node.getAttribute('aria-label'), node.getAttribute('title'), node.textContent].filter(Boolean).join(' ').trim();
90
+ return labels.test(label);
91
+ });
92
+ if (!item || item.getAttribute('disabled') !== null || item.getAttribute('aria-disabled') === 'true') return false;
93
+ item.click();
94
+ return true;
95
+ })()`);
96
+ if (!clicked)
97
+ throw new Error("ChatGPT 新聊天入口未找到或不可用");
98
+ await this.waitFor(`(() => {
99
+ const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"], [data-app-action-sidebar-thread-active="true"]');
100
+ const id = selected?.getAttribute('data-app-action-sidebar-thread-id');
101
+ const beforeId = ${JSON.stringify(before?.id ?? null)};
102
+ const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
103
+ const turns = document.querySelectorAll(${JSON.stringify(exports.turnSelector)}).length;
104
+ return Boolean(editor && !editor.innerText.trim() && turns === 0 && (!beforeId || id !== beforeId));
105
+ })()`, "ChatGPT 新会话状态校验失败");
106
+ return this.activeSession();
107
+ }
79
108
  async connect() {
80
109
  if (this.socket)
81
110
  return;
@@ -5,7 +5,7 @@ const cdp_client_1 = require("./cdp-client");
5
5
  const session_store_1 = require("./session-store");
6
6
  exports.chatgptManifest = {
7
7
  id: "chatgpt",
8
- version: "1.0.1",
8
+ version: "1.0.3",
9
9
  displayName: "ChatGPT(Codex)",
10
10
  type: "agent",
11
11
  entry: "dist/index.js",
@@ -22,6 +22,7 @@ function port(value, fallback) {
22
22
  class ChatgptPlugin {
23
23
  options;
24
24
  manifest = exports.chatgptManifest;
25
+ sessionOperations = [];
25
26
  client;
26
27
  failure;
27
28
  remote = false;
@@ -62,6 +63,7 @@ class ChatgptPlugin {
62
63
  timeoutMs: typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 30_000,
63
64
  ssh,
64
65
  });
66
+ this.sessionOperations = this.client.createSession ? ["new"] : [];
65
67
  try {
66
68
  await this.client.connect();
67
69
  }
@@ -93,6 +95,31 @@ class ChatgptPlugin {
93
95
  this.queue = run.then(() => undefined, () => undefined);
94
96
  return { text: await run, sessionId: request.sessionId };
95
97
  }
98
+ async controlSession(request) {
99
+ if (request.operation !== "new")
100
+ throw new Error(`ChatGPT 不支持会话操作:${String(request.operation)}`);
101
+ const sessionId = request.sessionId?.trim();
102
+ if (!sessionId)
103
+ throw new Error("ChatGPT 新会话缺少 sessionId");
104
+ if (!this.client?.createSession || !this.client.listSessions || !this.store)
105
+ throw new Error("ChatGPT 当前传输不支持新建会话");
106
+ const run = this.queue.then(async () => {
107
+ const existing = new Set((await this.client.listSessions()).map(item => JSON.stringify([item.host, item.id])));
108
+ const created = await this.client.createSession(request.name);
109
+ if (created) {
110
+ this.store.remember(created, "", "");
111
+ this.store.bindings[sessionId] = created;
112
+ }
113
+ else {
114
+ delete this.store.bindings[sessionId];
115
+ this.store.pendingBindings[sessionId] = [...existing];
116
+ }
117
+ this.persist();
118
+ return { text: "ChatGPT 已创建新会话", sessionId };
119
+ });
120
+ this.queue = run.then(() => undefined, () => undefined);
121
+ return run;
122
+ }
96
123
  persist() {
97
124
  void this.store.save().catch(error => this.logger?.warn("ChatGPT 会话缓存保存失败", { error: String(error) }));
98
125
  }
@@ -177,6 +204,15 @@ class ChatgptPlugin {
177
204
  selected ??= await this.client.activeSession?.();
178
205
  const message = explicit?.[2]?.trim() || request.task;
179
206
  const reply = await this.client.direct(message, request.timeoutMs);
207
+ const baseline = request.sessionId ? store.pendingBindings[request.sessionId] : undefined;
208
+ if (baseline) {
209
+ const existing = new Set(baseline);
210
+ const created = (await this.client.listSessions?.() ?? []).filter(item => !existing.has(JSON.stringify([item.host, item.id])));
211
+ if (created.length !== 1)
212
+ throw new Error(`ChatGPT 新消息已发送,但新会话标识无法唯一确认(找到 ${created.length} 个)`);
213
+ selected = created[0];
214
+ delete store.pendingBindings[request.sessionId];
215
+ }
180
216
  if (selected) {
181
217
  const current = await this.client.activeSession?.().catch(() => undefined);
182
218
  const entry = current?.id === selected.id && current.host === selected.host ? current : selected;