mu-agent-bridge 1.0.19 → 1.0.21

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
@@ -31,7 +31,7 @@ Windows 可运行 `start-mab.ps1` 注册并启动登录时自动运行的计划
31
31
  本机打开 `http://127.0.0.1:26107/abweb/` 无需配置码。局域网管理页可用一次性配置码配对。
32
32
  在 Windows 主机运行 `mab pair`,将显示的 6 位码输入
33
33
  abweb 的“配置码”栏;配对码 10 分钟有效,只能使用一次,连续输错 5 次后需重新执行
34
- `mab pair`。配对成功后登录有效期为 24 小时,浏览器关闭再打开仍可使用;过期后需重新配对。后台重启会使配对登录失效。本机免配对和手动配置的管理令牌按原有规则使用。配置码只能从 Windows 本机的
34
+ `mab pair`。配对成功后登录有效期为 24 小时,浏览器关闭再打开仍可使用;过期后需重新配对。后台重启及升级不会使配对登录提前失效,会话校验信息保存在状态目录的 `paired-sessions.json` 中,磁盘仅保存令牌哈希及原定过期时间;重启不会延长有效期。本机免配对和手动配置的管理令牌按原有规则使用。配置码只能从 Windows 本机的
35
35
  `127.0.0.1` 接口取得,远程浏览器不能读取。
36
36
 
37
37
  环境变量:`AGENT_BRIDGE_PORT`、`AGENT_BRIDGE_HOST`、`AGENT_BRIDGE_TOKEN`、
@@ -182,7 +182,7 @@ async function serve() {
182
182
  const fallbackPorts = (process.env.AGENT_BRIDGE_FALLBACK_PORTS ?? "").split(",").map((value) => Number(value.trim())).filter((value) => Number.isInteger(value) && value >= 0);
183
183
  if (port > 0 && !fallbackPorts.length)
184
184
  fallbackPorts.push(0);
185
- const server = new control_server_1.ControlServer({ manager, host, port, fallbackPorts, token: process.env.AGENT_BRIDGE_TOKEN });
185
+ const server = new control_server_1.ControlServer({ manager, host, port, fallbackPorts, token: process.env.AGENT_BRIDGE_TOKEN, sessionFile: (0, node_path_1.resolve)(stateDirectory(), "paired-sessions.json") });
186
186
  const address = await server.start();
187
187
  const addressFile = (0, node_path_1.resolve)(stateDirectory(), "daemon-address.json");
188
188
  await (0, promises_1.mkdir)(stateDirectory(), { recursive: true });
@@ -4,6 +4,8 @@ exports.ControlServer = exports.listExistingAids = void 0;
4
4
  const node_http_1 = require("node:http");
5
5
  const node_child_process_1 = require("node:child_process");
6
6
  const node_crypto_1 = require("node:crypto");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = require("node:path");
7
9
  const local_aids_1 = require("./local-aids");
8
10
  const openai_adapter_1 = require("./plugins/qwenwork/openai-adapter");
9
11
  var local_aids_2 = require("./local-aids");
@@ -16,6 +18,7 @@ class ControlServer {
16
18
  pairingCode = String((0, node_crypto_1.randomInt)(0, 1_000_000)).padStart(6, "0");
17
19
  pairingExpiresAt = Date.now() + 10 * 60 * 1000;
18
20
  pairedTokens = new Map();
21
+ sessionWrite = Promise.resolve();
19
22
  pairingUsed = false;
20
23
  pairingFailures = 0;
21
24
  constructor(options) {
@@ -23,6 +26,21 @@ class ControlServer {
23
26
  this.server = (0, node_http_1.createServer)((req, res) => { void this.handle(req, res); });
24
27
  }
25
28
  async start() {
29
+ if (this.options.sessionFile) {
30
+ let stored = {};
31
+ try {
32
+ stored = JSON.parse(await (0, promises_1.readFile)(this.options.sessionFile, "utf8"));
33
+ }
34
+ catch (error) {
35
+ if (error.code !== "ENOENT")
36
+ throw error;
37
+ }
38
+ this.pairedTokens.clear();
39
+ for (const [hash, expiry] of Object.entries(stored)) {
40
+ if (/^[a-f0-9]{64}$/.test(hash) && typeof expiry === "number" && Number.isFinite(expiry) && expiry > Date.now())
41
+ this.pairedTokens.set(hash, expiry);
42
+ }
43
+ }
26
44
  const host = this.options.host ?? "127.0.0.1";
27
45
  const candidates = [this.options.port ?? 0, ...(this.options.fallbackPorts ?? [])];
28
46
  for (const port of candidates) {
@@ -54,6 +72,24 @@ class ControlServer {
54
72
  }
55
73
  async stop() { if (!this.server.listening)
56
74
  return; await new Promise((resolve, reject) => this.server.close((error) => error ? reject(error) : resolve())); }
75
+ async saveSessions() {
76
+ const file = this.options.sessionFile;
77
+ if (!file)
78
+ return;
79
+ const pending = this.sessionWrite.catch(() => undefined).then(async () => {
80
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(file), { recursive: true });
81
+ const temporary = `${file}.${(0, node_crypto_1.randomBytes)(8).toString("hex")}.tmp`;
82
+ try {
83
+ await (0, promises_1.writeFile)(temporary, JSON.stringify(Object.fromEntries(this.pairedTokens)), { mode: 0o600 });
84
+ await (0, promises_1.rename)(temporary, file);
85
+ }
86
+ finally {
87
+ await (0, promises_1.rm)(temporary, { force: true });
88
+ }
89
+ });
90
+ this.sessionWrite = pending;
91
+ await pending;
92
+ }
57
93
  async handle(req, res) {
58
94
  try {
59
95
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -84,11 +120,19 @@ class ControlServer {
84
120
  for (const [storedToken, expiry] of this.pairedTokens)
85
121
  if (expiry <= Date.now())
86
122
  this.pairedTokens.delete(storedToken);
87
- this.pairedTokens.set(token, expiresAt);
123
+ const hash = (0, node_crypto_1.createHash)("sha256").update(token).digest("hex");
124
+ this.pairedTokens.set(hash, expiresAt);
125
+ try {
126
+ await this.saveSessions();
127
+ }
128
+ catch (error) {
129
+ this.pairedTokens.delete(hash);
130
+ throw error;
131
+ }
88
132
  return this.json(res, 200, { ok: true, token, expiresAt });
89
133
  }
90
134
  const authorization = req.headers.authorization;
91
- const authorized = authorization === `Bearer ${this.options.token}` || (authorization?.startsWith("Bearer ") && (this.pairedTokens.get(authorization.slice(7)) ?? 0) > Date.now());
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());
92
136
  if (this.options.token && !isLoopback(req.socket.remoteAddress) && !authorized && !(method === "GET" && url.pathname.startsWith("/abweb")))
93
137
  return this.json(res, 401, { error: "unauthorized" });
94
138
  if (parts[0] === "api" && parts[1] === "agents" && parts[2] && parts[3] === "v1") {
@@ -5,7 +5,7 @@ const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const MANIFEST = {
7
7
  id: "abweb",
8
- version: "1.0.5",
8
+ version: "1.0.6",
9
9
  displayName: "Agent Bridge Web",
10
10
  entry: "dist/index.js",
11
11
  apiVersion: "1",
@@ -145,7 +145,6 @@
145
145
  void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
146
146
  }
147
147
  catch (cause) {
148
- node("#access-panel").hidden = false;
149
148
  error(cause instanceof Error ? cause.message : String(cause));
150
149
  }
151
150
  finally {
@@ -0,0 +1,72 @@
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 promises_1 = require("node:fs/promises");
9
+ const node_vm_1 = require("node:vm");
10
+ class Element {
11
+ hidden = true;
12
+ value = "";
13
+ textContent = "";
14
+ options = [];
15
+ selectedIndex = 0;
16
+ onclick;
17
+ replaceChildren() { }
18
+ append() { }
19
+ setAttribute() { }
20
+ }
21
+ (0, node_test_1.default)("ABWeb缓存登录在刷新和后台暂时不可用时保留,到期才重新配对", async () => {
22
+ const script = await (0, promises_1.readFile)("src-ts/plugins/abweb/public/app.js", "utf8");
23
+ let now = Date.now();
24
+ const storage = new Map();
25
+ let unavailable = false;
26
+ const calls = [];
27
+ const open = async () => {
28
+ const nodes = new Map();
29
+ const node = (selector) => { if (!nodes.has(selector))
30
+ nodes.set(selector, new Element()); return nodes.get(selector); };
31
+ (0, node_vm_1.runInNewContext)(script, {
32
+ document: { querySelector: node, createElement: () => new Element(), hidden: false },
33
+ localStorage: { getItem: (key) => storage.get(key) ?? null, setItem: (key, value) => storage.set(key, value), removeItem: (key) => storage.delete(key) },
34
+ Date: class extends Date {
35
+ static now() { return now; }
36
+ },
37
+ setInterval() { }, confirm: () => false,
38
+ fetch: async (path, options) => {
39
+ calls.push({ path, token: options.headers.authorization });
40
+ if (unavailable)
41
+ throw new Error("后台正在重启");
42
+ if (path === "/api/pair")
43
+ return { ok: true, status: 200, json: async () => ({ token: "测试会话", expiresAt: now + 86400000 }) };
44
+ const authorized = options.headers.authorization === "Bearer 测试会话" && Number(storage.get("mab-login-expires-at")) > now;
45
+ return { ok: authorized, status: authorized ? 200 : 401, json: async () => ({ plugins: [], messages: [], sessions: [], mode: "auto-install" }) };
46
+ },
47
+ });
48
+ await new Promise(resolve => setImmediate(resolve));
49
+ return node;
50
+ };
51
+ const first = await open();
52
+ strict_1.default.equal(first("#access-panel").hidden, false);
53
+ first("#pair-code").value = "123456";
54
+ first("#pair-button").onclick();
55
+ await new Promise(resolve => setImmediate(resolve));
56
+ strict_1.default.equal(first("#access-panel").hidden, true);
57
+ const expiry = storage.get("mab-login-expires-at");
58
+ now += 23 * 3600000;
59
+ const refreshed = await open();
60
+ strict_1.default.equal(refreshed("#access-panel").hidden, true);
61
+ unavailable = true;
62
+ const restarting = await open();
63
+ strict_1.default.equal(restarting("#access-panel").hidden, true, "后台暂时不可用不能提示重新配对");
64
+ strict_1.default.equal(storage.get("mab-login-token"), "测试会话");
65
+ unavailable = false;
66
+ strict_1.default.equal((await open())("#access-panel").hidden, true);
67
+ strict_1.default.equal(storage.get("mab-login-expires-at"), expiry);
68
+ now = Number(expiry);
69
+ strict_1.default.equal((await open())("#access-panel").hidden, false);
70
+ strict_1.default.equal(storage.has("mab-login-token"), false);
71
+ strict_1.default.equal(calls.filter(call => call.path === "/api/pair").length, 1);
72
+ });
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -11,6 +44,7 @@ const evolcore_1 = require("../src-ts/plugins/aun/evolcore");
11
44
  const promises_1 = require("node:fs/promises");
12
45
  const node_os_1 = require("node:os");
13
46
  const node_path_1 = require("node:path");
47
+ const node_crypto_1 = require("node:crypto");
14
48
  const plugin_manager_1 = require("../src-ts/plugin-manager");
15
49
  (0, node_test_1.default)("关联 EvolCore AID 时配置相应的 Agent 路由", async () => {
16
50
  const calls = [];
@@ -238,13 +272,49 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
238
272
  return response.statusCode;
239
273
  };
240
274
  strict_1.default.equal(await checkRemote(), 200);
241
- server.pairedTokens.set(token, Date.now() - 1);
275
+ server.pairedTokens.set((0, node_crypto_1.createHash)("sha256").update(token).digest("hex"), Date.now() - 1);
242
276
  strict_1.default.equal(await checkRemote(), 401);
243
277
  }
244
278
  finally {
245
279
  await server.stop();
246
280
  }
247
281
  });
282
+ (0, node_test_1.default)("配对登录重启后保留原24小时期限且过期后拒绝访问", async () => {
283
+ const directory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-login-"));
284
+ const sessionFile = (0, node_path_1.join)(directory, "paired-sessions.json");
285
+ const manager = { list: () => [], dispatchHttp: async () => false };
286
+ const options = { manager, host: "127.0.0.1", port: 0, token: "before", sessionFile };
287
+ let server = new control_server_1.ControlServer(options);
288
+ try {
289
+ const address = await server.start();
290
+ const base = `http://${address.host}:${address.port}`;
291
+ const { code } = await (await fetch(`${base}/api/pair-code`)).json();
292
+ const response = await fetch(`${base}/api/pair`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ code }) });
293
+ const { token, expiresAt } = await response.json();
294
+ const check = async () => {
295
+ const res = { setHeader() { }, end() { } };
296
+ await server.handle({ url: "/api/health", method: "GET", headers: { host: "localhost", authorization: `Bearer ${token}` }, socket: { remoteAddress: "192.168.31.10" } }, res);
297
+ return res.statusCode;
298
+ };
299
+ await server.stop();
300
+ server = new control_server_1.ControlServer({ ...options, token: "after" });
301
+ await server.start();
302
+ strict_1.default.equal(await check(), 200);
303
+ const stored = JSON.parse(await Promise.resolve().then(() => __importStar(require("node:fs/promises"))).then(fs => fs.readFile(sessionFile, "utf8")));
304
+ const hash = (0, node_crypto_1.createHash)("sha256").update(token).digest("hex");
305
+ strict_1.default.equal(stored[hash], expiresAt);
306
+ strict_1.default.ok(!JSON.stringify(stored).includes(token), "磁盘不保存原始登录令牌");
307
+ await server.stop();
308
+ await (0, promises_1.writeFile)(sessionFile, JSON.stringify({ [hash]: Date.now() - 1 }));
309
+ server = new control_server_1.ControlServer(options);
310
+ await server.start();
311
+ strict_1.default.equal(await check(), 401);
312
+ }
313
+ finally {
314
+ await server.stop();
315
+ await (0, promises_1.rm)(directory, { recursive: true, force: true });
316
+ }
317
+ });
248
318
  (0, node_test_1.default)("配对码连续失败五次后必须从本机重新获取", async () => {
249
319
  const manager = { list: () => [], dispatchHttp: async () => false };
250
320
  const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0, token: "secret" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -5,7 +5,7 @@ const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const MANIFEST = {
7
7
  id: "abweb",
8
- version: "1.0.5",
8
+ version: "1.0.6",
9
9
  displayName: "Agent Bridge Web",
10
10
  entry: "dist/index.js",
11
11
  apiVersion: "1",
@@ -145,7 +145,6 @@
145
145
  void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
146
146
  }
147
147
  catch (cause) {
148
- node("#access-panel").hidden = false;
149
148
  error(cause instanceof Error ? cause.message : String(cause));
150
149
  }
151
150
  finally {
@@ -101,7 +101,7 @@
101
101
  sessionTable.append(row);
102
102
  }
103
103
  if (refreshCatalog) void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
104
- } catch (cause) { node<HTMLElement>("#access-panel").hidden = false; error(cause instanceof Error ? cause.message : String(cause)); }
104
+ } catch (cause) { error(cause instanceof Error ? cause.message : String(cause)); }
105
105
  finally { loading = false; }
106
106
  }
107
107
  function renderChannels(items: Plugin[]): void {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "abweb",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "type": "management",
5
5
  "displayName": "Agent Bridge Web",
6
6
  "entry": "dist/src-ts/plugins/abweb/index.js",