mu-agent-bridge 1.0.43 → 1.0.44

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.
@@ -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) {
@@ -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 {
@@ -387,6 +387,36 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
387
387
  await (0, promises_1.rm)(root, { recursive: true, force: true });
388
388
  }
389
389
  });
390
+ (0, node_test_1.default)("AUN 重绑和解绑只修改指定 AID,同一 Agent 的其它 AID 保持不变", async () => {
391
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-binding-isolation-"));
392
+ const aids = ["first.agentid.cn", "second.agentid.cn"];
393
+ const sdk = {
394
+ AIDStore: class {
395
+ list() { return { ok: true, data: { identities: aids.map((aid) => ({ aid })) } }; }
396
+ load(aid) { return { ok: true, data: { aid } }; }
397
+ close() { }
398
+ },
399
+ AUNClient: class {
400
+ on() { return { unsubscribe() { } }; }
401
+ async connect() { }
402
+ async close() { }
403
+ },
404
+ };
405
+ const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish: async () => undefined } });
406
+ try {
407
+ await plugin.activate({ dataDir: root, config: { bindings: { [aids[0]]: "qwenwork", [aids[1]]: "qwenwork" }, binding_times: { [aids[0]]: "old-1", [aids[1]]: "old-2" } }, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
408
+ await plugin.identity.bind("workbuddy", aids[0], 26107);
409
+ strict_1.default.deepEqual((await plugin.health()).details?.bindings, { [aids[0]]: "workbuddy", [aids[1]]: "qwenwork" });
410
+ await plugin.identity.unbind("workbuddy", aids[0], 26107);
411
+ strict_1.default.deepEqual((await plugin.health()).details?.bindings, { [aids[1]]: "qwenwork" });
412
+ const saved = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8"));
413
+ strict_1.default.deepEqual(saved.binding_times, { [aids[1]]: "old-2" });
414
+ }
415
+ finally {
416
+ await plugin.deactivate("stop");
417
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
418
+ }
419
+ });
390
420
  (0, node_test_1.default)("AUN 关联保留 ControlServer 的业务错误并回读状态", async () => {
391
421
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-cli-business-error-"));
392
422
  const routes = new Map();
@@ -55,6 +55,145 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
55
55
  const client = new cdp_client_1.WorkbuddyCdpClient({ fetchImpl: async () => new Response(JSON.stringify([{ type: "page", title: "Other App", webSocketDebuggerUrl: "ws://127.0.0.1:9229/devtools/page/1" }])) });
56
56
  await strict_1.default.rejects(() => client.connect(), /未找到 WorkBuddy 页面 target/);
57
57
  });
58
+ (0, node_test_1.default)("workbuddy 积分领取只点击免费领取按钮并跳过购买入口", async () => {
59
+ const clicked = [];
60
+ const buttons = [
61
+ ["每日签到", "每日签到领取积分"], ["领取积分", "积分奖励"], ["限时加购", "积分购买"],
62
+ ["购买积分", "积分商城"], ["去完成", "每日积分任务"], ["签到", "通知设置"], ["去完成", "充值积分任务"],
63
+ ].map(([label, context]) => ({
64
+ innerText: label,
65
+ textContent: label,
66
+ ariaLabel: "",
67
+ title: "",
68
+ disabled: false,
69
+ getAttribute: (name) => name === "aria-label" ? "" : null,
70
+ click: () => clicked.push(label),
71
+ getClientRects: () => [{}],
72
+ parentElement: { innerText: context, parentElement: null },
73
+ }));
74
+ const client = new cdp_client_1.WorkbuddyCdpClient();
75
+ const internal = client;
76
+ internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, {
77
+ document: { querySelectorAll: () => buttons, querySelector: () => null },
78
+ });
79
+ const result = await internal.claimDailyPoints();
80
+ strict_1.default.deepEqual([...result.clicked], ["每日签到", "领取积分", "去完成"]);
81
+ strict_1.default.equal(result.skipped, 3);
82
+ strict_1.default.deepEqual(clicked, ["每日签到", "领取积分", "去完成"]);
83
+ });
84
+ (0, node_test_1.default)("workbuddy 积分领取先切到 Buddy 加油站并点击每日立即领取", async () => {
85
+ const clicks = [];
86
+ const card = { innerText: "1024 造物挑战开启!" };
87
+ const action = {
88
+ innerText: "立即投稿",
89
+ disabled: false,
90
+ getAttribute: (name) => name === "data-track-props" ? JSON.stringify({ index: 1, type: "notice" }) : null,
91
+ getClientRects: () => [{}],
92
+ click: () => clicks.push("action"),
93
+ };
94
+ const dots = ["Buddy加油站", "活动通知", "成长计划"].map((label, index) => ({
95
+ getAttribute: (name) => name === "aria-label" ? label : null,
96
+ click: () => {
97
+ clicks.push(`dot-${index}`);
98
+ card.innerText = "Buddy加油站\n每日可领 100 积分";
99
+ action.innerText = "立即领取";
100
+ action.getAttribute = (name) => name === "data-track-props" ? JSON.stringify({ index: 0, type: "checkin" }) : null;
101
+ },
102
+ }));
103
+ const root = { querySelector: (selector) => selector === "#rw" ? card : selector === "#go" ? action : null, querySelectorAll: (selector) => selector === ".dot" ? dots : [] };
104
+ const client = new cdp_client_1.WorkbuddyCdpClient();
105
+ const internal = client;
106
+ internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: { querySelectorAll: () => [], querySelector: () => ({ shadowRoot: root }) } });
107
+ const result = await internal.claimDailyPoints();
108
+ strict_1.default.deepEqual([...result.clicked], ["Buddy加油站:立即领取"]);
109
+ strict_1.default.deepEqual(clicks, ["dot-0", "action"]);
110
+ });
111
+ (0, node_test_1.default)("workbuddy 每日积分调度同一天只执行一次", async () => {
112
+ let claims = 0;
113
+ const client = {
114
+ connect: async () => undefined, close() { }, connected: () => true,
115
+ claimDailyPoints: async () => { claims++; return { clicked: ["每日签到"], skipped: 0 }; },
116
+ direct: async () => "回复",
117
+ };
118
+ const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-daily-points-"));
119
+ const plugin = new workbuddy_1.WorkbuddyPlugin({ client });
120
+ try {
121
+ await plugin.activate({ dataDir, config: { transport: "cdp", dailyPoints: true, dailyPointsHour: 0, dailyPointsMinute: 0 }, logger: { info() { }, warn() { }, error() { } } });
122
+ for (let index = 0; index < 100 && claims === 0; index++)
123
+ await new Promise(resolve => setTimeout(resolve, 5));
124
+ strict_1.default.equal(claims, 1);
125
+ await plugin.deactivate("stop");
126
+ const restarted = new workbuddy_1.WorkbuddyPlugin({ client });
127
+ await restarted.activate({ dataDir, config: { transport: "cdp", dailyPoints: true, dailyPointsHour: 0, dailyPointsMinute: 0 }, logger: { info() { }, warn() { }, error() { } } });
128
+ await new Promise(resolve => setTimeout(resolve, 20));
129
+ strict_1.default.equal(claims, 1);
130
+ await restarted.deactivate("stop");
131
+ }
132
+ finally {
133
+ await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
134
+ }
135
+ });
136
+ (0, node_test_1.default)("workbuddy 每日积分检查失败后同一天重启会重试", async () => {
137
+ let attempts = 0;
138
+ const client = {
139
+ connect: async () => undefined, close() { }, connected: () => true,
140
+ claimDailyPoints: async () => {
141
+ attempts++;
142
+ if (attempts === 1)
143
+ throw new Error("CDP 暂时不可用");
144
+ return { clicked: ["每日签到"], skipped: 0 };
145
+ },
146
+ direct: async () => "回复",
147
+ };
148
+ const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-daily-points-retry-"));
149
+ const config = { transport: "cdp", dailyPoints: true, dailyPointsHour: 0, dailyPointsMinute: 0 };
150
+ const logger = { info() { }, warn() { }, error() { } };
151
+ try {
152
+ const first = new workbuddy_1.WorkbuddyPlugin({ client });
153
+ await first.activate({ dataDir, config, logger });
154
+ for (let index = 0; index < 100 && attempts === 0; index++)
155
+ await new Promise(resolve => setTimeout(resolve, 5));
156
+ strict_1.default.equal(attempts, 1);
157
+ await first.deactivate("stop");
158
+ const restarted = new workbuddy_1.WorkbuddyPlugin({ client });
159
+ await restarted.activate({ dataDir, config, logger });
160
+ for (let index = 0; index < 100 && attempts === 1; index++)
161
+ await new Promise(resolve => setTimeout(resolve, 5));
162
+ strict_1.default.equal(attempts, 2);
163
+ await restarted.deactivate("stop");
164
+ }
165
+ finally {
166
+ await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
167
+ }
168
+ });
169
+ (0, node_test_1.default)("workbuddy 只有进入任务入口时重启仍会重试领取", async () => {
170
+ let attempts = 0;
171
+ const client = {
172
+ connect: async () => undefined, close() { }, connected: () => true,
173
+ claimDailyPoints: async () => { attempts++; return { clicked: ["去完成"], skipped: 0 }; },
174
+ direct: async () => "回复",
175
+ };
176
+ const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-daily-points-entry-"));
177
+ const config = { transport: "cdp", dailyPoints: true, dailyPointsHour: 0, dailyPointsMinute: 0 };
178
+ const logger = { info() { }, warn() { }, error() { } };
179
+ try {
180
+ const first = new workbuddy_1.WorkbuddyPlugin({ client });
181
+ await first.activate({ dataDir, config, logger });
182
+ for (let index = 0; index < 100 && attempts === 0; index++)
183
+ await new Promise(resolve => setTimeout(resolve, 5));
184
+ strict_1.default.equal(attempts, 1);
185
+ await first.deactivate("stop");
186
+ const restarted = new workbuddy_1.WorkbuddyPlugin({ client });
187
+ await restarted.activate({ dataDir, config, logger });
188
+ for (let index = 0; index < 100 && attempts === 1; index++)
189
+ await new Promise(resolve => setTimeout(resolve, 5));
190
+ strict_1.default.equal(attempts, 2);
191
+ await restarted.deactivate("stop");
192
+ }
193
+ finally {
194
+ await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
195
+ }
196
+ });
58
197
  (0, node_test_1.default)("workbuddy 插件通过已连接 CDP 返回桌面端回复", async () => {
59
198
  const calls = [];
60
199
  const plugin = new workbuddy_1.WorkbuddyPlugin({ client: { connect: async () => undefined, close() { }, connected: () => true, createTask: async () => undefined, direct: async (text) => { calls.push(text); return "WB_DESKTOP_REPLY"; } } });
@@ -927,14 +1066,15 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
927
1066
  await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
928
1067
  }
929
1068
  });
930
- (0, node_test_1.default)("workbuddy 重载只恢复仍指向同一桌面任务的绑定", async () => {
1069
+ (0, node_test_1.default)("workbuddy 重载后恢复原桌面任务并继续同一会话", async () => {
931
1070
  const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-active-session-"));
932
1071
  let creates = 0;
933
1072
  let desktopId = "desktop-a";
934
1073
  const client = {
935
1074
  connect: async () => undefined, close() { }, connected: () => true,
936
1075
  conversationId: async () => desktopId,
937
- createTask: async () => { creates++; }, direct: async (task) => task,
1076
+ activateConversation: async (id) => { desktopId = id; },
1077
+ createTask: async () => { creates++; desktopId = `desktop-${creates}`; }, direct: async (task) => task,
938
1078
  };
939
1079
  const context = { dataDir, config: { transport: "cdp" }, logger: { info() { }, warn() { }, error() { } } };
940
1080
  try {
@@ -950,7 +1090,9 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
950
1090
  desktopId = "desktop-b";
951
1091
  const third = new workbuddy_1.WorkbuddyPlugin({ client });
952
1092
  await third.activate(context);
953
- await strict_1.default.rejects(() => third.handle({ task: "不应串会话", sessionId: "session-a" }), /不支持恢复旧 WorkBuddy 会话/);
1093
+ strict_1.default.equal((await third.handle({ task: "恢复原任务后继续", sessionId: "session-a" })).text, "恢复原任务后继续");
1094
+ strict_1.default.equal(desktopId, "desktop-1");
1095
+ strict_1.default.equal(creates, 1);
954
1096
  await third.deactivate("stop");
955
1097
  }
956
1098
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -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;
@@ -9,7 +9,10 @@
9
9
  "cdpPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 9229 },
10
10
  "autoStart": { "type": "boolean", "default": true, "description": "未连接时自动启动 WorkBuddy" },
11
11
  "startupTimeoutMs": { "type": "integer", "minimum": 1000, "default": 30000 },
12
- "timeoutMs": { "type": "integer", "minimum": 1, "default": 3600000, "description": "单次回复等待时间(毫秒),默认一小时;超时不终止桌面任务" }
12
+ "timeoutMs": { "type": "integer", "minimum": 1, "default": 3600000, "description": "单次回复等待时间(毫秒),默认一小时;超时不终止桌面任务" },
13
+ "dailyPoints": { "type": "boolean", "default": true, "description": "每天自动检查并领取页面明确提供的免费积分" },
14
+ "dailyPointsHour": { "type": "integer", "minimum": 0, "maximum": 23, "default": 9, "description": "每日免费积分领取时间(本机小时)" },
15
+ "dailyPointsMinute": { "type": "integer", "minimum": 0, "maximum": 59, "default": 0, "description": "每日免费积分领取时间(本机分钟)" }
13
16
  },
14
17
  "additionalProperties": false
15
18
  }
@@ -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 {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-bridge-workbuddy",
3
3
  "version": "1.1.14",
4
- "description": "agent-bridge 的 CodeBuddy CLI 插件。",
4
+ "description": "mu-agent-bridge 的 WorkBuddy 桌面端插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
7
7
  "mu-agent-bridge-agent"