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.
- package/dist/src-ts/control-server.js +4 -1
- package/dist/src-ts/plugins/aun/index.js +62 -32
- package/dist/src-ts/plugins/aun/public/app.js +8 -3
- package/dist/src-ts/plugins/feishu/index.js +5 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +51 -0
- package/dist/src-ts/plugins/workbuddy/index.js +91 -3
- package/dist/tests-ts/aun-plugin.test.js +30 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +145 -3
- package/package.json +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +62 -32
- package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +8 -3
- package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +5 -0
- package/plugins/workbuddy/config.schema.json +4 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +51 -0
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +91 -3
- package/plugins/workbuddy/package.json +1 -1
|
@@ -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
|
-
|
|
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: "
|
|
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
|
-
|
|
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:
|
|
528
|
-
data: { aid, agent: pluginId
|
|
557
|
+
message: `AID ${aid} 已关联到 ${pluginId}`,
|
|
558
|
+
data: { aid, agent: pluginId },
|
|
529
559
|
};
|
|
530
560
|
}
|
|
531
561
|
if (command === "unbind" && args.length === 1) {
|
|
532
|
-
const [
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
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.
|
|
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: `已解除 ${
|
|
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 ?
|
|
620
|
-
data: { ...data, ...(agent ? { agent
|
|
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] ||
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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: "
|
|
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
|
-
|
|
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:
|
|
528
|
-
data: { aid, agent: pluginId
|
|
557
|
+
message: `AID ${aid} 已关联到 ${pluginId}`,
|
|
558
|
+
data: { aid, agent: pluginId },
|
|
529
559
|
};
|
|
530
560
|
}
|
|
531
561
|
if (command === "unbind" && args.length === 1) {
|
|
532
|
-
const [
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
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.
|
|
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: `已解除 ${
|
|
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 ?
|
|
620
|
-
data: { ...data, ...(agent ? { agent
|
|
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] ||
|
|
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.
|
|
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
|
-
|
|
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 {
|