mu-agent-bridge 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,7 +59,7 @@ abweb 的“配置码”栏;配对码 10 分钟有效,只能使用一次,
59
59
  abweb 插件目录从 npm 搜索带 `mu-agent-bridge-plugin` 关键词的包,按包元数据中的
60
60
  `agentBridge.type` 展示 Agent、Channel 和管理插件;安装后以 `plugin.json` 的类型和能力
61
61
  再次校验,不根据包名推断类型。搜索结果有收录延迟,可在页面直接输入 `agent-bridge-*`
62
- 包名安装。已安装插件按清单能力兼容旧版分类;AUN Channel 仍需单独实现并发布后才会出现。
62
+ 包名安装。已安装插件按清单能力兼容旧版分类。AUN 通道插件包名为 `agent-bridge-aun`,安装后配置 AID 并启动即可;来信默认交给千问办公,也可通过 `agent_plugin` 指定其他已安装 Agent。
63
63
 
64
64
  插件仍在 daemon 的同一个 Node.js 进程内运行。修改插件入口依赖后应重启 daemon,避免模块缓存
65
65
  影响新代码。
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -32,5 +32,6 @@ async function searchPluginCatalog(query = "", get = getJson) {
32
32
  return;
33
33
  }
34
34
  }));
35
- return items.filter((item) => Boolean(item));
35
+ const needle = query.trim().toLowerCase();
36
+ return items.filter((item) => Boolean(item)).filter((item) => !needle || `${item.name} ${item.description}`.toLowerCase().includes(needle));
36
37
  }
@@ -199,7 +199,25 @@ class PluginManager {
199
199
  warn: (message, fields) => (this.options.logger ?? logger).warn(message, fields),
200
200
  error: (message, fields) => (this.options.logger ?? logger).error(message, fields),
201
201
  };
202
- await instance.activate({ dataDir, config, logger: pluginLogger, http: this.createHttpRegistry(id) });
202
+ await instance.activate({
203
+ dataDir,
204
+ config,
205
+ logger: pluginLogger,
206
+ http: this.createHttpRegistry(id),
207
+ callPlugin: async (targetId, request) => {
208
+ if (targetId === id)
209
+ throw new agent_1.AgentError(`插件不能调用自身: ${id}`);
210
+ return this.handle(targetId, request);
211
+ },
212
+ pluginStatus: (targetId) => this.plugins.get(targetId)?.record.status,
213
+ ingress: {
214
+ publish: async (message) => {
215
+ const target = message.agentPlugin || "qwenwork";
216
+ const response = await this.handle(target, { task: message.text, sessionId: `${message.channel}:${message.peerId}` });
217
+ await message.reply(response.text);
218
+ },
219
+ },
220
+ });
203
221
  loaded.instance = instance;
204
222
  loaded.record.status = "ready";
205
223
  }
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AunPlugin = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_module_1 = require("node:module");
6
+ const node_path_1 = require("node:path");
7
+ const node_url_1 = require("node:url");
8
+ const messages_1 = require("./messages");
9
+ const manifest = {
10
+ id: "aun", version: "1.0.0", type: "channel", displayName: "AUN 通道",
11
+ entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
12
+ capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health"],
13
+ configSchema: "config.schema.json",
14
+ };
15
+ async function loadSdk() {
16
+ const resolver = (0, node_module_1.createRequire)(__filename);
17
+ const dynamicImport = new Function("url", "return import(url)");
18
+ for (const modules of resolver.resolve.paths("@agentunion/fastaun") ?? []) {
19
+ const packageRoot = (0, node_path_1.join)(modules, "@agentunion", "fastaun");
20
+ try {
21
+ const metadata = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(packageRoot, "package.json"), "utf8"));
22
+ const entry = typeof metadata.exports?.["."] === "object" ? metadata.exports["."]?.import : metadata.exports?.["."];
23
+ return await dynamicImport((0, node_url_1.pathToFileURL)((0, node_path_1.join)(packageRoot, entry ?? metadata.main ?? "index.js")).href);
24
+ }
25
+ catch { /* 继续查找上一级 node_modules */ }
26
+ }
27
+ throw new Error("未找到 @agentunion/fastaun,请确认 AUN 插件依赖已安装");
28
+ }
29
+ const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
30
+ class AunPlugin {
31
+ manifest = manifest;
32
+ context;
33
+ sdk;
34
+ store;
35
+ client;
36
+ subscription;
37
+ unregister;
38
+ ingress;
39
+ aid = "";
40
+ config = {};
41
+ status = "degraded";
42
+ healthMessage = "尚未配置 AID";
43
+ seen = new messages_1.DuplicateTracker();
44
+ constructor(options = {}) { this.sdk = options.sdk; this.ingress = options.ingress; }
45
+ async activate(context) {
46
+ this.context = context;
47
+ this.config = { ...context.config };
48
+ for (const key of ["aid", "owner_aid", "aun_path", "encryption_seed", "root_ca_path", "allow_from", "agent_plugin"]) {
49
+ if (this.config[key] !== undefined && typeof this.config[key] !== "string")
50
+ throw new Error(`${key} 必须是字符串`);
51
+ }
52
+ if (this.config.verify_ssl !== undefined && typeof this.config.verify_ssl !== "boolean")
53
+ throw new Error("verify_ssl 必须是布尔值");
54
+ if (this.config.delivery_mode !== undefined && !["fanout", "queue"].includes(String(this.config.delivery_mode)))
55
+ throw new Error("delivery_mode 只能是 fanout 或 queue");
56
+ if (this.config.aid && !aidPattern.test(String(this.config.aid)))
57
+ throw new Error("AID 格式无效");
58
+ if (this.config.owner_aid && !aidPattern.test(String(this.config.owner_aid)))
59
+ throw new Error("Owner AID 格式无效");
60
+ this.ingress ??= context.ingress;
61
+ if (!this.ingress)
62
+ throw new Error("运行时未提供通道消息入口");
63
+ this.sdk ??= await loadSdk();
64
+ await (0, promises_1.mkdir)(this.path(), { recursive: true });
65
+ this.store = this.newStore();
66
+ const disposer = context.http.register({ method: "POST", path: "/api/plugins/aun/aid", handler: this.createAid });
67
+ if (typeof disposer === "function")
68
+ this.unregister = disposer;
69
+ this.aid = typeof this.config.aid === "string" ? this.config.aid.trim() : "";
70
+ if (this.aid)
71
+ await this.connectAid(this.aid);
72
+ }
73
+ async connectAid(aid) {
74
+ try {
75
+ const loaded = this.store.load(aid);
76
+ if (!loaded.ok || !loaded.data)
77
+ throw new Error(loaded.error?.message ?? "本地密钥库中不存在该 AID");
78
+ this.client = new this.sdk.AUNClient(loaded.data.aid);
79
+ this.subscription = this.client.on("message.received", (payload) => this.onMessage(payload));
80
+ await this.client.connect();
81
+ this.status = "ready";
82
+ this.healthMessage = "已连接";
83
+ }
84
+ catch (error) {
85
+ this.status = "degraded";
86
+ this.healthMessage = safeError(error);
87
+ this.context?.logger.warn("AUN 通道连接失败", { error: this.healthMessage });
88
+ await this.closeClient();
89
+ }
90
+ }
91
+ async deactivate(_reason) {
92
+ this.status = "degraded";
93
+ this.subscription?.unsubscribe();
94
+ this.subscription = undefined;
95
+ await this.closeClient();
96
+ await this.store?.close();
97
+ this.store = undefined;
98
+ this.unregister?.();
99
+ this.unregister = undefined;
100
+ this.context?.http.unregister("aun");
101
+ this.context = undefined;
102
+ }
103
+ async health() {
104
+ return { status: this.status, message: this.healthMessage, details: { aid: this.aid || undefined, connected: this.status === "ready" } };
105
+ }
106
+ async handle(_request) { throw new Error("AUN 是入站通道插件"); }
107
+ path() {
108
+ const configured = this.config.aun_path;
109
+ return typeof configured === "string" && configured.trim() ? configured : (0, node_path_1.join)(this.context.dataDir, "aun");
110
+ }
111
+ newStore() {
112
+ return new this.sdk.AIDStore({ aunPath: this.path(), encryptionSeed: typeof this.config.encryption_seed === "string" ? this.config.encryption_seed : "", verifySsl: typeof this.config.verify_ssl === "boolean" ? this.config.verify_ssl : true, rootCaPath: this.config.root_ca_path });
113
+ }
114
+ createAid = async (request, response) => {
115
+ let input;
116
+ try {
117
+ let body = "";
118
+ for await (const chunk of request) {
119
+ body += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
120
+ if (body.length > 65536)
121
+ throw new Error("请求内容过大");
122
+ }
123
+ input = JSON.parse(body);
124
+ }
125
+ catch (error) {
126
+ return this.json(response, 400, { error: error instanceof Error && error.message === "请求内容过大" ? error.message : "JSON 请求格式无效" });
127
+ }
128
+ const aid = input && typeof input === "object" && typeof input.aid === "string" ? input.aid.trim() : "";
129
+ if (!aidPattern.test(aid))
130
+ return this.json(response, 400, { error: "AID 格式无效" });
131
+ if (this.aid && aid !== this.aid)
132
+ return this.json(response, 409, { error: "创建其他身份前请先停止当前 AID" });
133
+ if (aid === this.aid)
134
+ return this.json(response, 200, { aid, status: "existing", connection: this.status });
135
+ try {
136
+ const body = input;
137
+ const owner = typeof body.owner_aid === "string" && body.owner_aid.trim()
138
+ ? body.owner_aid.trim()
139
+ : typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
140
+ if (!owner)
141
+ return this.json(response, 400, { error: "创建 AID 时必须提供 Owner AID" });
142
+ if (!aidPattern.test(owner))
143
+ return this.json(response, 400, { error: "Owner AID 格式无效" });
144
+ const result = await this.store.register({ aid, type: "agent", owner });
145
+ if (!result.ok)
146
+ return this.json(response, 502, { error: result.error?.code ?? "注册失败", message: redact(result.error?.message ?? "AID 注册失败") });
147
+ if (result.data?.ownership_status !== "active")
148
+ return this.json(response, 202, { aid, owner, status: "等待 Owner 确认", operation_id: result.data?.operation_id });
149
+ this.config.aid = aid;
150
+ this.config.owner_aid = owner;
151
+ this.config.aun_path = this.path();
152
+ await (0, promises_1.mkdir)(this.context.dataDir, { recursive: true });
153
+ await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 });
154
+ this.aid = aid;
155
+ await this.connectAid(aid);
156
+ return this.json(response, 201, { aid, status: "已创建", connection: this.status });
157
+ }
158
+ catch (error) {
159
+ this.context?.logger.warn("AUN AID 注册失败", { error: safeError(error) });
160
+ return this.json(response, 502, { error: safeError(error) });
161
+ }
162
+ };
163
+ async onMessage(payload) {
164
+ const message = (0, messages_1.normalizeAunMessage)(payload, this.aid, typeof this.config.allow_from === "string" ? this.config.allow_from : undefined);
165
+ if (!message || !this.seen.add(message.messageId ?? ""))
166
+ return;
167
+ const channelMessage = {
168
+ channel: "aun", peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
169
+ agentPlugin: typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork",
170
+ reply: async (text) => {
171
+ if (!text.trim())
172
+ return;
173
+ const payload = { type: "text", text };
174
+ if (message.threadId)
175
+ payload.thread_id = message.threadId;
176
+ const params = { to: message.peerId, payload, encrypt: true };
177
+ try {
178
+ await this.client.call("message.send", params);
179
+ }
180
+ catch (error) {
181
+ if (!this.isE2eeError(error))
182
+ throw error;
183
+ await this.client.call("message.send", { ...params, encrypt: false });
184
+ }
185
+ },
186
+ };
187
+ try {
188
+ await this.ingress.publish(channelMessage);
189
+ }
190
+ catch (error) {
191
+ this.context?.logger.warn("AUN 消息处理失败", { error: safeError(error) });
192
+ }
193
+ }
194
+ isE2eeError(error) {
195
+ const sdk = this.sdk;
196
+ return [sdk.E2EEError, sdk.E2EEDecryptFailedError, sdk.E2EEDegradedError].some((type) => !!type && error instanceof type)
197
+ || (error instanceof Error && /^(E2EE|E2EE[A-Za-z]+Error)/.test(error.name));
198
+ }
199
+ async closeClient() {
200
+ const client = this.client;
201
+ this.client = undefined;
202
+ if (client)
203
+ await client.close();
204
+ }
205
+ json(response, status, value) {
206
+ response.statusCode = status;
207
+ response.setHeader("content-type", "application/json; charset=utf-8");
208
+ response.end(JSON.stringify(value));
209
+ }
210
+ }
211
+ exports.AunPlugin = AunPlugin;
212
+ exports.default = AunPlugin;
213
+ function safeError(error) {
214
+ return redact(error instanceof Error ? error.message : String(error)).slice(0, 240);
215
+ }
216
+ function redact(message) {
217
+ return message.replace(/(private[_ -]?key|seed|token|certificate)\s*[:=]\s*[^\s,;]+/gi, "$1=[已隐藏]");
218
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DuplicateTracker = void 0;
4
+ exports.normalizeAunMessage = normalizeAunMessage;
5
+ function normalizeAunMessage(payload, selfAid, allowFrom) {
6
+ if (!payload || typeof payload !== "object")
7
+ return undefined;
8
+ const data = payload;
9
+ const peerId = typeof data.from === "string" ? data.from.trim() : "";
10
+ const rules = (allowFrom ?? "").split(",").map((value) => value.trim()).filter(Boolean);
11
+ if (!peerId || peerId === selfAid || (rules.length > 0 && !rules.includes(peerId)))
12
+ return undefined;
13
+ const body = data.payload && typeof data.payload === "object" ? data.payload : {};
14
+ const text = typeof data.payload === "string" ? data.payload : typeof body.text === "string" ? body.text : "";
15
+ if (!text.trim())
16
+ return undefined;
17
+ return {
18
+ peerId,
19
+ ...(typeof data.message_id === "string" ? { messageId: data.message_id } : {}),
20
+ text,
21
+ ...(typeof body.thread_id === "string" ? { threadId: body.thread_id } : {}),
22
+ };
23
+ }
24
+ class DuplicateTracker {
25
+ limit;
26
+ ttlMs;
27
+ ids = new Map();
28
+ constructor(limit = 10000, ttlMs = 5 * 60 * 1000) {
29
+ this.limit = limit;
30
+ this.ttlMs = ttlMs;
31
+ }
32
+ add(id, now = Date.now()) {
33
+ if (!id)
34
+ return true;
35
+ const seen = this.ids.get(id);
36
+ if (seen !== undefined && now - seen < this.ttlMs)
37
+ return false;
38
+ this.ids.delete(id);
39
+ this.ids.set(id, now);
40
+ while (this.ids.size > this.limit)
41
+ this.ids.delete(this.ids.keys().next().value);
42
+ return true;
43
+ }
44
+ }
45
+ exports.DuplicateTracker = DuplicateTracker;
@@ -27,6 +27,9 @@ class QwenworkPlugin {
27
27
  configuredCdpPort;
28
28
  activeCdpPort;
29
29
  configuredHttpPort;
30
+ relayTarget;
31
+ callPlugin;
32
+ pluginStatus;
30
33
  disposers = [];
31
34
  requestQueue = Promise.resolve();
32
35
  state = "stopped";
@@ -42,16 +45,26 @@ class QwenworkPlugin {
42
45
  this.state = "starting";
43
46
  try {
44
47
  const config = context.config;
48
+ const relayTarget = config.relayTarget;
49
+ if (relayTarget !== undefined && relayTarget !== "dsh")
50
+ throw new Error("无效的中继目标,仅支持 dsh");
51
+ if (relayTarget === "dsh" && !context.callPlugin)
52
+ throw new Error("中继目标 dsh 不可调用");
53
+ this.relayTarget = relayTarget === "dsh" ? relayTarget : undefined;
54
+ this.callPlugin = context.callPlugin;
55
+ this.pluginStatus = context.pluginStatus;
45
56
  this.activeCdpPort = asNumber(config.cdpPort, this.configuredCdpPort ?? exports.qwenworkManifest.defaults.cdpPort);
46
- const client = this.injectedClient ?? new cdp_client_1.QwenCdpClient({
47
- port: this.activeCdpPort,
48
- subChatId: typeof config.subChatId === "string" ? config.subChatId : undefined,
49
- chatId: typeof config.chatId === "string" ? config.chatId : undefined,
50
- workspace: typeof config.workspace === "string" ? config.workspace : undefined,
51
- });
52
- this.client = client;
53
- await client.connect();
54
- this.adapter = new openai_adapter_1.OpenAIAdapter((prompt, signal) => this.enqueue(() => client.direct(prompt, asNumber(config.timeoutMs, 240_000), signal)), {
57
+ if (!this.relayTarget) {
58
+ const client = this.injectedClient ?? new cdp_client_1.QwenCdpClient({
59
+ port: this.activeCdpPort,
60
+ subChatId: typeof config.subChatId === "string" ? config.subChatId : undefined,
61
+ chatId: typeof config.chatId === "string" ? config.chatId : undefined,
62
+ workspace: typeof config.workspace === "string" ? config.workspace : undefined,
63
+ });
64
+ this.client = client;
65
+ await client.connect();
66
+ }
67
+ this.adapter = new openai_adapter_1.OpenAIAdapter((prompt, signal) => this.enqueue(() => this.complete(prompt, asNumber(config.timeoutMs, 240_000), signal)), {
55
68
  onLog: (message) => context.logger?.info(message),
56
69
  onExchange: (direction, text, peer, agentAid) => context.logger?.info("千问通信", { direction, text, peer, agentAid }),
57
70
  });
@@ -80,16 +93,27 @@ class QwenworkPlugin {
80
93
  }
81
94
  async health() {
82
95
  const details = { cdpPort: this.activeCdpPort ?? exports.qwenworkManifest.defaults.cdpPort, httpPort: this.configuredHttpPort ?? exports.qwenworkManifest.defaults.httpPort };
96
+ if (this.state === "ready" && this.relayTarget && this.pluginStatus?.(this.relayTarget) !== "ready")
97
+ return { status: "degraded", message: "DSH 中继目标未就绪", details: { ...details, relayTarget: this.relayTarget } };
83
98
  if (this.state === "ready" && this.client && !this.client.connected())
84
99
  return { status: "degraded", message: "千问 CDP 已断开", details };
85
100
  return { status: this.state, ...(this.message ? { message: this.message } : {}), details };
86
101
  }
87
102
  async handle(request) {
88
- if (this.state !== "ready" || !this.client)
103
+ if (this.state !== "ready" || (!this.relayTarget && !this.client))
89
104
  throw new Error("qwenwork 插件未就绪");
90
- const text = await this.enqueue(() => this.client.direct(request.task, request.timeoutMs ?? 240_000));
105
+ const text = await this.enqueue(() => this.complete(request.task, request.timeoutMs ?? 240_000));
91
106
  return { text };
92
107
  }
108
+ async complete(prompt, timeoutMs, signal) {
109
+ if (this.relayTarget) {
110
+ const response = await this.callPlugin(this.relayTarget, { task: prompt, timeoutMs });
111
+ return response.text;
112
+ }
113
+ if (!this.client)
114
+ throw new Error("qwenwork 插件未就绪");
115
+ return this.client.direct(prompt, timeoutMs, signal);
116
+ }
93
117
  registerRoutes(http) {
94
118
  if (!this.adapter)
95
119
  return;
@@ -107,6 +131,9 @@ class QwenworkPlugin {
107
131
  this.adapter = undefined;
108
132
  this.client?.close();
109
133
  this.client = undefined;
134
+ this.relayTarget = undefined;
135
+ this.callPlugin = undefined;
136
+ this.pluginStatus = undefined;
110
137
  }
111
138
  enqueue(operation) {
112
139
  const run = this.requestQueue.then(operation, operation);
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = __importDefault(require("node:test"));
8
+ const messages_1 = require("../src-ts/plugins/aun/messages");
9
+ (0, node_test_1.default)("AUN 入站消息过滤自身、白名单外来源和非文本内容", () => {
10
+ strict_1.default.equal((0, messages_1.normalizeAunMessage)({ from: "self.agentid.cn", payload: { text: "忽略" } }, "self.agentid.cn"), undefined);
11
+ strict_1.default.equal((0, messages_1.normalizeAunMessage)({ from: "other.agentid.cn", payload: { text: "忽略" } }, "self.agentid.cn", "allowed.agentid.cn"), undefined);
12
+ strict_1.default.equal((0, messages_1.normalizeAunMessage)({ from: "other.agentid.cn", payload: { image: {} } }, "self.agentid.cn"), undefined);
13
+ strict_1.default.deepEqual((0, messages_1.normalizeAunMessage)({ from: "other.agentid.cn", message_id: "m1", payload: { text: "你好", thread_id: "t1" } }, "self.agentid.cn"), {
14
+ peerId: "other.agentid.cn", messageId: "m1", text: "你好", threadId: "t1",
15
+ });
16
+ });
17
+ (0, node_test_1.default)("AUN 消息去重在 TTL 内拒绝重复 ID", () => {
18
+ const tracker = new messages_1.DuplicateTracker(2, 100);
19
+ strict_1.default.equal(tracker.add("m1", 10), true);
20
+ strict_1.default.equal(tracker.add("m1", 50), false);
21
+ strict_1.default.equal(tracker.add("m1", 111), true);
22
+ });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
10
+ const node_test_1 = __importDefault(require("node:test"));
11
+ const aun_1 = require("../src-ts/plugins/aun");
12
+ (0, node_test_1.default)("AUN 插件注册 AID 并将加密回复发回来源 Peer", async () => {
13
+ const calls = [];
14
+ const clients = [];
15
+ const sdk = {
16
+ AIDStore: class {
17
+ async register(input) { calls.push(`register:${input.aid}:${input.owner}`); return { ok: true, data: { ownership_status: "active" } }; }
18
+ load(aid) { return { ok: true, data: { aid } }; }
19
+ close() { calls.push("store.close"); }
20
+ },
21
+ AUNClient: class {
22
+ constructor() { const current = this; current.emit = (payload) => current.handler?.(payload); clients.push(current); }
23
+ on(_event, handler) { this.handler = handler; return { unsubscribe() { } }; }
24
+ async connect() { calls.push("client.connect"); }
25
+ async close() { calls.push("client.close"); }
26
+ async call(method, params) { calls.push(`${method}:${params.to}:${params.encrypt}`); }
27
+ },
28
+ };
29
+ const received = [];
30
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-plugin-"));
31
+ const routes = [];
32
+ const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish: async (message) => { received.push(message); } } });
33
+ try {
34
+ await plugin.activate({
35
+ dataDir: root, config: {}, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready",
36
+ http: { register(route) { routes.push(route); return () => undefined; }, unregister() { } },
37
+ });
38
+ const route = routes[0];
39
+ const response = { end(value) { this.body = JSON.parse(value); }, setHeader() { }, statusCode: 0 };
40
+ const request = (async function* () { yield Buffer.from('{"aid":"bridge.agentid.pub","owner_aid":"owner.agentid.pub"}'); })();
41
+ await route.handler(request, response);
42
+ strict_1.default.equal(response.statusCode, 201);
43
+ strict_1.default.equal(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8")).aid, "bridge.agentid.pub");
44
+ clients[0].emit({ from: "peer.agentid.pub", message_id: "m1", payload: { text: "你好" } });
45
+ strict_1.default.equal(received[0].agentPlugin, "qwenwork");
46
+ await received[0].reply("收到");
47
+ strict_1.default.ok(calls.includes("message.send:peer.agentid.pub:true"));
48
+ await plugin.deactivate("stop");
49
+ strict_1.default.deepEqual(calls.slice(-2), ["client.close", "store.close"]);
50
+ }
51
+ finally {
52
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
53
+ }
54
+ });
@@ -25,6 +25,19 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
25
25
  await server.stop();
26
26
  }
27
27
  });
28
+ (0, node_test_1.default)("插件目录 API 返回远程候选包", async () => {
29
+ const manager = { list: () => [], searchPackages: async (query) => query === "workbuddy" ? [{ name: "agent-bridge-workbuddy", version: "1.0.0", type: "agent", description: "办公 Agent" }] : [] };
30
+ const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0 });
31
+ const address = await server.start();
32
+ try {
33
+ const response = await fetch(`http://${address.host}:${address.port}/api/plugins/catalog?q=workbuddy`);
34
+ strict_1.default.equal(response.status, 200);
35
+ strict_1.default.equal((await response.json()).plugins[0].type, "agent");
36
+ }
37
+ finally {
38
+ await server.stop();
39
+ }
40
+ });
28
41
  (0, node_test_1.default)("control server dispatches plugin HTTP routes and unregisters them on stop", async () => {
29
42
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-http-plugin-"));
30
43
  try {
@@ -17,7 +17,8 @@ const qwenwork_1 = require("../src-ts/plugins/qwenwork");
17
17
  const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-platform-"));
18
18
  const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.resolve)(process.cwd(), "plugins"), stateDir: state });
19
19
  const records = await manager.discover();
20
- strict_1.default.deepEqual(records.map((record) => record.id).sort(), ["abweb", "doubao", "dsh", "qwenwork"]);
20
+ strict_1.default.deepEqual(records.map((record) => record.id).sort(), ["abweb", "aun", "doubao", "dsh", "qwenwork"]);
21
+ strict_1.default.equal(records.find((record) => record.id === "aun")?.type, "channel");
21
22
  const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0 });
22
23
  const address = await server.start();
23
24
  try {
@@ -21,6 +21,7 @@ const plugin_manifest_1 = require("../src-ts/plugin-manifest");
21
21
  strict_1.default.deepEqual((await (0, plugin_catalog_1.searchPluginCatalog)("", get)).map(({ name, type }) => [name, type]), [
22
22
  ["agent-bridge-workbuddy", "agent"], ["agent-bridge-aun", "channel"],
23
23
  ]);
24
+ strict_1.default.deepEqual((await (0, plugin_catalog_1.searchPluginCatalog)("aun", get)).map(({ name }) => name), ["agent-bridge-aun"]);
24
25
  });
25
26
  (0, node_test_1.default)("插件清单类型从能力兼容识别,冲突类型拒绝", () => {
26
27
  const base = { id: "sample", version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: ["chat"] };
@@ -32,6 +32,59 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
32
32
  await (0, promises_1.rm)(root, { recursive: true, force: true });
33
33
  }
34
34
  });
35
+ (0, node_test_1.default)("插件上下文只允许调用已就绪的其他插件", async () => {
36
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-call-plugin-"));
37
+ try {
38
+ for (const [id, text] of [["source", "source"], ["target", "target"]]) {
39
+ const dir = (0, node_path_1.join)(root, id, "1.0.0");
40
+ await (0, promises_1.mkdir)(dir, { recursive: true });
41
+ 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", process: "in-process", capabilities: ["chat"] }));
42
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), `module.exports = { manifest: require('./plugin.json'), activate: async(context)=>{ if ('${id}' === 'source') { globalThis.callPlugin = context.callPlugin; } }, deactivate: async()=>{}, health: async()=>({status:'ready'}), handle: async(request)=>({text:'${text}:'+request.task}) };\n`);
43
+ }
44
+ const manager = new plugin_manager_1.PluginManager({ pluginsDir: root, stateDir: (0, node_path_1.join)(root, "state") });
45
+ await manager.discover();
46
+ await manager.start("target");
47
+ await manager.start("source");
48
+ const callPlugin = globalThis.callPlugin;
49
+ strict_1.default.ok(callPlugin);
50
+ strict_1.default.deepEqual(await callPlugin("target", { task: "你好" }), { text: "target:你好" });
51
+ await strict_1.default.rejects(() => callPlugin("source", { task: "loop" }), /自身|self/);
52
+ await manager.stop("target");
53
+ await strict_1.default.rejects(() => callPlugin("target", { task: "offline" }), /未就绪|not ready/);
54
+ }
55
+ finally {
56
+ delete globalThis.callPlugin;
57
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
58
+ }
59
+ });
60
+ (0, node_test_1.default)("通道消息调用指定 Agent 并通过原消息回复", async () => {
61
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-channel-ingress-"));
62
+ try {
63
+ for (const [id, source] of [["qwenwork", false], ["aun", true]]) {
64
+ const dir = (0, node_path_1.join)(root, id);
65
+ await (0, promises_1.mkdir)(dir, { recursive: true });
66
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id, version: "1.0.0", type: source ? "channel" : "agent", entry: "index.js", apiVersion: "1", capabilities: source ? ["channel"] : ["chat"] }));
67
+ const code = source
68
+ ? "module.exports={manifest:require('./plugin.json'),activate:async(c)=>{globalThis.testIngress=c.ingress},deactivate:async()=>{},health:async()=>({status:'ready'}),handle:async()=>({text:''})};"
69
+ : "module.exports={manifest:require('./plugin.json'),activate:async()=>{},deactivate:async()=>{},health:async()=>({status:'ready'}),handle:async(r)=>({text:'回复:'+r.task})};";
70
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), code);
71
+ }
72
+ const manager = new plugin_manager_1.PluginManager({ pluginsDir: root, stateDir: (0, node_path_1.join)(root, "state") });
73
+ await manager.discover();
74
+ await manager.start("qwenwork");
75
+ await manager.start("aun");
76
+ let reply = "";
77
+ const ingress = globalThis.testIngress;
78
+ await ingress.publish({ channel: "aun", peerId: "peer.agentid.pub", text: "你好", reply: async (text) => { reply = text; } });
79
+ strict_1.default.equal(reply, "回复:你好");
80
+ await manager.stop("aun");
81
+ await manager.stop("qwenwork");
82
+ }
83
+ finally {
84
+ delete globalThis.testIngress;
85
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
86
+ }
87
+ });
35
88
  (0, node_test_1.default)("plugin manager rejects path traversal and duplicate routes", async () => {
36
89
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-plugin-"));
37
90
  try {
@@ -41,3 +41,29 @@ const cdp_client_1 = require("../src-ts/plugins/qwenwork/cdp-client");
41
41
  await plugin.deactivate("stop");
42
42
  strict_1.default.equal((await plugin.health()).status, "stopped");
43
43
  });
44
+ (0, node_test_1.default)("中继模式绕过千问 CDP 并原样返回 DSH 回复", async () => {
45
+ const calls = [];
46
+ let targetStatus = "ready";
47
+ const plugin = new qwenwork_1.QwenworkPlugin({ client: {
48
+ connect: async () => { calls.push("connect"); },
49
+ close: () => { calls.push("close"); },
50
+ direct: async () => { calls.push("direct"); return "千问回复"; },
51
+ send: async () => undefined,
52
+ connected: () => false,
53
+ }, httpPort: 0 });
54
+ await plugin.activate({ config: { relayTarget: "dsh" }, dataDir: ".", logger: { info() { }, error() { } },
55
+ callPlugin: async (id, request) => { calls.push(`${id}:${request.task}`); return { text: "DSH 原文" }; },
56
+ pluginStatus: () => targetStatus,
57
+ });
58
+ strict_1.default.deepEqual(await plugin.handle({ task: "原始问题" }), { text: "DSH 原文" });
59
+ strict_1.default.deepEqual(calls, ["dsh:原始问题"]);
60
+ strict_1.default.equal((await plugin.health()).status, "ready");
61
+ targetStatus = "disabled";
62
+ strict_1.default.equal((await plugin.health()).status, "degraded");
63
+ await plugin.deactivate("stop");
64
+ strict_1.default.deepEqual(calls, ["dsh:原始问题"]);
65
+ });
66
+ (0, node_test_1.default)("中继模式拒绝非 DSH 目标", async () => {
67
+ const plugin = new qwenwork_1.QwenworkPlugin({ httpPort: 0 });
68
+ await strict_1.default.rejects(() => plugin.activate({ config: { relayTarget: "qwenwork" }, dataDir: ".", logger: { info() { }, error() { } } }), /中继目标/);
69
+ });
@@ -7,6 +7,7 @@ const strict_1 = __importDefault(require("node:assert/strict"));
7
7
  const node_http_1 = require("node:http");
8
8
  const node_test_1 = __importDefault(require("node:test"));
9
9
  const openai_adapter_1 = require("../src-ts/plugins/qwenwork/openai-adapter");
10
+ const qwenwork_1 = require("../src-ts/plugins/qwenwork");
10
11
  async function listen(server) {
11
12
  await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
12
13
  const address = server.address();
@@ -89,3 +90,34 @@ async function listen(server) {
89
90
  await new Promise((resolve) => server.close(() => resolve()));
90
91
  }
91
92
  });
93
+ (0, node_test_1.default)("QwenWork 中继入口把 HTTP 请求原样交给 DSH", async () => {
94
+ let handler;
95
+ const plugin = new qwenwork_1.QwenworkPlugin({ client: {
96
+ connect: async () => { throw new Error("CDP 不应连接"); },
97
+ close: () => undefined,
98
+ direct: async () => "错误回复",
99
+ send: async () => undefined,
100
+ connected: () => false,
101
+ } });
102
+ await plugin.activate({ config: { relayTarget: "dsh" }, dataDir: ".", logger: { info() { }, error() { } },
103
+ callPlugin: async (_id, request) => ({ text: `DSH:${request.task}` }),
104
+ pluginStatus: () => "ready",
105
+ http: { register: (route) => { if (route.method === "POST" && route.path === "/v1/chat/completions")
106
+ handler = route.handler; return () => undefined; } },
107
+ });
108
+ strict_1.default.ok(handler);
109
+ const server = (0, node_http_1.createServer)((req, res) => void handler(req, res));
110
+ const port = await listen(server);
111
+ try {
112
+ const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
113
+ method: "POST", headers: { "content-type": "application/json" },
114
+ body: JSON.stringify({ messages: [{ role: "user", content: "原始问题" }] }),
115
+ });
116
+ strict_1.default.equal(response.status, 200);
117
+ strict_1.default.equal((await response.json()).choices[0].message.content, "DSH:原始问题");
118
+ }
119
+ finally {
120
+ await plugin.deactivate("stop");
121
+ await new Promise((resolve) => server.close(() => resolve()));
122
+ }
123
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": { "mab": "dist/src-ts/cli.js", "agent-bridge": "dist/src-ts/cli.js" },
@@ -8,7 +8,8 @@
8
8
  "subChatId": { "type": "string" },
9
9
  "chatId": { "type": "string" },
10
10
  "workspace": { "type": "string" },
11
- "timeoutMs": { "type": "integer", "minimum": 1000, "default": 240000 }
11
+ "timeoutMs": { "type": "integer", "minimum": 1000, "default": 240000 },
12
+ "relayTarget": { "type": "string", "enum": ["dsh"], "description": "将文本原样中继到 DSH,不调用千问 CDP" }
12
13
  },
13
14
  "additionalProperties": false
14
15
  }