mu-agent-bridge 1.0.19 → 1.0.20

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") {
@@ -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.20",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {