mu-agent-bridge 1.0.43 → 1.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src-ts/cli.js +41 -0
- package/dist/src-ts/control-server.js +4 -1
- package/dist/src-ts/core-updater.js +77 -0
- 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 +54 -19
- package/dist/tests-ts/control-server.test.js +3 -1
- package/dist/tests-ts/core-updater.test.js +86 -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
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ mab start
|
|
|
77
77
|
abweb 插件目录从 npm 搜索带 `mu-agent-bridge-plugin` 关键词的包,按包元数据中的
|
|
78
78
|
`agentBridge.type` 展示 Agent、Channel 和管理插件;安装后以 `plugin.json` 的类型和能力
|
|
79
79
|
再次校验,不根据包名推断类型。搜索结果有收录延迟,可在页面直接输入 `agent-bridge-*`
|
|
80
|
-
包名安装。已安装插件按清单能力兼容旧版分类。AUN 通道插件包名为 `agent-bridge-aun
|
|
80
|
+
包名安装。已安装插件按清单能力兼容旧版分类。AUN 通道插件包名为 `agent-bridge-aun`,安装后在 AUN 身份页手动关联每个 AID;未关联的 AID 不会自动投递,关联操作只修改指定 AID,不影响其它身份。`workbuddy` 插件包名为 `agent-bridge-workbuddy`,显式关联到 WorkBuddy 后即可按对端保持 CodeBuddy 会话。默认问答使用独立的 `@tencent-ai/codebuddy-code` CLI,须单独登录;没有 CLI 时使用 WorkBuddy 桌面端 CDP,并默认自动启动桌面应用。CLI 未登录时不会自动切换到桌面端账号;可设置 `transport` 为 `cdp` 显式使用桌面端。
|
|
81
81
|
|
|
82
82
|
插件仍在 daemon 的同一个 Node.js 进程内运行。修改插件入口依赖后应重启 daemon,避免模块缓存
|
|
83
83
|
影响新代码。
|
package/dist/src-ts/cli.js
CHANGED
|
@@ -48,6 +48,8 @@ const bridge_1 = require("./bridge");
|
|
|
48
48
|
const plugin_manager_1 = require("./plugin-manager");
|
|
49
49
|
const daemon_process_1 = require("./daemon-process");
|
|
50
50
|
const cli_commands_1 = require("./cli-commands");
|
|
51
|
+
const core_updater_1 = require("./core-updater");
|
|
52
|
+
const log_store_1 = require("./log-store");
|
|
51
53
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
52
54
|
let activeManager;
|
|
53
55
|
const logger = {
|
|
@@ -170,6 +172,31 @@ async function startDaemon() {
|
|
|
170
172
|
const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.resolve)(__dirname, "cli.js"), "start"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
171
173
|
child.unref();
|
|
172
174
|
}
|
|
175
|
+
async function startUpdatedDaemon() {
|
|
176
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.resolve)(__dirname, "cli.js"), "start"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
177
|
+
let launchFailed = false;
|
|
178
|
+
child.once("error", () => { launchFailed = true; });
|
|
179
|
+
child.unref();
|
|
180
|
+
const healthy = async () => {
|
|
181
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
182
|
+
if (launchFailed || child.exitCode !== null || child.signalCode !== null)
|
|
183
|
+
return false;
|
|
184
|
+
const address = await daemonAddress();
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch(`http://127.0.0.1:${address.port}/api/health`, {
|
|
187
|
+
headers: process.env.AGENT_BRIDGE_TOKEN ? { authorization: `Bearer ${process.env.AGENT_BRIDGE_TOKEN}` } : undefined,
|
|
188
|
+
signal: AbortSignal.timeout(1000),
|
|
189
|
+
});
|
|
190
|
+
if (response.ok && (await response.json()).status === "ok")
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
catch { /* daemon 尚未就绪 */ }
|
|
194
|
+
await new Promise((done) => setTimeout(done, 1000));
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
};
|
|
198
|
+
return { child, healthy };
|
|
199
|
+
}
|
|
173
200
|
async function stop() {
|
|
174
201
|
await stopDaemon();
|
|
175
202
|
console.log("旧 daemon 已停止");
|
|
@@ -344,6 +371,20 @@ async function main() {
|
|
|
344
371
|
if (args[0] === "--self-test")
|
|
345
372
|
return selfTest(args[1] ?? "hi");
|
|
346
373
|
if (args[0] === "--serve" || args[0] === "start") {
|
|
374
|
+
const logs = new log_store_1.LogStore((0, node_path_1.resolve)(stateDirectory(), "logs"));
|
|
375
|
+
let launched;
|
|
376
|
+
const updated = await (0, core_updater_1.updateCoreAtStartup)({
|
|
377
|
+
packageRoot: (0, node_path_1.resolve)(__dirname, "..", ".."),
|
|
378
|
+
stateDir: stateDirectory(),
|
|
379
|
+
stopDaemon: () => stopDaemon(false),
|
|
380
|
+
startDaemon: async () => { launched = await startUpdatedDaemon(); },
|
|
381
|
+
waitForHealth: async () => launched?.healthy() ?? false,
|
|
382
|
+
log: (level, message, fields) => logs.append({ time: new Date().toISOString(), level, source: "核心升级", message, fields }),
|
|
383
|
+
});
|
|
384
|
+
if (updated) {
|
|
385
|
+
await logs.flush();
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
347
388
|
await stopDaemon(false);
|
|
348
389
|
return serve();
|
|
349
390
|
}
|
|
@@ -300,10 +300,13 @@ class ControlServer {
|
|
|
300
300
|
const identity = this.identity();
|
|
301
301
|
if (body.aid && !identity)
|
|
302
302
|
return this.json(res, 409, { error: "请先安装并启动 AUN 插件,才能关联 AID" });
|
|
303
|
+
if (!body.aid && identity)
|
|
304
|
+
return this.json(res, 400, { error: "请使用 AUN 身份页指定要解除关联的 AID" });
|
|
303
305
|
try {
|
|
304
306
|
if (identity)
|
|
305
307
|
await identity.bind(id, body.aid, this.address.port);
|
|
306
|
-
|
|
308
|
+
else
|
|
309
|
+
await this.options.manager.setAid(id, body.aid);
|
|
307
310
|
this.options.manager.log("info", "AID 关联已保存", { aid: body.aid, pluginId: id });
|
|
308
311
|
}
|
|
309
312
|
catch (error) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateCoreAtStartup = updateCoreAtStartup;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_util_1 = require("node:util");
|
|
8
|
+
const version_manager_1 = require("./version-manager");
|
|
9
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
10
|
+
function normalizePath(value) {
|
|
11
|
+
const path = (0, node_path_1.resolve)(value).replace(/[\\/]+$/, "");
|
|
12
|
+
return process.platform === "win32" ? path.toLowerCase() : path;
|
|
13
|
+
}
|
|
14
|
+
async function sameDirectory(left, right) {
|
|
15
|
+
try {
|
|
16
|
+
return normalizePath(await (0, promises_1.realpath)(left)) === normalizePath(await (0, promises_1.realpath)(right));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return normalizePath(left) === normalizePath(right);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function runNpm(args) {
|
|
23
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
24
|
+
const { stdout } = await execFileAsync(command, args, { timeout: 180_000, maxBuffer: 4 * 1024 * 1024, ...(process.platform === "win32" ? { shell: true } : {}) });
|
|
25
|
+
return stdout.trim();
|
|
26
|
+
}
|
|
27
|
+
async function updateCoreAtStartup(options) {
|
|
28
|
+
const npm = options.npm ?? runNpm;
|
|
29
|
+
try {
|
|
30
|
+
const globalRoot = options.globalRoot ?? await npm(["root", "-g"]);
|
|
31
|
+
const packageRoot = (0, node_path_1.resolve)(options.packageRoot);
|
|
32
|
+
if (!await sameDirectory(packageRoot, (0, node_path_1.join)(globalRoot, "mu-agent-bridge")))
|
|
33
|
+
return false;
|
|
34
|
+
const metadata = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(packageRoot, "package.json"), "utf8"));
|
|
35
|
+
if (metadata.name !== "mu-agent-bridge" || !metadata.version)
|
|
36
|
+
throw new Error("当前全局核心包信息无效");
|
|
37
|
+
const rawVersion = await npm(["view", "mu-agent-bridge", "version", "--json"]);
|
|
38
|
+
const latestVersion = (() => { try {
|
|
39
|
+
return String(JSON.parse(rawVersion));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return rawVersion.replace(/^['\"]|['\"]$/g, "");
|
|
43
|
+
} })();
|
|
44
|
+
if ((0, version_manager_1.compareVersions)(latestVersion, metadata.version) <= 0)
|
|
45
|
+
return false;
|
|
46
|
+
options.log?.("info", "发现 daemon 新版本,开始自动升级", { currentVersion: metadata.version, latestVersion });
|
|
47
|
+
await (0, promises_1.mkdir)(options.stateDir, { recursive: true });
|
|
48
|
+
const backupRoot = await (0, promises_1.mkdtemp)((0, node_path_1.join)(options.stateDir, "mab-core-update-"));
|
|
49
|
+
const backup = (0, node_path_1.join)(backupRoot, "package");
|
|
50
|
+
await (0, promises_1.cp)(packageRoot, backup, { recursive: true });
|
|
51
|
+
try {
|
|
52
|
+
await options.stopDaemon();
|
|
53
|
+
await npm(["install", "-g", `mu-agent-bridge@${latestVersion}`, "--no-fund", "--no-audit"]);
|
|
54
|
+
const installed = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(packageRoot, "package.json"), "utf8"));
|
|
55
|
+
if (installed.name !== "mu-agent-bridge" || installed.version !== latestVersion)
|
|
56
|
+
throw new Error(`核心安装结果不匹配:${installed.version ?? "未知版本"}`);
|
|
57
|
+
await options.startDaemon();
|
|
58
|
+
if (!await options.waitForHealth())
|
|
59
|
+
throw new Error("新 daemon 健康检查失败");
|
|
60
|
+
options.log?.("info", "daemon 自动升级成功", { version: latestVersion });
|
|
61
|
+
await (0, promises_1.rm)(backupRoot, { recursive: true, force: true });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
await options.stopDaemon().catch(() => undefined);
|
|
66
|
+
await (0, promises_1.rm)(packageRoot, { recursive: true, force: true });
|
|
67
|
+
await (0, promises_1.cp)(backup, packageRoot, { recursive: true });
|
|
68
|
+
options.log?.("error", "daemon 自动升级失败,已恢复旧版本", { error: error instanceof Error ? error.message : String(error), version: metadata.version });
|
|
69
|
+
await (0, promises_1.rm)(backupRoot, { recursive: true, force: true });
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
options.log?.("warn", "daemon 自动升级检查失败,继续使用当前版本", { error: error instanceof Error ? error.message : String(error) });
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -41,7 +41,7 @@ const cliCommands = [
|
|
|
41
41
|
{ name: "default-owner", summary: "查询或设置默认 Owner AID", usage: "mab aun default-owner [set <owner-aid>]", examples: ["mab aun default-owner", "mab aun default-owner set owner.agentid.cn"] },
|
|
42
42
|
{ name: "create", summary: "创建并连接 AID,可选关联 Agent", usage: "mab aun create <aid> [--owner <owner-aid>] [--agent <plugin-id>]", examples: ["mab aun create new.agentid.cn --owner owner.agentid.cn --agent qwenwork"] },
|
|
43
43
|
{ name: "bind", summary: "将 AID 关联到 Agent", usage: "mab aun bind <aid> <plugin-id>", examples: ["mab aun bind new.agentid.cn qwenwork"] },
|
|
44
|
-
{ name: "unbind", summary: "
|
|
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 {
|