mu-agent-bridge 1.0.5 → 1.0.13
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 +16 -11
- package/dist/src-ts/channel-sessions.js +68 -0
- package/dist/src-ts/cli.js +60 -25
- package/dist/src-ts/control-server.js +75 -94
- package/dist/src-ts/local-aids.js +26 -0
- package/dist/src-ts/log-store.js +64 -0
- package/dist/src-ts/plugin-manager.js +240 -39
- package/dist/src-ts/plugin-manifest.js +5 -1
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +164 -100
- package/dist/src-ts/plugins/abweb/public/index.html +7 -6
- package/dist/src-ts/plugins/abweb/public/style.css +12 -13
- package/dist/src-ts/plugins/aun/evolcore.js +54 -0
- package/dist/src-ts/plugins/aun/index.js +253 -51
- package/dist/src-ts/plugins/aun/public/app.js +66 -0
- package/dist/src-ts/plugins/aun/public/index.html +14 -0
- package/dist/src-ts/plugins/aun/public/style.css +21 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +5 -4
- package/dist/src-ts/plugins/doubao/index.js +3 -2
- package/dist/src-ts/plugins/feishu/client.js +133 -0
- package/dist/src-ts/plugins/feishu/index.js +257 -0
- package/dist/src-ts/plugins/feishu/public/app.js +147 -0
- package/dist/src-ts/plugins/feishu/public/index.html +17 -0
- package/dist/src-ts/plugins/feishu/public/style.css +30 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +118 -0
- package/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
- package/dist/src-ts/plugins/workbuddy/index.js +139 -0
- package/dist/src-ts/version-manager.js +9 -1
- package/dist/tests-ts/abweb-plugin.test.js +13 -2
- package/dist/tests-ts/aun-plugin.test.js +108 -4
- package/dist/tests-ts/control-server.test.js +88 -6
- package/dist/tests-ts/doubao-plugin.test.js +18 -0
- package/dist/tests-ts/feishu-client.test.js +72 -0
- package/dist/tests-ts/feishu-page.test.js +53 -0
- package/dist/tests-ts/feishu-plugin.test.js +148 -0
- package/dist/tests-ts/log-store.test.js +27 -0
- package/dist/tests-ts/platform-integration.test.js +40 -1
- package/dist/tests-ts/plugin-catalog.test.js +8 -0
- package/dist/tests-ts/plugin-manager.test.js +219 -13
- package/dist/tests-ts/workbuddy-plugin.test.js +85 -0
- package/package.json +30 -6
- package/plugins/abweb/plugin.json +1 -1
|
@@ -6,10 +6,36 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
7
|
const node_test_1 = __importDefault(require("node:test"));
|
|
8
8
|
const control_server_1 = require("../src-ts/control-server");
|
|
9
|
+
const evolcore_1 = require("../src-ts/plugins/aun/evolcore");
|
|
9
10
|
const promises_1 = require("node:fs/promises");
|
|
10
11
|
const node_os_1 = require("node:os");
|
|
11
12
|
const node_path_1 = require("node:path");
|
|
12
13
|
const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
14
|
+
(0, node_test_1.default)("豆包等 Agent 的 EvolCore 关联使用独立路由", async () => {
|
|
15
|
+
const calls = [];
|
|
16
|
+
await (0, evolcore_1.configureAidForPlugin)("doubao", "doubao-lih.agentid.cn", 26107, async (args) => { calls.push(args); return { ok: true }; });
|
|
17
|
+
strict_1.default.ok(calls.some((args) => args.includes("http://127.0.0.1:26107/api/agents/doubao/v1")));
|
|
18
|
+
await strict_1.default.rejects((0, evolcore_1.configureAidForPlugin)("doubao", "doubao-lih.agentid.cn", 26107, async (args) => args[1] === "reload" ? { ok: false, code: "AGENT_BUSY", error: "还有任务正在运行" } : { ok: true }), /AGENT_BUSY.*还有任务正在运行/);
|
|
19
|
+
});
|
|
20
|
+
(0, node_test_1.default)("通用 Agent 路由调用插件且关联失败进入日志", async () => {
|
|
21
|
+
const logs = [];
|
|
22
|
+
const recordMessage = () => undefined;
|
|
23
|
+
const manager = { log: (...args) => logs.push(args), handle: async (id, request) => ({ text: `${id}:${request.task}` }), get: () => ({ record: { status: "ready" }, instance: { identity: { bind: async () => { throw new Error("目标 Agent 未启动"); } } } }) };
|
|
24
|
+
manager.recordMessage = recordMessage;
|
|
25
|
+
const server = new control_server_1.ControlServer({ manager, port: 0 });
|
|
26
|
+
const address = await server.start();
|
|
27
|
+
try {
|
|
28
|
+
const base = `http://127.0.0.1:${address.port}`;
|
|
29
|
+
const reply = await fetch(`${base}/api/agents/doubao/v1/chat/completions`, { method: "POST", body: JSON.stringify({ messages: [{ role: "user", content: "测试" }] }) });
|
|
30
|
+
strict_1.default.equal(reply.status, 200);
|
|
31
|
+
strict_1.default.equal((await reply.json()).choices[0].message.content, "doubao:测试");
|
|
32
|
+
await fetch(`${base}/api/plugins/doubao/aid`, { method: "PATCH", body: JSON.stringify({ aid: "doubao-lih.agentid.cn" }) });
|
|
33
|
+
strict_1.default.ok(logs.some((args) => JSON.stringify(args).includes("目标 Agent 未启动")));
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await server.stop();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
13
39
|
(0, node_test_1.default)("control server exposes health and plugin list", async () => {
|
|
14
40
|
const manager = { list: () => [{ id: "demo", version: "1.0.0", displayName: "Demo", status: "ready", capabilities: [] }] };
|
|
15
41
|
const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0 });
|
|
@@ -105,7 +131,7 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
|
105
131
|
{ aid: "disabled.agentid.cn", status: "disabled", channels: ["aun#disabled.agentid.cn#main"] },
|
|
106
132
|
] }
|
|
107
133
|
: { ok: true, aid: args[2], status: "running", connection: { status: args[2] === "connected.agentid.cn" ? "connected" : "disconnected" } };
|
|
108
|
-
strict_1.default.deepEqual(await (0,
|
|
134
|
+
strict_1.default.deepEqual(await (0, evolcore_1.listConnectedAids)(run), ["connected.agentid.cn"]);
|
|
109
135
|
}
|
|
110
136
|
finally {
|
|
111
137
|
if (before === undefined)
|
|
@@ -118,7 +144,7 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
|
118
144
|
(0, node_test_1.default)("关联 DSH 时配置 EvolCore 路由并重载 AID", async () => {
|
|
119
145
|
const calls = [];
|
|
120
146
|
const run = async (args) => { calls.push(args); return { ok: true }; };
|
|
121
|
-
await (0,
|
|
147
|
+
await (0, evolcore_1.configureAidForPlugin)("dsh", "test.agentid.cn", 26107, run);
|
|
122
148
|
strict_1.default.deepEqual(calls.map((args) => args.slice(0, 4)), [
|
|
123
149
|
["agent", "set", "test.agentid.cn", "baseagents.ecagent.baseUrl"],
|
|
124
150
|
["agent", "set", "test.agentid.cn", "baseagents.ecagent.apiKey"],
|
|
@@ -129,7 +155,7 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
|
129
155
|
strict_1.default.equal(calls[0][4], "http://127.0.0.1:26107/dsh/v1");
|
|
130
156
|
strict_1.default.equal(calls[2][4], "dsh");
|
|
131
157
|
calls.length = 0;
|
|
132
|
-
await (0,
|
|
158
|
+
await (0, evolcore_1.configureAidForPlugin)("qwenwork", "qwenbg.agentid.cn", 26107, run);
|
|
133
159
|
strict_1.default.equal(calls[0][4], "http://127.0.0.1:26107/v1");
|
|
134
160
|
strict_1.default.equal(calls[2][4], "qwenwork");
|
|
135
161
|
});
|
|
@@ -147,6 +173,48 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
|
147
173
|
await server.stop();
|
|
148
174
|
}
|
|
149
175
|
});
|
|
176
|
+
(0, node_test_1.default)("未启动 AUN 插件时不能创建,仍可扫描本地身份", async () => {
|
|
177
|
+
const manager = { get: () => undefined, list: () => [] };
|
|
178
|
+
const server = new control_server_1.ControlServer({ manager, port: 0 });
|
|
179
|
+
const address = await server.start();
|
|
180
|
+
try {
|
|
181
|
+
const base = `http://${address.host}:${address.port}`;
|
|
182
|
+
const response = await fetch(`${base}/api/aids`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ aid: "new.agentid.cn", ownerAid: "owner.agentid.cn" }) });
|
|
183
|
+
strict_1.default.equal(response.status, 409);
|
|
184
|
+
strict_1.default.match((await response.json()).error, /AUN/);
|
|
185
|
+
const identities = await (await fetch(`${base}/api/aids`)).json();
|
|
186
|
+
strict_1.default.equal(identities.canCreate, false);
|
|
187
|
+
strict_1.default.deepEqual(identities.aids, []);
|
|
188
|
+
strict_1.default.ok(Array.isArray(identities.localAids));
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await server.stop();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
(0, node_test_1.default)("创建和关联身份由通道插件执行", async () => {
|
|
195
|
+
const calls = [];
|
|
196
|
+
const identity = {
|
|
197
|
+
list: async () => ({ evolcore: [], muAgentBridge: ["new.agentid.cn"] }),
|
|
198
|
+
create: async (aid, owner) => { calls.push([aid, owner]); return { aid, status: "已创建", connection: "ready" }; },
|
|
199
|
+
bind: async (...args) => { calls.push(args); },
|
|
200
|
+
};
|
|
201
|
+
const manager = { log() { }, get: () => ({ record: { status: "ready" }, instance: { identity } }), setAid: async (...args) => calls.push(args) };
|
|
202
|
+
const server = new control_server_1.ControlServer({ manager, port: 0 });
|
|
203
|
+
const address = await server.start();
|
|
204
|
+
try {
|
|
205
|
+
const base = `http://${address.host}:${address.port}`;
|
|
206
|
+
const response = await fetch(`${base}/api/aids`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ aid: "new.agentid.cn", ownerAid: "owner.agentid.cn" }) });
|
|
207
|
+
strict_1.default.equal(response.status, 201);
|
|
208
|
+
strict_1.default.deepEqual(calls[0], ["new.agentid.cn", "owner.agentid.cn"]);
|
|
209
|
+
const bound = await fetch(`${base}/api/plugins/dsh/aid`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ aid: "new.agentid.cn" }) });
|
|
210
|
+
strict_1.default.equal(bound.status, 200);
|
|
211
|
+
strict_1.default.deepEqual(calls[1], ["dsh", "new.agentid.cn", address.port]);
|
|
212
|
+
strict_1.default.deepEqual(calls[2], ["dsh", "new.agentid.cn"]);
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
await server.stop();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
150
218
|
(0, node_test_1.default)("control API protects data with a token while keeping abweb reachable", async () => {
|
|
151
219
|
const manager = { list: () => [], dispatchHttp: async () => false };
|
|
152
220
|
const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0, token: "secret" });
|
|
@@ -168,19 +236,33 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
|
168
236
|
const base = `http://${address.host}:${address.port}`;
|
|
169
237
|
const codeResponse = await fetch(`${base}/api/pair-code`);
|
|
170
238
|
strict_1.default.equal(codeResponse.status, 200);
|
|
171
|
-
const { code } = await codeResponse.json();
|
|
239
|
+
const { code, expiresAt } = await codeResponse.json();
|
|
240
|
+
strict_1.default.ok(expiresAt - Date.now() > 9 * 60 * 1000);
|
|
241
|
+
strict_1.default.ok(expiresAt - Date.now() <= 10 * 60 * 1000);
|
|
172
242
|
strict_1.default.match(code, /^\d{6}$/);
|
|
173
243
|
const redeem = () => fetch(`${base}/api/pair`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ code }) });
|
|
174
244
|
const [first, second] = await Promise.all([redeem(), redeem()]);
|
|
175
245
|
strict_1.default.deepEqual([first.status, second.status].sort(), [200, 403]);
|
|
176
246
|
const winner = first.status === 200 ? first : second;
|
|
177
|
-
const { token } = await winner.json();
|
|
247
|
+
const { token, expiresAt: loginExpiresAt } = await winner.json();
|
|
248
|
+
strict_1.default.ok(loginExpiresAt - Date.now() > 23 * 60 * 60 * 1000);
|
|
249
|
+
strict_1.default.ok(loginExpiresAt - Date.now() <= 24 * 60 * 60 * 1000);
|
|
178
250
|
strict_1.default.notEqual(token, "secret");
|
|
179
251
|
strict_1.default.equal((await fetch(`${base}/api/plugins`, { headers: { authorization: `Bearer ${token}` } })).status, 200);
|
|
180
252
|
strict_1.default.equal((await fetch(`${base}/api/plugins`)).status, 200);
|
|
181
253
|
const next = await fetch(`${base}/api/pair-code`);
|
|
182
254
|
strict_1.default.equal(next.status, 200);
|
|
183
|
-
|
|
255
|
+
const renewed = await next.json();
|
|
256
|
+
strict_1.default.match(renewed.code, /^\d{6}$/);
|
|
257
|
+
strict_1.default.ok(renewed.expiresAt - Date.now() > 9 * 60 * 1000);
|
|
258
|
+
const checkRemote = async () => {
|
|
259
|
+
const response = { setHeader() { }, end() { } };
|
|
260
|
+
await server.handle({ url: "/api/health", method: "GET", headers: { host: "localhost", authorization: `Bearer ${token}` }, socket: { remoteAddress: "192.168.31.10" } }, response);
|
|
261
|
+
return response.statusCode;
|
|
262
|
+
};
|
|
263
|
+
strict_1.default.equal(await checkRemote(), 200);
|
|
264
|
+
server.pairedTokens.set(token, Date.now() - 1);
|
|
265
|
+
strict_1.default.equal(await checkRemote(), 401);
|
|
184
266
|
}
|
|
185
267
|
finally {
|
|
186
268
|
await server.stop();
|
|
@@ -0,0 +1,18 @@
|
|
|
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 cdp_client_1 = require("../src-ts/plugins/doubao/cdp-client");
|
|
9
|
+
(0, node_test_1.default)("未配置豆包参数时保留默认 CDP 端口", async () => {
|
|
10
|
+
let url = "";
|
|
11
|
+
const client = new cdp_client_1.DoubaoCdpClient({ port: undefined, conversationId: undefined, botId: undefined, timeoutMs: undefined, fetchImpl: async (input) => { url = String(input); return new Response("[]"); } });
|
|
12
|
+
await strict_1.default.rejects(client.connect(), /未找到豆包/);
|
|
13
|
+
strict_1.default.match(url, /:\d+\/json$/);
|
|
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
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
40
|
+
const node_test_1 = __importStar(require("node:test"));
|
|
41
|
+
const node_sdk_1 = require("@larksuiteoapi/node-sdk");
|
|
42
|
+
const client_1 = require("../src-ts/plugins/feishu/client");
|
|
43
|
+
(0, node_test_1.default)("飞书长连接使用官方 SDK 且握手失败不能报连接成功", async () => {
|
|
44
|
+
let failed = false;
|
|
45
|
+
const start = node_test_1.mock.method(node_sdk_1.WSClient.prototype, "start", async function () {
|
|
46
|
+
if (failed)
|
|
47
|
+
this.onError(new Error("应用未开启长连接"));
|
|
48
|
+
else
|
|
49
|
+
this.onReady();
|
|
50
|
+
});
|
|
51
|
+
const originalClose = node_sdk_1.WSClient.prototype.close;
|
|
52
|
+
const close = node_test_1.mock.method(node_sdk_1.WSClient.prototype, "close", function (params) { originalClose.call(this, params); });
|
|
53
|
+
const client = new client_1.FeishuClient({ app_id: "cli_test", app_secret: "private" });
|
|
54
|
+
try {
|
|
55
|
+
await client.start(async () => undefined);
|
|
56
|
+
strict_1.default.equal(start.mock.calls.length, 1);
|
|
57
|
+
client.close();
|
|
58
|
+
strict_1.default.equal(close.mock.calls.length, 1);
|
|
59
|
+
failed = true;
|
|
60
|
+
await strict_1.default.rejects(client.start(async () => undefined), /应用未开启长连接/);
|
|
61
|
+
strict_1.default.equal(close.mock.calls.length, 2);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
client.close();
|
|
65
|
+
start.mock.restore();
|
|
66
|
+
close.mock.restore();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
(0, node_test_1.default)("解析飞书文本事件并忽略非文本消息", () => {
|
|
70
|
+
strict_1.default.deepEqual((0, client_1.parseFeishuEvent)({ event: { sender: { sender_id: { open_id: "ou_1" } }, message: { message_id: "om_1", chat_id: "oc_1", message_type: "text", content: JSON.stringify({ text: "你好" }) } } }), { messageId: "om_1", peerId: "ou_1", chatId: "oc_1", text: "你好" });
|
|
71
|
+
strict_1.default.equal((0, client_1.parseFeishuEvent)({ event: { sender: { sender_id: { open_id: "ou_1" } }, message: { message_id: "om_2", message_type: "image", content: "{}" } } }), undefined);
|
|
72
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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_path_1 = require("node:path");
|
|
9
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
10
|
+
const node_vm_1 = require("node:vm");
|
|
11
|
+
(0, node_test_1.default)("飞书密码掩码在刷新和重连后保留,掩码不提交为真实密钥", async () => {
|
|
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;
|
|
14
|
+
const button = { disabled: false };
|
|
15
|
+
const credentials = { elements: { app_id: field(), app_secret: field() }, querySelector: () => button };
|
|
16
|
+
const nodes = {
|
|
17
|
+
"#credentials": credentials,
|
|
18
|
+
"#binding": { elements: { pluginId: field() } },
|
|
19
|
+
"#settings": { elements: { agent_plugin: field(), allow_from: field(), allow_chats: field() } },
|
|
20
|
+
};
|
|
21
|
+
const submitted = [];
|
|
22
|
+
let status = "ready";
|
|
23
|
+
const source = await (0, promises_1.readFile)((0, node_path_1.join)(__dirname, "../src-ts/plugins/feishu/public/app.js"), "utf8");
|
|
24
|
+
(0, node_vm_1.runInNewContext)(source, {
|
|
25
|
+
document: { querySelector: (id) => nodes[id] ??= element(), createElement: element },
|
|
26
|
+
localStorage: { getItem: () => null }, sessionStorage: { getItem: () => null },
|
|
27
|
+
window: { addEventListener() { } }, AbortSignal, setTimeout, clearTimeout, setInterval() { }, clearInterval() { },
|
|
28
|
+
Option: function () { },
|
|
29
|
+
fetch: async (path, init) => {
|
|
30
|
+
if (path.endsWith("/bot") || path.endsWith("/bind"))
|
|
31
|
+
submitted.push(JSON.parse(init.body));
|
|
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: [] } };
|
|
33
|
+
},
|
|
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");
|
|
41
|
+
strict_1.default.equal(credentials.elements.app_secret.value, "********");
|
|
42
|
+
await credentials.onsubmit({ preventDefault() { } });
|
|
43
|
+
strict_1.default.equal(submitted[1].app_secret, "");
|
|
44
|
+
strict_1.default.equal(credentials.elements.app_secret.value, "********");
|
|
45
|
+
status = "degraded";
|
|
46
|
+
await credentials.onsubmit({ preventDefault() { } });
|
|
47
|
+
strict_1.default.match(nodes["#notice"].textContent, /连接失败/);
|
|
48
|
+
strict_1.default.equal(credentials.elements.app_secret.value, "********");
|
|
49
|
+
credentials.elements.app_id.value = "cli_2";
|
|
50
|
+
credentials.elements.app_id.oninput();
|
|
51
|
+
strict_1.default.equal(credentials.elements.app_secret.value, "");
|
|
52
|
+
strict_1.default.equal(credentials.elements.app_secret.required, true);
|
|
53
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
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_stream_1 = require("node:stream");
|
|
11
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
12
|
+
const feishu_1 = require("../src-ts/plugins/feishu");
|
|
13
|
+
(0, node_test_1.default)("飞书插件把入站消息投递到绑定 Agent 并支持回复", async () => {
|
|
14
|
+
let received;
|
|
15
|
+
let sent = "";
|
|
16
|
+
const client = { async start(handler) { this.handler = handler; }, close() { }, async send(_peer, text) { sent = text; }, handler: undefined };
|
|
17
|
+
const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "feishu-plugin-"));
|
|
18
|
+
const plugin = new feishu_1.FeishuPlugin({ client: client, ingress: { publish: async (message) => { received = message; await message.reply("收到"); } } });
|
|
19
|
+
await plugin.activate({ dataDir: state, config: { app_id: "cli_1", app_secret: "secret", agent_plugin: "qwenwork" }, logger: { info() { }, warn() { }, error() { } }, http: { register() { }, unregister() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: (id) => id === "qwenwork" ? "ready" : undefined });
|
|
20
|
+
await client.handler({ messageId: "om_1", peerId: "ou_1", text: "你好" });
|
|
21
|
+
strict_1.default.equal(received.channel, "feishu");
|
|
22
|
+
strict_1.default.equal(received.agentPlugin, "qwenwork");
|
|
23
|
+
strict_1.default.equal(sent, "收到");
|
|
24
|
+
await plugin.deactivate("stop");
|
|
25
|
+
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
(0, node_test_1.default)("未配置凭据时仍注册机器人创建和配置路由", async () => {
|
|
28
|
+
const routes = new Map();
|
|
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({ client: { async start() { }, close() { } }, ingress: { publish: async () => undefined } });
|
|
31
|
+
try {
|
|
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
|
+
strict_1.default.ok(routes.has("GET /abweb/channels/feishu"));
|
|
34
|
+
strict_1.default.ok(routes.has("GET /api/plugins/feishu/setup/status"));
|
|
35
|
+
strict_1.default.ok(routes.has("POST /api/plugins/feishu/settings"));
|
|
36
|
+
strict_1.default.ok(routes.has("POST /api/plugins/feishu/bind"));
|
|
37
|
+
strict_1.default.ok(routes.has("POST /api/plugins/feishu/setup/begin"));
|
|
38
|
+
strict_1.default.ok(routes.has("POST /api/plugins/feishu/setup/poll"));
|
|
39
|
+
const response = { statusCode: 0, setHeader() { }, end(value) { this.body = value; } };
|
|
40
|
+
await routes.get("GET /api/plugins/feishu/setup/status")({}, response);
|
|
41
|
+
strict_1.default.equal(JSON.parse(response.body).app_id, "cli_1");
|
|
42
|
+
strict_1.default.doesNotMatch(response.body, /private/);
|
|
43
|
+
const page = { statusCode: 0, setHeader() { }, end(value) { this.body = value.toString(); } };
|
|
44
|
+
await routes.get("GET /abweb/channels/feishu")({ url: "/abweb/channels/feishu" }, page);
|
|
45
|
+
strict_1.default.match(page.body, /飞书机器人/);
|
|
46
|
+
await routes.get("POST /api/plugins/feishu/settings")(node_stream_1.Readable.from([JSON.stringify({ agent_plugin: "qwenwork", allow_from: "ou_1", allow_chats: "" })]), response);
|
|
47
|
+
strict_1.default.equal(JSON.parse((await (0, promises_1.readFile)((0, node_path_1.join)(state, "config.json"), "utf8")).toString()).app_secret, "private");
|
|
48
|
+
await routes.get("POST /api/plugins/feishu/bind")(node_stream_1.Readable.from([JSON.stringify({ pluginId: "qwenwork" })]), response);
|
|
49
|
+
strict_1.default.equal(JSON.parse((await (0, promises_1.readFile)((0, node_path_1.join)(state, "config.json"), "utf8")).toString()).bindings.cli_1, "qwenwork");
|
|
50
|
+
await plugin.deactivate("stop");
|
|
51
|
+
strict_1.default.equal(routes.size, 0);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
55
|
+
}
|
|
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
|
+
});
|
|
74
|
+
(0, node_test_1.default)("飞书设备授权轮询区分等待、拒绝、过期、完成和接口错误", async () => {
|
|
75
|
+
const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "feishu-poll-"));
|
|
76
|
+
const routes = new Map();
|
|
77
|
+
const plugin = new feishu_1.FeishuPlugin({ ingress: { publish: async () => undefined } });
|
|
78
|
+
const originalFetch = globalThis.fetch;
|
|
79
|
+
const results = [{}, { device_code: "device_1", verification_uri_complete: "https://accounts.feishu.cn/authorize?code=device_1", interval: 2, expire_in: 60 }, { error: "authorization_pending" }, { error: "slow_down" }, { error: "access_denied" }, { error: "expired_token" }, { error: "invalid_client", error_description: "应用凭据无效" }, { client_id: "cli_2", client_secret: "private" }];
|
|
80
|
+
globalThis.fetch = async () => new Response(JSON.stringify(results.shift() || {}), { headers: { "content-type": "application/json" } });
|
|
81
|
+
try {
|
|
82
|
+
await plugin.activate({ dataDir: state, config: {}, 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" });
|
|
83
|
+
const handler = routes.get("POST /api/plugins/feishu/setup/poll");
|
|
84
|
+
const response = { statusCode: 0, setHeader() { }, end(value) { this.body = value; } };
|
|
85
|
+
await routes.get("POST /api/plugins/feishu/setup/begin")({}, response);
|
|
86
|
+
const begin = JSON.parse(response.body);
|
|
87
|
+
strict_1.default.equal(begin.qr_url, begin.url);
|
|
88
|
+
strict_1.default.match(begin.qr_image, /^data:image\/png;base64,/);
|
|
89
|
+
strict_1.default.equal(begin.interval, 2);
|
|
90
|
+
for (const expected of ["pending", "pending", "denied", "expired", "error", "completed"]) {
|
|
91
|
+
await handler(node_stream_1.Readable.from([JSON.stringify({ device_code: "device_1" })]), response);
|
|
92
|
+
const result = JSON.parse(response.body);
|
|
93
|
+
strict_1.default.equal(result.status, expected);
|
|
94
|
+
if (result.slow_down)
|
|
95
|
+
strict_1.default.equal(result.slow_down, true);
|
|
96
|
+
strict_1.default.equal(result.app_secret, undefined);
|
|
97
|
+
}
|
|
98
|
+
strict_1.default.equal(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(state, "config.json"), "utf8")).app_secret, "private");
|
|
99
|
+
await routes.get("GET /api/plugins/feishu/setup/status")({}, response);
|
|
100
|
+
strict_1.default.equal(JSON.parse(response.body).app_id, "cli_2");
|
|
101
|
+
strict_1.default.equal(JSON.parse(response.body).configured, true);
|
|
102
|
+
strict_1.default.doesNotMatch(response.body, /private/);
|
|
103
|
+
globalThis.fetch = async () => { throw new Error("连接失败"); };
|
|
104
|
+
await handler(node_stream_1.Readable.from([JSON.stringify({ device_code: "device_1" })]), response);
|
|
105
|
+
strict_1.default.equal(response.statusCode, 502);
|
|
106
|
+
strict_1.default.match(response.body, /连接失败/);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
globalThis.fetch = originalFetch;
|
|
110
|
+
await plugin.deactivate("stop");
|
|
111
|
+
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
112
|
+
}
|
|
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
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
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 log_store_1 = require("../src-ts/log-store");
|
|
12
|
+
(0, node_test_1.default)("日志保留重复事件并支持分页", async () => {
|
|
13
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-logs-"));
|
|
14
|
+
try {
|
|
15
|
+
const store = new log_store_1.LogStore(root, 1024 * 1024);
|
|
16
|
+
store.append({ time: "2026-01-01T00:00:00.000Z", level: "info", source: "测试", message: "重复" });
|
|
17
|
+
store.append({ time: "2026-01-01T00:00:01.000Z", level: "info", source: "测试", message: "重复" });
|
|
18
|
+
await store.flush();
|
|
19
|
+
const page = await store.page(1, 1);
|
|
20
|
+
strict_1.default.equal(page.total, 2);
|
|
21
|
+
strict_1.default.equal(page.logs.length, 1);
|
|
22
|
+
strict_1.default.equal(page.hasNext, true);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
@@ -17,19 +17,32 @@ 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", "aun", "doubao", "dsh", "qwenwork"]);
|
|
20
|
+
strict_1.default.deepEqual(records.map((record) => record.id).sort(), ["abweb", "aun", "doubao", "dsh", "feishu", "qwenwork", "workbuddy"]);
|
|
21
21
|
strict_1.default.equal(records.find((record) => record.id === "aun")?.type, "channel");
|
|
22
22
|
const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0 });
|
|
23
23
|
const address = await server.start();
|
|
24
24
|
try {
|
|
25
25
|
await manager.start("abweb");
|
|
26
|
+
await manager.start("feishu");
|
|
26
27
|
const page = await fetch(`http://${address.host}:${address.port}/abweb/index.html`);
|
|
27
28
|
strict_1.default.equal(page.status, 200);
|
|
28
29
|
strict_1.default.match(await page.text(), /mab 管理/);
|
|
30
|
+
const plugins = await (await fetch(`http://${address.host}:${address.port}/api/plugins`)).json();
|
|
31
|
+
strict_1.default.equal(plugins.plugins.find((plugin) => plugin.id === "feishu")?.manifest.managementPage, "/abweb/channels/feishu");
|
|
32
|
+
const feishuPage = await fetch(`http://${address.host}:${address.port}/abweb/channels/feishu`);
|
|
33
|
+
strict_1.default.equal(feishuPage.status, 200);
|
|
34
|
+
strict_1.default.match(await feishuPage.text(), /飞书机器人/);
|
|
35
|
+
const oldAids = await fetch(`http://${address.host}:${address.port}/api/aids`);
|
|
36
|
+
strict_1.default.equal(oldAids.status, 200);
|
|
37
|
+
strict_1.default.equal((await oldAids.json()).canCreate, false);
|
|
38
|
+
await manager.stop("feishu");
|
|
39
|
+
strict_1.default.equal((await fetch(`http://${address.host}:${address.port}/abweb/channels/feishu`)).status, 404);
|
|
29
40
|
const unavailable = await fetch(`http://${address.host}:${address.port}/v1/models`);
|
|
30
41
|
strict_1.default.equal(unavailable.status, 404);
|
|
31
42
|
}
|
|
32
43
|
finally {
|
|
44
|
+
if (manager.get("feishu")?.record.status === "ready")
|
|
45
|
+
await manager.stop("feishu");
|
|
33
46
|
await manager.stop("abweb");
|
|
34
47
|
await server.stop();
|
|
35
48
|
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
@@ -41,6 +54,32 @@ const qwenwork_1 = require("../src-ts/plugins/qwenwork");
|
|
|
41
54
|
strict_1.default.equal(dsh_1.dshManifest.id, "dsh");
|
|
42
55
|
strict_1.default.equal(doubao_1.doubaoManifest.id, "doubao");
|
|
43
56
|
});
|
|
57
|
+
(0, node_test_1.default)("新 Channel 只通过清单和自身路由提供管理页面", async () => {
|
|
58
|
+
const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-channel-"));
|
|
59
|
+
const pluginDir = (0, node_path_1.join)(state, "plugins", "sample");
|
|
60
|
+
await (0, promises_1.mkdir)(pluginDir, { recursive: true });
|
|
61
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(pluginDir, "plugin.json"), JSON.stringify({ id: "sample", version: "1.0.0", type: "channel", displayName: "示例通道", entry: "index.js", apiVersion: "1", capabilities: ["channel"], managementPage: "/abweb/channels/sample" }));
|
|
62
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(pluginDir, "index.js"), `module.exports = class { async activate(context) { this.dispose = context.http.register({ method: "GET", path: "/abweb/channels/sample", handler: (_request, response) => { response.statusCode = 200; response.end("示例通道设置"); } }); } async deactivate() { this.dispose(); } async health() { return { status: "ready" }; } async handle() { throw new Error("不支持"); } }`);
|
|
63
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(state, "plugins"), stateDir: (0, node_path_1.join)(state, "state") });
|
|
64
|
+
const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0 });
|
|
65
|
+
try {
|
|
66
|
+
await manager.discover();
|
|
67
|
+
const address = await server.start();
|
|
68
|
+
await manager.start("sample");
|
|
69
|
+
const base = `http://${address.host}:${address.port}`;
|
|
70
|
+
const plugins = await (await fetch(`${base}/api/plugins`)).json();
|
|
71
|
+
strict_1.default.equal(plugins.plugins.find((plugin) => plugin.id === "sample")?.manifest.managementPage, "/abweb/channels/sample");
|
|
72
|
+
strict_1.default.equal(await (await fetch(`${base}/abweb/channels/sample`)).text(), "示例通道设置");
|
|
73
|
+
await manager.stop("sample");
|
|
74
|
+
strict_1.default.equal((await fetch(`${base}/abweb/channels/sample`)).status, 404);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (manager.get("sample")?.record.status === "ready")
|
|
78
|
+
await manager.stop("sample");
|
|
79
|
+
await server.stop();
|
|
80
|
+
await (0, promises_1.rm)(state, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
44
83
|
(0, node_test_1.default)("doubao plugin forwards a request through its client", async () => {
|
|
45
84
|
const calls = [];
|
|
46
85
|
const client = {
|
|
@@ -29,3 +29,11 @@ const plugin_manifest_1 = require("../src-ts/plugin-manifest");
|
|
|
29
29
|
strict_1.default.equal((0, plugin_manifest_1.validateManifest)({ ...base, capabilities: ["channel", "channel.receive", "channel.send"], type: "channel" }).type, "channel");
|
|
30
30
|
strict_1.default.throws(() => (0, plugin_manifest_1.validateManifest)({ ...base, type: "channel" }), /类型|type/);
|
|
31
31
|
});
|
|
32
|
+
(0, node_test_1.default)("Channel 管理页只能声明本插件的同源路径", () => {
|
|
33
|
+
const base = { id: "sample", version: "1.0.0", type: "channel", entry: "index.js", apiVersion: "1", capabilities: ["channel"] };
|
|
34
|
+
strict_1.default.equal((0, plugin_manifest_1.validateManifest)({ ...base, managementPage: "/abweb/channels/sample" }).managementPage, "/abweb/channels/sample");
|
|
35
|
+
for (const managementPage of ["https://other.invalid/page", "/abweb/channels/other", "/abweb/channels/sample?token=1", "/abweb/channels/sample/../other"]) {
|
|
36
|
+
strict_1.default.throws(() => (0, plugin_manifest_1.validateManifest)({ ...base, managementPage }), /管理页面/);
|
|
37
|
+
}
|
|
38
|
+
strict_1.default.throws(() => (0, plugin_manifest_1.validateManifest)({ ...base, type: "agent", capabilities: ["chat"], managementPage: "/abweb/channels/sample" }), /管理页面/);
|
|
39
|
+
});
|