mu-agent-bridge 1.0.43 → 1.0.45

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
@@ -77,7 +77,7 @@ mab start
77
77
  abweb 插件目录从 npm 搜索带 `mu-agent-bridge-plugin` 关键词的包,按包元数据中的
78
78
  `agentBridge.type` 展示 Agent、Channel 和管理插件;安装后以 `plugin.json` 的类型和能力
79
79
  再次校验,不根据包名推断类型。搜索结果有收录延迟,可在页面直接输入 `agent-bridge-*`
80
- 包名安装。已安装插件按清单能力兼容旧版分类。AUN 通道插件包名为 `agent-bridge-aun`,安装后配置 AID 并启动即可;来信默认交给千问办公,也可通过 `agent_plugin` 指定其他已安装 Agent。`workbuddy` 插件包名为 `agent-bridge-workbuddy`,将 AUN 的 `agent_plugin` 设为 `workbuddy` 即可按对端保持 CodeBuddy 会话。默认问答使用独立的 `@tencent-ai/codebuddy-code` CLI,须单独登录;没有 CLI 时使用 WorkBuddy 桌面端 CDP,并默认自动启动桌面应用。CLI 未登录时不会自动切换到桌面端账号;可设置 `transport` 为 `cdp` 显式使用桌面端。
80
+ 包名安装。已安装插件按清单能力兼容旧版分类。AUN 通道插件包名为 `agent-bridge-aun`,安装后在 AUN 身份页手动关联每个 AID;未关联的 AID 不会自动投递,关联操作只修改指定 AID,不影响其它身份。`workbuddy` 插件包名为 `agent-bridge-workbuddy`,显式关联到 WorkBuddy 后即可按对端保持 CodeBuddy 会话。默认问答使用独立的 `@tencent-ai/codebuddy-code` CLI,须单独登录;没有 CLI 时使用 WorkBuddy 桌面端 CDP,并默认自动启动桌面应用。CLI 未登录时不会自动切换到桌面端账号;可设置 `transport` 为 `cdp` 显式使用桌面端。
81
81
 
82
82
  插件仍在 daemon 的同一个 Node.js 进程内运行。修改插件入口依赖后应重启 daemon,避免模块缓存
83
83
  影响新代码。
@@ -48,6 +48,8 @@ const bridge_1 = require("./bridge");
48
48
  const plugin_manager_1 = require("./plugin-manager");
49
49
  const daemon_process_1 = require("./daemon-process");
50
50
  const cli_commands_1 = require("./cli-commands");
51
+ const core_updater_1 = require("./core-updater");
52
+ const log_store_1 = require("./log-store");
51
53
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
52
54
  let activeManager;
53
55
  const logger = {
@@ -170,6 +172,31 @@ async function startDaemon() {
170
172
  const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.resolve)(__dirname, "cli.js"), "start"], { detached: true, stdio: "ignore", windowsHide: true });
171
173
  child.unref();
172
174
  }
175
+ async function startUpdatedDaemon() {
176
+ const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.resolve)(__dirname, "cli.js"), "start"], { detached: true, stdio: "ignore", windowsHide: true });
177
+ let launchFailed = false;
178
+ child.once("error", () => { launchFailed = true; });
179
+ child.unref();
180
+ const healthy = async () => {
181
+ for (let attempt = 0; attempt < 30; attempt++) {
182
+ if (launchFailed || child.exitCode !== null || child.signalCode !== null)
183
+ return false;
184
+ const address = await daemonAddress();
185
+ try {
186
+ const response = await fetch(`http://127.0.0.1:${address.port}/api/health`, {
187
+ headers: process.env.AGENT_BRIDGE_TOKEN ? { authorization: `Bearer ${process.env.AGENT_BRIDGE_TOKEN}` } : undefined,
188
+ signal: AbortSignal.timeout(1000),
189
+ });
190
+ if (response.ok && (await response.json()).status === "ok")
191
+ return true;
192
+ }
193
+ catch { /* daemon 尚未就绪 */ }
194
+ await new Promise((done) => setTimeout(done, 1000));
195
+ }
196
+ return false;
197
+ };
198
+ return { child, healthy };
199
+ }
173
200
  async function stop() {
174
201
  await stopDaemon();
175
202
  console.log("旧 daemon 已停止");
@@ -344,6 +371,20 @@ async function main() {
344
371
  if (args[0] === "--self-test")
345
372
  return selfTest(args[1] ?? "hi");
346
373
  if (args[0] === "--serve" || args[0] === "start") {
374
+ const logs = new log_store_1.LogStore((0, node_path_1.resolve)(stateDirectory(), "logs"));
375
+ let launched;
376
+ const updated = await (0, core_updater_1.updateCoreAtStartup)({
377
+ packageRoot: (0, node_path_1.resolve)(__dirname, "..", ".."),
378
+ stateDir: stateDirectory(),
379
+ stopDaemon: () => stopDaemon(false),
380
+ startDaemon: async () => { launched = await startUpdatedDaemon(); },
381
+ waitForHealth: async () => launched?.healthy() ?? false,
382
+ log: (level, message, fields) => logs.append({ time: new Date().toISOString(), level, source: "核心升级", message, fields }),
383
+ });
384
+ if (updated) {
385
+ await logs.flush();
386
+ return 0;
387
+ }
347
388
  await stopDaemon(false);
348
389
  return serve();
349
390
  }
@@ -300,10 +300,13 @@ class ControlServer {
300
300
  const identity = this.identity();
301
301
  if (body.aid && !identity)
302
302
  return this.json(res, 409, { error: "请先安装并启动 AUN 插件,才能关联 AID" });
303
+ if (!body.aid && identity)
304
+ return this.json(res, 400, { error: "请使用 AUN 身份页指定要解除关联的 AID" });
303
305
  try {
304
306
  if (identity)
305
307
  await identity.bind(id, body.aid, this.address.port);
306
- await this.options.manager.setAid(id, body.aid);
308
+ else
309
+ await this.options.manager.setAid(id, body.aid);
307
310
  this.options.manager.log("info", "AID 关联已保存", { aid: body.aid, pluginId: id });
308
311
  }
309
312
  catch (error) {
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updateCoreAtStartup = updateCoreAtStartup;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const node_child_process_1 = require("node:child_process");
7
+ const node_util_1 = require("node:util");
8
+ const version_manager_1 = require("./version-manager");
9
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
10
+ function normalizePath(value) {
11
+ const path = (0, node_path_1.resolve)(value).replace(/[\\/]+$/, "");
12
+ return process.platform === "win32" ? path.toLowerCase() : path;
13
+ }
14
+ async function sameDirectory(left, right) {
15
+ try {
16
+ return normalizePath(await (0, promises_1.realpath)(left)) === normalizePath(await (0, promises_1.realpath)(right));
17
+ }
18
+ catch {
19
+ return normalizePath(left) === normalizePath(right);
20
+ }
21
+ }
22
+ async function runNpm(args) {
23
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
24
+ const { stdout } = await execFileAsync(command, args, { timeout: 180_000, maxBuffer: 4 * 1024 * 1024, ...(process.platform === "win32" ? { shell: true } : {}) });
25
+ return stdout.trim();
26
+ }
27
+ async function updateCoreAtStartup(options) {
28
+ const npm = options.npm ?? runNpm;
29
+ try {
30
+ const globalRoot = options.globalRoot ?? await npm(["root", "-g"]);
31
+ const packageRoot = (0, node_path_1.resolve)(options.packageRoot);
32
+ if (!await sameDirectory(packageRoot, (0, node_path_1.join)(globalRoot, "mu-agent-bridge")))
33
+ return false;
34
+ const metadata = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(packageRoot, "package.json"), "utf8"));
35
+ if (metadata.name !== "mu-agent-bridge" || !metadata.version)
36
+ throw new Error("当前全局核心包信息无效");
37
+ const rawVersion = await npm(["view", "mu-agent-bridge", "version", "--json"]);
38
+ const latestVersion = (() => { try {
39
+ return String(JSON.parse(rawVersion));
40
+ }
41
+ catch {
42
+ return rawVersion.replace(/^['\"]|['\"]$/g, "");
43
+ } })();
44
+ if ((0, version_manager_1.compareVersions)(latestVersion, metadata.version) <= 0)
45
+ return false;
46
+ options.log?.("info", "发现 daemon 新版本,开始自动升级", { currentVersion: metadata.version, latestVersion });
47
+ await (0, promises_1.mkdir)(options.stateDir, { recursive: true });
48
+ const backupRoot = await (0, promises_1.mkdtemp)((0, node_path_1.join)(options.stateDir, "mab-core-update-"));
49
+ const backup = (0, node_path_1.join)(backupRoot, "package");
50
+ await (0, promises_1.cp)(packageRoot, backup, { recursive: true });
51
+ try {
52
+ await options.stopDaemon();
53
+ await npm(["install", "-g", `mu-agent-bridge@${latestVersion}`, "--no-fund", "--no-audit"]);
54
+ const installed = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(packageRoot, "package.json"), "utf8"));
55
+ if (installed.name !== "mu-agent-bridge" || installed.version !== latestVersion)
56
+ throw new Error(`核心安装结果不匹配:${installed.version ?? "未知版本"}`);
57
+ await options.startDaemon();
58
+ if (!await options.waitForHealth())
59
+ throw new Error("新 daemon 健康检查失败");
60
+ options.log?.("info", "daemon 自动升级成功", { version: latestVersion });
61
+ await (0, promises_1.rm)(backupRoot, { recursive: true, force: true });
62
+ return true;
63
+ }
64
+ catch (error) {
65
+ await options.stopDaemon().catch(() => undefined);
66
+ await (0, promises_1.rm)(packageRoot, { recursive: true, force: true });
67
+ await (0, promises_1.cp)(backup, packageRoot, { recursive: true });
68
+ options.log?.("error", "daemon 自动升级失败,已恢复旧版本", { error: error instanceof Error ? error.message : String(error), version: metadata.version });
69
+ await (0, promises_1.rm)(backupRoot, { recursive: true, force: true });
70
+ return false;
71
+ }
72
+ }
73
+ catch (error) {
74
+ options.log?.("warn", "daemon 自动升级检查失败,继续使用当前版本", { error: error instanceof Error ? error.message : String(error) });
75
+ return false;
76
+ }
77
+ }
@@ -41,7 +41,7 @@ const cliCommands = [
41
41
  { name: "default-owner", summary: "查询或设置默认 Owner AID", usage: "mab aun default-owner [set <owner-aid>]", examples: ["mab aun default-owner", "mab aun default-owner set owner.agentid.cn"] },
42
42
  { name: "create", summary: "创建并连接 AID,可选关联 Agent", usage: "mab aun create <aid> [--owner <owner-aid>] [--agent <plugin-id>]", examples: ["mab aun create new.agentid.cn --owner owner.agentid.cn --agent qwenwork"] },
43
43
  { name: "bind", summary: "将 AID 关联到 Agent", usage: "mab aun bind <aid> <plugin-id>", examples: ["mab aun bind new.agentid.cn qwenwork"] },
44
- { name: "unbind", summary: "解除 Agent 与 AID 的关联", usage: "mab aun unbind <plugin-id>", examples: ["mab aun unbind qwenwork"] },
44
+ { name: "unbind", summary: "解除指定 AID 与 Agent 的关联", usage: "mab aun unbind <aid>", examples: ["mab aun unbind old.agentid.cn"] },
45
45
  ];
46
46
  class AunCommandError extends Error {
47
47
  statusCode;
@@ -140,11 +140,6 @@ class AunPlugin {
140
140
  await this.connectAid(aid);
141
141
  if (!this.clients.has(aid))
142
142
  throw new Error("AID 当前未连接");
143
- for (const [identity, target] of Object.entries(this.bindings))
144
- if (target === pluginId) {
145
- delete this.bindings[identity];
146
- delete this.bindingTimes[identity];
147
- }
148
143
  this.bindings[aid] = pluginId;
149
144
  }
150
145
  else {
@@ -157,21 +152,11 @@ class AunPlugin {
157
152
  await this.connectAid(aid);
158
153
  if (!this.clients.has(aid))
159
154
  throw new Error("AID 当前未连接");
160
- for (const [identity, target] of Object.entries(this.bindings))
161
- if (target === pluginId) {
162
- delete this.bindings[identity];
163
- delete this.bindingTimes[identity];
164
- }
165
155
  this.bindings[aid] = pluginId;
166
156
  }
167
157
  }
168
- else {
169
- for (const [identity, target] of Object.entries(this.bindings))
170
- if (target === pluginId) {
171
- delete this.bindings[identity];
172
- delete this.bindingTimes[identity];
173
- }
174
- }
158
+ else
159
+ throw new Error("必须指定要关联的 AID");
175
160
  if (aid)
176
161
  this.bindingTimes[aid] = new Date().toISOString();
177
162
  await this.persist();
@@ -186,6 +171,29 @@ class AunPlugin {
186
171
  }
187
172
  this.context?.logger.info("AUN 身份关联已更新", { aid, pluginId });
188
173
  },
174
+ unbind: async (pluginId, aid) => {
175
+ if (!aidPattern.test(aid))
176
+ throw new Error("AID 格式无效");
177
+ if (this.bindings[aid] !== pluginId)
178
+ throw new Error("指定 AID 当前未关联到该 Agent");
179
+ const previous = this.bindings;
180
+ const previousTimes = this.bindingTimes;
181
+ this.bindings = { ...previous };
182
+ this.bindingTimes = { ...previousTimes };
183
+ delete this.bindings[aid];
184
+ delete this.bindingTimes[aid];
185
+ try {
186
+ await this.persist();
187
+ }
188
+ catch (error) {
189
+ this.bindings = previous;
190
+ this.bindingTimes = previousTimes;
191
+ this.config.bindings = previous;
192
+ this.config.binding_times = previousTimes;
193
+ throw error;
194
+ }
195
+ this.context?.logger.info("AUN 单个身份关联已解除", { aid, pluginId });
196
+ },
189
197
  };
190
198
  async persist() {
191
199
  this.config.identities = [...this.identities].sort();
@@ -236,6 +244,7 @@ class AunPlugin {
236
244
  ["POST", "/api/plugins/aun/aid", this.createAid],
237
245
  ["GET", "/api/plugins/aun/identities", this.listIdentities],
238
246
  ["POST", "/api/plugins/aun/bind", this.bindAgent],
247
+ ["POST", "/api/plugins/aun/unbind", this.unbindAgent],
239
248
  ["POST", "/api/plugins/aun/settings", this.saveSettings],
240
249
  ["GET", "/api/plugins/aun/commands", this.listCommands],
241
250
  ["POST", "/api/plugins/aun/commands", this.runCommand],
@@ -455,6 +464,28 @@ class AunPlugin {
455
464
  this.json(response, 400, { error: safeError(error) });
456
465
  }
457
466
  };
467
+ unbindAgent = async (request, response) => {
468
+ try {
469
+ let raw = "";
470
+ for await (const chunk of request) {
471
+ raw += chunk;
472
+ if (raw.length > 65536)
473
+ throw new Error("请求内容过大");
474
+ }
475
+ const body = JSON.parse(raw || "{}");
476
+ const aid = typeof body.aid === "string" ? body.aid.trim() : "";
477
+ if (!aidPattern.test(aid))
478
+ return this.json(response, 400, { error: "AID 格式无效" });
479
+ const pluginId = this.bindings[aid];
480
+ if (!pluginId)
481
+ return this.json(response, 404, { error: "指定 AID 当前未关联 Agent" });
482
+ await this.identity.unbind(pluginId, aid, Number(this.context?.runtime?.ports?.aun ?? 0));
483
+ this.json(response, 200, { ok: true, aid, pluginId });
484
+ }
485
+ catch (error) {
486
+ this.json(response, 400, { error: safeError(error) });
487
+ }
488
+ };
458
489
  listCommands = async (_request, response) => {
459
490
  response.setHeader("cache-control", "no-store");
460
491
  this.json(response, 200, { protocolVersion: 1, plugin: "aun", version: manifest.version, commands: cliCommands });
@@ -515,7 +546,6 @@ class AunPlugin {
515
546
  throw new AunCommandError("AID 格式无效");
516
547
  this.validatePluginId(pluginId);
517
548
  await this.requireAgent(pluginId);
518
- const replacedAids = (await this.agentBindings(pluginId)).filter((previousAid) => previousAid !== aid);
519
549
  try {
520
550
  await this.patchAgentAid(pluginId, aid);
521
551
  }
@@ -524,23 +554,25 @@ class AunPlugin {
524
554
  throw new AunCommandError(`AID 关联请求失败,已回读当前状态:${safeError(error)}`, error instanceof AunCommandError ? error.statusCode : 502, { aid, agent: pluginId, ...(bindingState ? { bindingState } : { bindingState: "unknown" }) });
525
555
  }
526
556
  return {
527
- message: replacedAids.length ? `AID ${replacedAids.join("、")} 已替换为 ${aid} 并关联到 ${pluginId}` : `AID 已关联到 ${pluginId}`,
528
- data: { aid, agent: pluginId, replacedAids },
557
+ message: `AID ${aid} 已关联到 ${pluginId}`,
558
+ data: { aid, agent: pluginId },
529
559
  };
530
560
  }
531
561
  if (command === "unbind" && args.length === 1) {
532
- const [pluginId] = args;
533
- this.validatePluginId(pluginId);
534
- await this.requireAgent(pluginId, false);
535
- const unboundAids = await this.agentBindings(pluginId);
562
+ const [aid] = args;
563
+ if (!aidPattern.test(aid))
564
+ throw new AunCommandError("AID 格式无效");
565
+ const pluginId = this.bindings[aid];
566
+ if (!pluginId)
567
+ throw new AunCommandError("指定 AID 当前未关联 Agent");
536
568
  try {
537
- await this.patchAgentAid(pluginId, "");
569
+ await this.identity.unbind(pluginId, aid, 0);
538
570
  }
539
571
  catch (error) {
540
572
  const bindingState = await this.bindingState(pluginId).catch(() => undefined);
541
- throw new AunCommandError(`解除关联请求失败,已回读当前状态:${safeError(error)}`, error instanceof AunCommandError ? error.statusCode : 502, { agent: pluginId, ...(bindingState ? { bindingState } : { bindingState: "unknown" }) });
573
+ throw new AunCommandError(`解除关联请求失败,已回读当前状态:${safeError(error)}`, error instanceof AunCommandError ? error.statusCode : 502, { aid, agent: pluginId, ...(bindingState ? { bindingState } : { bindingState: "unknown" }) });
542
574
  }
543
- return { message: `已解除 ${pluginId} 的 AID 关联`, data: { agent: pluginId, unboundAids } };
575
+ return { message: `已解除 AID ${aid} 与 ${pluginId} 的关联`, data: { aid, agent: pluginId } };
544
576
  }
545
577
  if (!["status", "list", "agents", "default-owner", "create", "bind", "unbind"].includes(command ?? ""))
546
578
  throw new AunCommandError(`未知 AUN 命令:${command ?? ""}`);
@@ -604,10 +636,8 @@ class AunPlugin {
604
636
  const data = { created: true, aid, connection: created.connection ?? "ready" };
605
637
  if (created.connection !== "ready")
606
638
  throw new AunCommandError("AID 已创建,但连接失败;请先执行 mab aun list 确认,不要重试创建", 502, { ...data, connectionError: created.connectionError });
607
- let replacedAids = [];
608
639
  if (agent) {
609
640
  try {
610
- replacedAids = (await this.agentBindings(agent)).filter((previousAid) => previousAid !== aid);
611
641
  await this.patchAgentAid(agent, aid);
612
642
  }
613
643
  catch (error) {
@@ -616,8 +646,8 @@ class AunPlugin {
616
646
  }
617
647
  }
618
648
  return {
619
- message: agent ? (replacedAids.length ? `AID ${replacedAids.join("、")} 已替换为 ${aid} 并关联到 ${agent}` : `AID 已创建并关联到 ${agent}`) : "AID 已创建并连接",
620
- data: { ...data, ...(agent ? { agent, replacedAids } : {}) },
649
+ message: agent ? `AID 已创建并关联到 ${agent}` : "AID 已创建并连接",
650
+ data: { ...data, ...(agent ? { agent } : {}) },
621
651
  };
622
652
  }
623
653
  async agents(readyOnly = true) {
@@ -28,7 +28,6 @@
28
28
  const [identities, plugins, config] = await Promise.all([request("/api/plugins/aun/identities"), request("/api/plugins"), request("/api/plugins/aun/config")]);
29
29
  agents = (plugins.plugins || []).filter((plugin) => plugin.type === "agent");
30
30
  if (refreshForm) createForm.elements.ownerAid.value = config.owner_aid || "";
31
- await Promise.all(agents.map(async (agent) => { agent.configAid = (await request(`/api/plugins/${encodeURIComponent(agent.id)}/config`)).aid || ""; }));
32
31
  if (refreshForm) {
33
32
  for (const key of ["allow_from", "aun_path"]) configForm.elements[key].value = config[key] || "";
34
33
  configForm.elements.encryption_seed.value = "";
@@ -40,7 +39,7 @@
40
39
  for (const aid of values) {
41
40
  if (seen.has(aid)) continue;
42
41
  seen.add(aid);
43
- entries.push({ aid, label, group, bound: identities.bindings?.[aid] || agents.find((agent) => agent.id && agent.configAid === aid)?.id || "", time: Date.parse(identities.bindingTimes?.[aid] || "") || 0 });
42
+ entries.push({ aid, label, group, bound: identities.bindings?.[aid] || "", time: Date.parse(identities.bindingTimes?.[aid] || "") || 0 });
44
43
  }
45
44
  }
46
45
  entries.sort((a, b) => Number(Boolean(b.bound)) - Number(Boolean(a.bound)) || a.group - b.group || b.time - a.time || a.aid.localeCompare(b.aid));
@@ -49,7 +48,7 @@
49
48
  for (const value of [aid, label, bound || "未关联"]) { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); }
50
49
  const cell = document.createElement("td"); const select = document.createElement("select"); select.setAttribute("aria-label", `${aid} 关联 Agent`);
51
50
  select.append(new Option("选择 Agent", ""), ...agents.map((agent) => new Option(agent.displayName || agent.id, agent.id)));
52
- select.value = bound; select.onchange = () => { if (select.value) void bind(aid, select.value); };
51
+ select.value = bound; select.onchange = () => { if (select.value) void bind(aid, select.value); else if (bound) void unbind(aid); };
53
52
  cell.append(select); row.append(cell); list.append(row);
54
53
  }
55
54
  if (!list.children.length) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = 4; cell.textContent = "暂无已连接 AID"; row.append(cell); list.append(row); }
@@ -63,6 +62,12 @@
63
62
  await load(); notify("关联成功", `${aid} 已关联 ${pluginId}`);
64
63
  } catch (error) { notify("关联失败", errorMessage(error), true); }
65
64
  }
65
+ async function unbind(aid) {
66
+ try {
67
+ await request("/api/plugins/aun/unbind", "POST", { aid });
68
+ await load(); notify("解除关联成功", `${aid} 已解除关联`);
69
+ } catch (error) { notify("解除关联失败", errorMessage(error), true); }
70
+ }
66
71
  createForm.onsubmit = (event) => { event.preventDefault(); const aid = createForm.elements.aid.value.trim(); const owner = createForm.elements.ownerAid.value.trim();
67
72
  show("正在创建 AID 并连接,请稍候……");
68
73
  void request("/api/plugins/aun/aid", "POST", { aid, ownerAid: owner }).then((result) => {
@@ -43,6 +43,11 @@ class FeishuPlugin {
43
43
  create: async (aid) => { if (!aid.trim())
44
44
  throw new Error("机器人标识不能为空"); return { aid, status: "请通过配置接口提供 app_id 和 app_secret" }; },
45
45
  bind: async (pluginId, aid) => { await this.bindBot(aid || String(this.config.app_id || ""), aid ? pluginId : ""); },
46
+ unbind: async (pluginId, aid) => {
47
+ if (this.bindings[aid] !== pluginId)
48
+ throw new FeishuBindingError("指定飞书机器人当前未关联到该 Agent");
49
+ await this.bindBot(aid, "");
50
+ },
46
51
  };
47
52
  async activate(context) {
48
53
  this.context = context;
@@ -34,6 +34,20 @@ class WorkbuddyCdpClient {
34
34
  const value = await this.evaluate(`([...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_')))?.getAttribute('data-conversation-id')`);
35
35
  return typeof value === "string" && value.trim() ? value : undefined;
36
36
  }
37
+ async activateConversation(conversationId) {
38
+ const id = conversationId.trim();
39
+ if (!id)
40
+ throw new Error("WorkBuddy 会话标识不能为空");
41
+ const clicked = await this.evaluate(`(() => {
42
+ const item = [...document.querySelectorAll('[data-conversation-id]')].find(element => element.getAttribute('data-conversation-id') === ${JSON.stringify(id)});
43
+ const button = item?.matches('[role="button"],button') ? item : item?.closest('[role="button"],button');
44
+ if (!button || button.getAttribute('aria-disabled') === 'true' || button.disabled) return false;
45
+ button.click(); return true;
46
+ })()`);
47
+ if (clicked !== true)
48
+ throw new Error(`WorkBuddy 未找到会话 ${id}`);
49
+ await this.waitFor(`(() => [...document.querySelectorAll('[data-conversation-id]')].some(element => element.getAttribute('data-conversation-id') === ${JSON.stringify(id)} && String(element.className).includes('_selected_')))()`, "WorkBuddy 原会话切换未完成");
50
+ }
37
51
  async readHistorySnapshot() {
38
52
  return await this.evaluate(`(() => {
39
53
  const selected = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'));
@@ -104,6 +118,43 @@ class WorkbuddyCdpClient {
104
118
  if (temporaryPaths?.length)
105
119
  await this.cleanupTemporaryFiles(temporaryPaths);
106
120
  }
121
+ async claimDailyPoints() {
122
+ return await this.evaluate(`(() => {
123
+ const buttons = [...document.querySelectorAll('button,[role="button"]')];
124
+ const clicked = [];
125
+ let skipped = 0;
126
+ for (const button of buttons) {
127
+ const label = [...new Set([button.innerText, button.textContent, button.getAttribute('aria-label'), button.getAttribute('title')].filter(Boolean))].join(' ').normalize('NFKC').replace(/\\s+/g, ' ').trim();
128
+ if (!label || !button.getClientRects().length || button.disabled || button.getAttribute('aria-disabled') === 'true') continue;
129
+ let parent = button.parentElement;
130
+ let context = '';
131
+ for (let depth = 0; parent && depth < 5; depth++, parent = parent.parentElement) {
132
+ const text = (parent.innerText || parent.textContent || '').normalize('NFKC').replace(/\\s+/g, ' ').trim();
133
+ if (/(积分|签到|奖励|任务)/.test(text)) { context = text; break; }
134
+ }
135
+ if (!context) continue;
136
+ if (/购买|加购|充值|升级|开通|兑换|抽奖/.test(label + context)) { skipped++; continue; }
137
+ if (!/(签到|领取(?:积分|奖励)?|收下奖励|去完成)/.test(label)) continue;
138
+ button.click(); clicked.push(label);
139
+ }
140
+ const carousel = document.querySelector('.wb-slot--avatar-top-combo')?.shadowRoot;
141
+ const dots = [...(carousel?.querySelectorAll('.dot') ?? [])];
142
+ const active = dots.find((dot) => dot.getAttribute('data-on') === '1');
143
+ for (const dot of dots) {
144
+ dot.click();
145
+ const card = carousel?.querySelector('#rw')?.innerText ?? '';
146
+ const claim = carousel?.querySelector('#go');
147
+ let props;
148
+ try { props = JSON.parse(claim?.getAttribute('data-track-props') ?? '{}'); } catch { props = {}; }
149
+ if (/Buddy加油站/.test(card) && /每日可领/.test(card) && claim?.innerText?.trim() === '立即领取' && props.index === 0 && props.type === 'checkin' && !claim.disabled && claim.getAttribute('aria-disabled') !== 'true') {
150
+ claim.click(); clicked.push('Buddy加油站:立即领取');
151
+ break;
152
+ }
153
+ }
154
+ if (!clicked.includes('Buddy加油站:立即领取')) active?.click();
155
+ return { clicked, skipped };
156
+ })()`);
157
+ }
107
158
  async direct(text, timeoutMs = 3_600_000, expert, newTask = false) {
108
159
  return (await this.complete({ task: text, timeoutMs }, expert, newTask)).text;
109
160
  }
@@ -15,7 +15,7 @@ const session_history_1 = require("./session-history");
15
15
  const desktop_launch_1 = require("../desktop-launch");
16
16
  const desktop_launch_2 = require("./desktop-launch");
17
17
  exports.workbuddyManifest = {
18
- id: "workbuddy", version: "1.1.13", displayName: "WorkBuddy", type: "agent",
18
+ id: "workbuddy", version: "1.1.14", displayName: "WorkBuddy", type: "agent",
19
19
  entry: "dist/index.js", apiVersion: "1", process: "in-process",
20
20
  capabilities: ["chat", "health"], configSchema: "config.schema.json",
21
21
  };
@@ -49,11 +49,17 @@ class WorkbuddyPlugin {
49
49
  queue = Promise.resolve();
50
50
  pending = 0;
51
51
  activeSessionId;
52
+ /** 重载后待恢复的会话身份。 */
53
+ recoverableSessionId;
54
+ recoverableConversationId;
52
55
  activeDesktopConversationId;
53
56
  desktopSessionValid = true;
54
57
  knownSessions = new Set();
55
58
  sessionDataDir;
56
59
  sessionWrites = Promise.resolve();
60
+ dailyPointsTimer;
61
+ dailyPointsDataDir;
62
+ dailyPointsLogger;
57
63
  failure;
58
64
  expertSession = false;
59
65
  activeExpert;
@@ -73,12 +79,16 @@ class WorkbuddyPlugin {
73
79
  this.cliPath = (0, cli_client_1.findCli)(typeof config.cliPath === "string" ? config.cliPath : undefined);
74
80
  this.failure = undefined;
75
81
  this.activeSessionId = undefined;
82
+ this.recoverableSessionId = undefined;
83
+ this.recoverableConversationId = undefined;
76
84
  this.activeDesktopConversationId = undefined;
77
85
  this.activeExpert = undefined;
78
86
  this.expertSession = false;
79
87
  this.desktopSessionValid = true;
80
88
  this.knownSessions.clear();
81
89
  this.sessionDataDir = context.dataDir;
90
+ this.dailyPointsDataDir = context.dataDir;
91
+ this.dailyPointsLogger = context.logger;
82
92
  let savedBinding;
83
93
  if (this.sessionDataDir) {
84
94
  const saved = await this.loadKnownSessions(this.sessionDataDir);
@@ -128,8 +138,22 @@ class WorkbuddyPlugin {
128
138
  this.activeDesktopConversationId = savedBinding.conversationId;
129
139
  this.activeExpert = savedBinding.expert;
130
140
  }
141
+ else if (savedBinding && this.sessionOperations.length) {
142
+ this.recoverableSessionId = savedBinding.sessionId;
143
+ this.recoverableConversationId = savedBinding.conversationId;
144
+ }
145
+ if (config.dailyPoints !== false && this.client?.claimDailyPoints) {
146
+ const hour = typeof config.dailyPointsHour === "number" ? config.dailyPointsHour : 9;
147
+ const minute = typeof config.dailyPointsMinute === "number" ? config.dailyPointsMinute : 0;
148
+ if (!Number.isInteger(hour) || hour < 0 || hour > 23 || !Number.isInteger(minute) || minute < 0 || minute > 59)
149
+ throw new Error("积分领取时间无效");
150
+ this.scheduleDailyPoints(hour, minute);
151
+ }
131
152
  }
132
153
  async deactivate(_reason) {
154
+ if (this.dailyPointsTimer)
155
+ clearTimeout(this.dailyPointsTimer);
156
+ this.dailyPointsTimer = undefined;
133
157
  await this.queue;
134
158
  await this.sessionWrites.catch(() => { });
135
159
  await (0, desktop_launch_1.terminateOwnedProcess)(this.process).catch(() => undefined);
@@ -138,6 +162,59 @@ class WorkbuddyPlugin {
138
162
  this.process = undefined;
139
163
  this.reconnect = undefined;
140
164
  }
165
+ scheduleDailyPoints(hour, minute) {
166
+ const now = new Date();
167
+ const scheduled = new Date(now);
168
+ scheduled.setHours(hour, minute, 0, 0);
169
+ const due = scheduled <= now;
170
+ const arm = () => {
171
+ const next = new Date();
172
+ next.setHours(hour, minute, 0, 0);
173
+ if (next <= new Date())
174
+ next.setDate(next.getDate() + 1);
175
+ this.dailyPointsTimer = setTimeout(() => { void run(); }, Math.max(1, next.getTime() - Date.now()));
176
+ };
177
+ const run = async () => {
178
+ if (!this.dailyPointsDataDir || !this.client?.claimDailyPoints)
179
+ return;
180
+ const date = new Date().toLocaleDateString("sv-SE");
181
+ const file = (0, node_path_1.join)(this.dailyPointsDataDir, "daily-points.json");
182
+ try {
183
+ const state = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
184
+ if (state.date === date) {
185
+ arm();
186
+ return;
187
+ }
188
+ }
189
+ catch (error) {
190
+ if (error.code !== "ENOENT") {
191
+ this.dailyPointsLogger?.error("WorkBuddy 每日积分状态读取失败", { error: error instanceof Error ? error.message : String(error) });
192
+ arm();
193
+ return;
194
+ }
195
+ }
196
+ try {
197
+ await (0, promises_1.mkdir)(this.dailyPointsDataDir, { recursive: true });
198
+ const result = await this.enqueue(() => this.client.claimDailyPoints());
199
+ if (result.clicked.some(label => /(签到|领取(?:积分|奖励)?|收下奖励)/.test(label))) {
200
+ await (0, promises_1.writeFile)(file, JSON.stringify({ date }), { mode: 0o600 });
201
+ }
202
+ this.dailyPointsLogger?.info("WorkBuddy 每日免费积分领取检查完成", result);
203
+ }
204
+ catch (error) {
205
+ this.dailyPointsLogger?.error("WorkBuddy 每日积分领取失败", { error: error instanceof Error ? error.message : String(error) });
206
+ }
207
+ finally {
208
+ arm();
209
+ }
210
+ };
211
+ if (due) {
212
+ this.dailyPointsTimer = setTimeout(() => { void run(); }, 1);
213
+ }
214
+ else {
215
+ this.dailyPointsTimer = setTimeout(() => { void run(); }, Math.max(1, scheduled.getTime() - Date.now()));
216
+ }
217
+ }
141
218
  async health() {
142
219
  if (this.client?.connected())
143
220
  return { status: "ready", details: { transport: "cdp" } };
@@ -258,8 +335,17 @@ class WorkbuddyPlugin {
258
335
  }
259
336
  return;
260
337
  }
261
- if (this.knownSessions.has(sessionId))
262
- throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
338
+ if (this.knownSessions.has(sessionId)) {
339
+ if (this.recoverableSessionId !== sessionId || !this.recoverableConversationId || !this.client.activateConversation)
340
+ throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
341
+ await this.client.activateConversation(this.recoverableConversationId);
342
+ this.activeSessionId = sessionId;
343
+ this.activeDesktopConversationId = this.recoverableConversationId;
344
+ this.desktopSessionValid = true;
345
+ this.recoverableSessionId = undefined;
346
+ this.recoverableConversationId = undefined;
347
+ return;
348
+ }
263
349
  await this.createSession(sessionId);
264
350
  }
265
351
  async createSession(sessionId, name) {
@@ -274,6 +360,8 @@ class WorkbuddyPlugin {
274
360
  this.expertSession = false;
275
361
  this.activeExpert = undefined;
276
362
  this.activeSessionId = sessionId;
363
+ this.recoverableSessionId = undefined;
364
+ this.recoverableConversationId = undefined;
277
365
  }
278
366
  async loadKnownSessions(directory) {
279
367
  try {