mu-agent-bridge 1.0.42 → 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/README.md +4 -0
- package/dist/src-ts/control-server.js +4 -1
- package/dist/src-ts/headless-account.patch.yml +10 -0
- package/dist/src-ts/plugin-manifest.js +3 -1
- package/dist/src-ts/plugins/abweb/public/app.js +34 -0
- package/dist/src-ts/plugins/abweb/public/index.html +10 -0
- package/dist/src-ts/plugins/abweb/public/style.css +3 -0
- package/dist/src-ts/plugins/aun/index.js +67 -37
- package/dist/src-ts/plugins/aun/public/app.js +9 -7
- package/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/dist/src-ts/plugins/claude-cli/index.js +14 -0
- package/dist/src-ts/plugins/codex-cli/index.js +14 -0
- package/dist/src-ts/plugins/feishu/index.js +5 -0
- package/dist/src-ts/plugins/local-cli/agent.js +321 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +51 -0
- package/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
- package/dist/src-ts/plugins/workbuddy/index.js +100 -9
- package/dist/tests-ts/abweb-cli.test.js +65 -0
- package/dist/tests-ts/aun-default-owner-page.test.js +1 -1
- package/dist/tests-ts/aun-plugin.test.js +46 -6
- package/dist/tests-ts/bundled-package.test.js +1 -1
- package/dist/tests-ts/local-cli.test.js +170 -0
- package/dist/tests-ts/platform-integration.test.js +1 -1
- package/dist/tests-ts/workbuddy-plugin.test.js +201 -3
- package/package.json +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +34 -0
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +30 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +10 -0
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +3 -0
- package/plugins/aun/config.schema.json +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +67 -37
- package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +9 -7
- package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/plugins/claude-cli/config.schema.json +37 -0
- package/plugins/claude-cli/dist/src-ts/agent.js +10 -0
- package/plugins/claude-cli/dist/src-ts/plugin.js +13 -0
- package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +14 -0
- package/plugins/claude-cli/dist/src-ts/plugins/local-cli/agent.js +321 -0
- package/plugins/claude-cli/package.json +35 -0
- package/plugins/claude-cli/plugin.json +15 -0
- package/plugins/codex-cli/config.schema.json +37 -0
- package/plugins/codex-cli/dist/src-ts/agent.js +10 -0
- package/plugins/codex-cli/dist/src-ts/plugin.js +13 -0
- package/plugins/codex-cli/dist/src-ts/plugins/codex-cli/index.js +14 -0
- package/plugins/codex-cli/dist/src-ts/plugins/local-cli/agent.js +321 -0
- package/plugins/codex-cli/package.json +35 -0
- package/plugins/codex-cli/plugin.json +15 -0
- package/plugins/dsh/dist/src-ts/headless-account.patch.yml +10 -0
- 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/desktop-launch.js +10 -0
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +100 -9
- package/plugins/workbuddy/package.json +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalCliAgent = void 0;
|
|
4
|
+
exports.resolveWindowsCliShim = resolveWindowsCliShim;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const nativeId = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
11
|
+
const maximumOutput = 4 * 1024 * 1024;
|
|
12
|
+
function diagnostic(value) {
|
|
13
|
+
return value.replace(/\b(?:Bearer\s+)[^\s"']+/gi, "Bearer [已隐藏]")
|
|
14
|
+
.replace(/((?:[\w-]*(?:token|secret|password|api[_-]?key)[\w-]*)["']?\s*[:=]\s*["']?)[^\s"',}]+/gi, "$1[已隐藏]")
|
|
15
|
+
.replace(/\b(?:sk-|npm_)[A-Za-z0-9_-]+/g, "[已隐藏]")
|
|
16
|
+
.replace(/https?:\/\/[^\s"']+/g, text => { try {
|
|
17
|
+
const url = new URL(text);
|
|
18
|
+
return `${url.origin}/[路径已隐藏]`;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "[地址已隐藏]";
|
|
22
|
+
} })
|
|
23
|
+
.slice(0, 2000);
|
|
24
|
+
}
|
|
25
|
+
async function resolveWindowsCliShim(file, provider) {
|
|
26
|
+
const modules = (0, node_path_1.basename)((0, node_path_1.dirname)(file)) === ".bin" ? (0, node_path_1.dirname)((0, node_path_1.dirname)(file)) : (0, node_path_1.join)((0, node_path_1.dirname)(file), "node_modules");
|
|
27
|
+
const root = (0, node_path_1.join)(modules, ...(provider === "codex" ? ["@openai", "codex"] : ["@anthropic-ai", "claude-code"]));
|
|
28
|
+
const metadata = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "package.json"), "utf8"));
|
|
29
|
+
const entry = typeof metadata.bin === "string" ? metadata.bin : metadata.bin?.[provider];
|
|
30
|
+
if (typeof entry !== "string" || !entry)
|
|
31
|
+
throw new Error("npm CLI 包未声明可执行入口");
|
|
32
|
+
const target = (0, node_path_1.resolve)(root, entry), path = (0, node_path_1.relative)(root, target);
|
|
33
|
+
if ((0, node_path_1.isAbsolute)(entry) || path === ".." || path.startsWith(`..${node_path_1.sep}`))
|
|
34
|
+
throw new Error("npm CLI 入口路径超出包目录");
|
|
35
|
+
if (!(await (0, promises_1.stat)(target)).isFile())
|
|
36
|
+
throw new Error("npm CLI 入口不是文件");
|
|
37
|
+
await (0, promises_1.access)(target, node_fs_1.constants.R_OK);
|
|
38
|
+
if ([".js", ".cjs", ".mjs"].includes((0, node_path_1.extname)(target)))
|
|
39
|
+
return { command: process.execPath, args: [target] };
|
|
40
|
+
if ((0, node_path_1.extname)(target).toLowerCase() !== ".exe")
|
|
41
|
+
throw new Error("npm CLI 入口必须是原生 exe 或 Node 脚本");
|
|
42
|
+
return { command: target, args: [] };
|
|
43
|
+
}
|
|
44
|
+
async function executable(provider, configured) {
|
|
45
|
+
const candidates = configured ? [(0, node_path_1.resolve)(configured)] : (process.env.PATH ?? "").split(node_path_1.delimiter).filter(Boolean)
|
|
46
|
+
.flatMap(directory => (process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""]).map(extension => (0, node_path_1.join)(directory, provider + extension)));
|
|
47
|
+
for (const file of candidates) {
|
|
48
|
+
try {
|
|
49
|
+
if (!(await (0, promises_1.stat)(file)).isFile())
|
|
50
|
+
continue;
|
|
51
|
+
if (process.platform === "win32" && [".cmd", ".bat"].includes((0, node_path_1.extname)(file).toLowerCase()))
|
|
52
|
+
return await resolveWindowsCliShim(file, provider);
|
|
53
|
+
if ([".js", ".cjs", ".mjs"].includes((0, node_path_1.extname)(file))) {
|
|
54
|
+
await (0, promises_1.access)(file, node_fs_1.constants.R_OK);
|
|
55
|
+
return { command: process.execPath, args: [file] };
|
|
56
|
+
}
|
|
57
|
+
await (0, promises_1.access)(file, process.platform === "win32" ? node_fs_1.constants.F_OK : node_fs_1.constants.X_OK);
|
|
58
|
+
return { command: file, args: [] };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (configured)
|
|
62
|
+
throw new Error(`无法使用 ${provider} CLI 程序:${diagnostic(String(error))};请检查路径、安装及 daemon 用户的执行权限`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`未找到 ${provider} CLI 可执行程序,请安装并在 daemon 用户下登录,或配置 executablePath`);
|
|
66
|
+
}
|
|
67
|
+
class LocalCliAgent {
|
|
68
|
+
manifest;
|
|
69
|
+
provider;
|
|
70
|
+
sessionOperations = ["new"];
|
|
71
|
+
attachmentInput = false;
|
|
72
|
+
context;
|
|
73
|
+
command;
|
|
74
|
+
cwd = "";
|
|
75
|
+
timeoutMs = 240000;
|
|
76
|
+
ready = false;
|
|
77
|
+
message = "插件尚未启动";
|
|
78
|
+
version = "";
|
|
79
|
+
sessions = new Map();
|
|
80
|
+
writes = Promise.resolve();
|
|
81
|
+
pending = new Map();
|
|
82
|
+
children = new Map();
|
|
83
|
+
constructor(manifest, provider) {
|
|
84
|
+
this.manifest = manifest;
|
|
85
|
+
this.provider = provider;
|
|
86
|
+
}
|
|
87
|
+
async activate(context) {
|
|
88
|
+
this.context = context;
|
|
89
|
+
this.ready = false;
|
|
90
|
+
try {
|
|
91
|
+
const cwd = context.config.cwd;
|
|
92
|
+
if (typeof cwd !== "string" || !cwd.trim() || !(0, node_path_1.isAbsolute)(cwd))
|
|
93
|
+
throw new Error("请配置已存在的绝对工作目录 cwd");
|
|
94
|
+
if (!(await (0, promises_1.stat)(cwd)).isDirectory())
|
|
95
|
+
throw new Error("工作目录 cwd 不是目录");
|
|
96
|
+
this.cwd = cwd;
|
|
97
|
+
const timeout = context.config.timeoutMs ?? 240000;
|
|
98
|
+
if (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout <= 0 || timeout > 2147483647)
|
|
99
|
+
throw new Error("timeoutMs 必须是有效的正整数毫秒数");
|
|
100
|
+
this.timeoutMs = timeout;
|
|
101
|
+
const configured = context.config.executablePath;
|
|
102
|
+
if (configured !== undefined && typeof configured !== "string")
|
|
103
|
+
throw new Error("executablePath 必须是路径字符串");
|
|
104
|
+
this.command = await executable(this.provider, typeof configured === "string" && configured.trim() ? configured : undefined);
|
|
105
|
+
this.version = (await this.run(["--version"], "", 10000)).stdout.trim();
|
|
106
|
+
if (!/\d+\.\d+\.\d+/.test(this.version))
|
|
107
|
+
throw new Error("无法识别 CLI 版本");
|
|
108
|
+
const help = (await this.run(this.provider === "codex" ? ["exec", "--help"] : ["--help"], "", 10000)).stdout;
|
|
109
|
+
if (this.provider === "codex" ? !help.includes("--json") || !help.includes("resume") : !help.includes("--output-format") || !help.includes("--resume"))
|
|
110
|
+
throw new Error("CLI 版本不支持所需的结构化输出和会话恢复,请升级 CLI");
|
|
111
|
+
this.sessions = new Map();
|
|
112
|
+
try {
|
|
113
|
+
const data = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(context.dataDir, "cli-sessions.json"), "utf8"));
|
|
114
|
+
if (data.version !== 1 || !Array.isArray(data.sessions) || data.sessions.some((entry) => !Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || typeof entry[1] !== "string" || !nativeId.test(entry[1])))
|
|
115
|
+
throw new Error("CLI 会话映射文件无效,请检查数据文件,不能安全恢复会话");
|
|
116
|
+
this.sessions = new Map(data.sessions);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.code !== "ENOENT")
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
this.ready = true;
|
|
123
|
+
this.message = "";
|
|
124
|
+
context.logger.info("本地 CLI 插件已就绪", { plugin: this.manifest.id, version: diagnostic(this.version) });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this.message = diagnostic(String(error));
|
|
128
|
+
throw new Error(this.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
run(args, input, timeout) {
|
|
132
|
+
const command = this.command;
|
|
133
|
+
let child;
|
|
134
|
+
let stop;
|
|
135
|
+
const done = new Promise((accept, reject) => {
|
|
136
|
+
let stdout = "", stderr = "", size = 0, failure = "";
|
|
137
|
+
child = (0, node_child_process_1.spawn)(command.command, [...command.args, ...args], { cwd: this.cwd, env: process.env, shell: false, windowsHide: true, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
|
|
138
|
+
const terminate = (reason) => {
|
|
139
|
+
if (failure)
|
|
140
|
+
return;
|
|
141
|
+
failure = reason;
|
|
142
|
+
if (!child.pid)
|
|
143
|
+
return;
|
|
144
|
+
if (process.platform === "win32")
|
|
145
|
+
(0, node_child_process_1.execFile)("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true }, () => { });
|
|
146
|
+
else {
|
|
147
|
+
try {
|
|
148
|
+
process.kill(-child.pid, "SIGKILL");
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
child.kill("SIGKILL");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
stop = () => terminate("插件已停止,当前 CLI 任务已终止");
|
|
156
|
+
const timer = setTimeout(() => terminate(`CLI 调用超时(${timeout}ms),请检查登录、网络或权限审批`), timeout);
|
|
157
|
+
const cleanup = () => { clearTimeout(timer); this.children.delete(child); };
|
|
158
|
+
child.stdout.setEncoding("utf8");
|
|
159
|
+
child.stderr.setEncoding("utf8");
|
|
160
|
+
const collect = (value, error) => {
|
|
161
|
+
size += Buffer.byteLength(value);
|
|
162
|
+
if (size > maximumOutput) {
|
|
163
|
+
terminate("CLI 输出超过 4MB 限制");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (error)
|
|
167
|
+
stderr += value;
|
|
168
|
+
else
|
|
169
|
+
stdout += value;
|
|
170
|
+
};
|
|
171
|
+
child.stdout.on("data", value => collect(value, false));
|
|
172
|
+
child.stderr.on("data", value => collect(value, true));
|
|
173
|
+
child.on("error", error => { cleanup(); reject(new Error(`无法启动 ${this.provider} CLI:${diagnostic(String(error))}`)); });
|
|
174
|
+
child.on("close", (code, signal) => {
|
|
175
|
+
cleanup();
|
|
176
|
+
if (failure) {
|
|
177
|
+
reject(new Error(failure));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (code !== 0) {
|
|
181
|
+
const detail = diagnostic(stderr || stdout);
|
|
182
|
+
const hint = /login|authenticat|not logged|unauthorized|401/i.test(detail) ? "请检查 daemon 运行用户的 CLI 登录状态" : /permission|approval|denied|EACCES/i.test(detail) ? "权限审批受阻,请在本机 CLI 确认目录和工具权限" : "请检查 CLI 配置及网络";
|
|
183
|
+
reject(new Error(`${this.provider} CLI 退出码 ${code ?? signal}:${hint};${detail}`));
|
|
184
|
+
}
|
|
185
|
+
else
|
|
186
|
+
accept({ stdout, stderr });
|
|
187
|
+
});
|
|
188
|
+
child.stdin.on("error", () => { });
|
|
189
|
+
child.stdin.end(input);
|
|
190
|
+
});
|
|
191
|
+
this.children.set(child, { stop, done });
|
|
192
|
+
return done;
|
|
193
|
+
}
|
|
194
|
+
save(session, id) {
|
|
195
|
+
const operation = this.writes.catch(() => { }).then(async () => {
|
|
196
|
+
const next = new Map(this.sessions);
|
|
197
|
+
if (id)
|
|
198
|
+
next.set(session, id);
|
|
199
|
+
else
|
|
200
|
+
next.delete(session);
|
|
201
|
+
const directory = this.context.dataDir;
|
|
202
|
+
await (0, promises_1.mkdir)(directory, { recursive: true, mode: 0o700 });
|
|
203
|
+
const file = (0, node_path_1.join)(directory, "cli-sessions.json"), temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
204
|
+
try {
|
|
205
|
+
await (0, promises_1.writeFile)(temporary, JSON.stringify({ version: 1, sessions: [...next] }), { flag: "wx", mode: 0o600 });
|
|
206
|
+
await (0, promises_1.rename)(temporary, file);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await (0, promises_1.rm)(temporary, { force: true });
|
|
210
|
+
}
|
|
211
|
+
this.sessions = next;
|
|
212
|
+
});
|
|
213
|
+
this.writes = operation;
|
|
214
|
+
return operation;
|
|
215
|
+
}
|
|
216
|
+
serial(key, operation) {
|
|
217
|
+
const next = (this.pending.get(key) ?? Promise.resolve()).catch(() => { }).then(operation);
|
|
218
|
+
this.pending.set(key, next);
|
|
219
|
+
return next.finally(() => { if (this.pending.get(key) === next)
|
|
220
|
+
this.pending.delete(key); });
|
|
221
|
+
}
|
|
222
|
+
requireReady() { if (!this.ready)
|
|
223
|
+
throw new Error(`${this.manifest.displayName} 插件未启动或已停止:${this.message}`); }
|
|
224
|
+
async handle(request) {
|
|
225
|
+
this.requireReady();
|
|
226
|
+
if (request.attachments?.length)
|
|
227
|
+
throw new Error(`${this.manifest.displayName} 本期不支持附件输入`);
|
|
228
|
+
if (!request.task?.trim())
|
|
229
|
+
throw new Error("CLI 任务文本不能为空");
|
|
230
|
+
const logical = request.sessionId || (0, node_crypto_1.randomUUID)();
|
|
231
|
+
return this.serial(logical, async () => {
|
|
232
|
+
this.requireReady();
|
|
233
|
+
const id = request.newSession ? undefined : this.sessions.get(logical);
|
|
234
|
+
const args = this.provider === "codex"
|
|
235
|
+
? ["exec", ...(id ? ["resume", id] : []), "--json", "-"]
|
|
236
|
+
: ["--print", "--output-format", "json", ...(id ? ["--resume", id] : [])];
|
|
237
|
+
const timeout = request.timeoutMs ?? this.timeoutMs;
|
|
238
|
+
if (!Number.isInteger(timeout) || timeout <= 0 || timeout > 2147483647)
|
|
239
|
+
throw new Error("任务超时必须是有效的正整数毫秒数");
|
|
240
|
+
const started = Date.now();
|
|
241
|
+
this.context.logger.info("本地 CLI 任务开始", { plugin: this.manifest.id, sessionId: logical, resume: Boolean(id) });
|
|
242
|
+
try {
|
|
243
|
+
const output = await this.run(args, request.task, timeout);
|
|
244
|
+
const response = this.parse(output.stdout);
|
|
245
|
+
if (id && response.id !== id)
|
|
246
|
+
throw new Error("CLI 恢复返回的会话 ID 与指定会话不一致,保留原会话绑定");
|
|
247
|
+
await this.save(logical, response.id);
|
|
248
|
+
this.context.logger.info("本地 CLI 任务完成", { plugin: this.manifest.id, sessionId: logical, durationMs: Date.now() - started, exitCode: 0 });
|
|
249
|
+
return { text: response.text, sessionId: logical };
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const detail = diagnostic(String(error));
|
|
253
|
+
this.context.logger.error("本地 CLI 任务失败", { plugin: this.manifest.id, sessionId: logical, durationMs: Date.now() - started, error: detail });
|
|
254
|
+
throw new Error(`${this.manifest.displayName}:${detail}`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
parse(stdout) {
|
|
259
|
+
let id = "", text = "";
|
|
260
|
+
try {
|
|
261
|
+
if (this.provider === "codex") {
|
|
262
|
+
let completed = false, lastError = "";
|
|
263
|
+
const messages = [];
|
|
264
|
+
for (const line of stdout.split(/\r?\n/).filter(line => line.trim())) {
|
|
265
|
+
const event = JSON.parse(line);
|
|
266
|
+
if (event.type === "thread.started")
|
|
267
|
+
id = event.thread_id;
|
|
268
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message" && typeof event.item.text === "string")
|
|
269
|
+
messages.push(event.item.text);
|
|
270
|
+
if (event.type === "turn.failed")
|
|
271
|
+
throw new Error(JSON.stringify(event.error ?? event));
|
|
272
|
+
// 中途错误后仍可能恢复,必须以最终完成事件判定成功。
|
|
273
|
+
if (event.type === "error") {
|
|
274
|
+
completed = false;
|
|
275
|
+
lastError = JSON.stringify(event.error ?? event);
|
|
276
|
+
}
|
|
277
|
+
if (event.type === "turn.completed")
|
|
278
|
+
completed = true;
|
|
279
|
+
}
|
|
280
|
+
if (!completed)
|
|
281
|
+
throw new Error(lastError || "缺少完成事件");
|
|
282
|
+
text = messages.join("\n\n");
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const event = JSON.parse(stdout);
|
|
286
|
+
if (event.is_error || event.type !== "result")
|
|
287
|
+
throw new Error(JSON.stringify(event.errors ?? event));
|
|
288
|
+
id = event.session_id;
|
|
289
|
+
text = event.result;
|
|
290
|
+
}
|
|
291
|
+
if (typeof id !== "string" || !nativeId.test(id))
|
|
292
|
+
throw new Error("缺少有效原生会话 ID");
|
|
293
|
+
if (typeof text !== "string" || !text.trim())
|
|
294
|
+
throw new Error("未返回有效回复文本");
|
|
295
|
+
return { id, text };
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
throw new Error(`CLI 输出或会话无效:${diagnostic(String(error))}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async controlSession(request) {
|
|
302
|
+
this.requireReady();
|
|
303
|
+
return this.serial(request.sessionId, async () => {
|
|
304
|
+
this.requireReady();
|
|
305
|
+
await this.save(request.sessionId);
|
|
306
|
+
return { text: "已新建会话,下条消息将使用新的 CLI 会话", sessionId: (0, node_crypto_1.randomUUID)() };
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async health() {
|
|
310
|
+
return { status: this.ready ? "ready" : "failed", ...(this.message ? { message: this.message } : {}), details: { executionMode: "cli", version: diagnostic(this.version), activeRequests: this.children.size } };
|
|
311
|
+
}
|
|
312
|
+
async deactivate(_reason) {
|
|
313
|
+
this.ready = false;
|
|
314
|
+
this.message = "插件已停止";
|
|
315
|
+
const children = [...this.children.values()];
|
|
316
|
+
children.forEach(child => child.stop());
|
|
317
|
+
await Promise.allSettled([...children.map(child => child.done), ...this.pending.values()]);
|
|
318
|
+
await this.writes.catch(() => { });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.LocalCliAgent = LocalCliAgent;
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.launchWorkbuddyDesktop = launchWorkbuddyDesktop;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
function launchWorkbuddyDesktop(executable, args, profileDirectory, port) {
|
|
6
|
+
// WorkBuddy 会覆盖 Chromium 的用户目录参数,必须使用它自己的环境变量隔离实例。
|
|
7
|
+
const env = { ...process.env, WORKBUDDY_USER_DATA_DIR: profileDirectory, WORKBUDDY_REMOTE_DEBUGGING_PORT: String(port) };
|
|
8
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
9
|
+
return (0, node_child_process_1.spawn)(executable, args, { env, detached: true, stdio: "ignore", windowsHide: false });
|
|
10
|
+
}
|
|
@@ -13,8 +13,9 @@ const cdp_client_1 = require("./cdp-client");
|
|
|
13
13
|
const cli_client_1 = require("./cli-client");
|
|
14
14
|
const session_history_1 = require("./session-history");
|
|
15
15
|
const desktop_launch_1 = require("../desktop-launch");
|
|
16
|
+
const desktop_launch_2 = require("./desktop-launch");
|
|
16
17
|
exports.workbuddyManifest = {
|
|
17
|
-
id: "workbuddy", version: "1.1.
|
|
18
|
+
id: "workbuddy", version: "1.1.14", displayName: "WorkBuddy", type: "agent",
|
|
18
19
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
19
20
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
20
21
|
};
|
|
@@ -48,11 +49,17 @@ class WorkbuddyPlugin {
|
|
|
48
49
|
queue = Promise.resolve();
|
|
49
50
|
pending = 0;
|
|
50
51
|
activeSessionId;
|
|
52
|
+
/** 重载后待恢复的会话身份。 */
|
|
53
|
+
recoverableSessionId;
|
|
54
|
+
recoverableConversationId;
|
|
51
55
|
activeDesktopConversationId;
|
|
52
56
|
desktopSessionValid = true;
|
|
53
57
|
knownSessions = new Set();
|
|
54
58
|
sessionDataDir;
|
|
55
59
|
sessionWrites = Promise.resolve();
|
|
60
|
+
dailyPointsTimer;
|
|
61
|
+
dailyPointsDataDir;
|
|
62
|
+
dailyPointsLogger;
|
|
56
63
|
failure;
|
|
57
64
|
expertSession = false;
|
|
58
65
|
activeExpert;
|
|
@@ -72,12 +79,16 @@ class WorkbuddyPlugin {
|
|
|
72
79
|
this.cliPath = (0, cli_client_1.findCli)(typeof config.cliPath === "string" ? config.cliPath : undefined);
|
|
73
80
|
this.failure = undefined;
|
|
74
81
|
this.activeSessionId = undefined;
|
|
82
|
+
this.recoverableSessionId = undefined;
|
|
83
|
+
this.recoverableConversationId = undefined;
|
|
75
84
|
this.activeDesktopConversationId = undefined;
|
|
76
85
|
this.activeExpert = undefined;
|
|
77
86
|
this.expertSession = false;
|
|
78
87
|
this.desktopSessionValid = true;
|
|
79
88
|
this.knownSessions.clear();
|
|
80
89
|
this.sessionDataDir = context.dataDir;
|
|
90
|
+
this.dailyPointsDataDir = context.dataDir;
|
|
91
|
+
this.dailyPointsLogger = context.logger;
|
|
81
92
|
let savedBinding;
|
|
82
93
|
if (this.sessionDataDir) {
|
|
83
94
|
const saved = await this.loadKnownSessions(this.sessionDataDir);
|
|
@@ -112,7 +123,7 @@ class WorkbuddyPlugin {
|
|
|
112
123
|
const port = (0, plugin_1.resolveRuntimePort)(config, context.runtime, "workbuddy", "WORKBUDDY_CDP_PORT", this.options.cdpPort ?? 9229);
|
|
113
124
|
this.client = this.options.client ?? new cdp_client_1.WorkbuddyCdpClient({ port });
|
|
114
125
|
try {
|
|
115
|
-
await this.connectOrStart(config, context.logger, port);
|
|
126
|
+
await this.connectOrStart(config, context.logger, port, context.dataDir);
|
|
116
127
|
}
|
|
117
128
|
catch (error) {
|
|
118
129
|
this.failure = error instanceof Error ? error.message : String(error);
|
|
@@ -127,8 +138,22 @@ class WorkbuddyPlugin {
|
|
|
127
138
|
this.activeDesktopConversationId = savedBinding.conversationId;
|
|
128
139
|
this.activeExpert = savedBinding.expert;
|
|
129
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
|
+
}
|
|
130
152
|
}
|
|
131
153
|
async deactivate(_reason) {
|
|
154
|
+
if (this.dailyPointsTimer)
|
|
155
|
+
clearTimeout(this.dailyPointsTimer);
|
|
156
|
+
this.dailyPointsTimer = undefined;
|
|
132
157
|
await this.queue;
|
|
133
158
|
await this.sessionWrites.catch(() => { });
|
|
134
159
|
await (0, desktop_launch_1.terminateOwnedProcess)(this.process).catch(() => undefined);
|
|
@@ -137,6 +162,59 @@ class WorkbuddyPlugin {
|
|
|
137
162
|
this.process = undefined;
|
|
138
163
|
this.reconnect = undefined;
|
|
139
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
|
+
}
|
|
140
218
|
async health() {
|
|
141
219
|
if (this.client?.connected())
|
|
142
220
|
return { status: "ready", details: { transport: "cdp" } };
|
|
@@ -257,8 +335,17 @@ class WorkbuddyPlugin {
|
|
|
257
335
|
}
|
|
258
336
|
return;
|
|
259
337
|
}
|
|
260
|
-
if (this.knownSessions.has(sessionId))
|
|
261
|
-
|
|
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
|
+
}
|
|
262
349
|
await this.createSession(sessionId);
|
|
263
350
|
}
|
|
264
351
|
async createSession(sessionId, name) {
|
|
@@ -273,6 +360,8 @@ class WorkbuddyPlugin {
|
|
|
273
360
|
this.expertSession = false;
|
|
274
361
|
this.activeExpert = undefined;
|
|
275
362
|
this.activeSessionId = sessionId;
|
|
363
|
+
this.recoverableSessionId = undefined;
|
|
364
|
+
this.recoverableConversationId = undefined;
|
|
276
365
|
}
|
|
277
366
|
async loadKnownSessions(directory) {
|
|
278
367
|
try {
|
|
@@ -314,10 +403,10 @@ class WorkbuddyPlugin {
|
|
|
314
403
|
this.queue = run.then(() => { this.pending--; }, () => { this.pending--; });
|
|
315
404
|
return run;
|
|
316
405
|
}
|
|
317
|
-
async connectOrStart(config, logger, port) {
|
|
406
|
+
async connectOrStart(config, logger, port, dataDir) {
|
|
318
407
|
const executable = findExecutable(typeof config.executablePath === "string" ? config.executablePath : this.options.executablePath);
|
|
319
|
-
|
|
320
|
-
const
|
|
408
|
+
const profile = (0, node_path_1.join)(dataDir ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".agent-bridge", "workbuddy"), "desktop");
|
|
409
|
+
const launch = (file, args) => (0, desktop_launch_2.launchWorkbuddyDesktop)(file, args, profile, port);
|
|
321
410
|
const timeoutMs = typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000;
|
|
322
411
|
this.reconnect = async () => {
|
|
323
412
|
try {
|
|
@@ -327,7 +416,9 @@ class WorkbuddyPlugin {
|
|
|
327
416
|
catch (first) {
|
|
328
417
|
if (config.autoStart === false || this.options.client || !executable)
|
|
329
418
|
throw first;
|
|
330
|
-
await (0, desktop_launch_1.
|
|
419
|
+
await (0, desktop_launch_1.terminateOwnedProcess)(this.process);
|
|
420
|
+
this.process = undefined;
|
|
421
|
+
await (0, desktop_launch_1.launchManagedAndWait)(executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; }, launch);
|
|
331
422
|
}
|
|
332
423
|
};
|
|
333
424
|
try {
|
|
@@ -340,7 +431,7 @@ class WorkbuddyPlugin {
|
|
|
340
431
|
if (!executable)
|
|
341
432
|
throw new Error("未找到 WorkBuddy.exe,请配置 executablePath");
|
|
342
433
|
logger.info(`WorkBuddy 已启动,等待 CDP ${port}`);
|
|
343
|
-
await (0, desktop_launch_1.launchManagedAndWait)(executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; });
|
|
434
|
+
await (0, desktop_launch_1.launchManagedAndWait)(executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; }, launch);
|
|
344
435
|
}
|
|
345
436
|
}
|
|
346
437
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_vm_1 = require("node:vm");
|
|
9
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
10
|
+
class Element {
|
|
11
|
+
hidden = false;
|
|
12
|
+
open = false;
|
|
13
|
+
value = "";
|
|
14
|
+
textContent = "";
|
|
15
|
+
children = [];
|
|
16
|
+
options = [];
|
|
17
|
+
selectedIndex = 0;
|
|
18
|
+
onclick;
|
|
19
|
+
onsubmit;
|
|
20
|
+
append(...children) { this.children.push(...children); }
|
|
21
|
+
replaceChildren(...children) { this.children = children; }
|
|
22
|
+
setAttribute() { }
|
|
23
|
+
showModal() { this.open = true; }
|
|
24
|
+
close() { this.open = false; }
|
|
25
|
+
}
|
|
26
|
+
(0, node_test_1.default)("abweb CLI 配置入口读取、保存配置且保留通道关联", async () => {
|
|
27
|
+
const nodes = new Map();
|
|
28
|
+
const node = (selector) => { if (!nodes.has(selector))
|
|
29
|
+
nodes.set(selector, new Element()); return nodes.get(selector); };
|
|
30
|
+
let saved;
|
|
31
|
+
(0, node_vm_1.runInNewContext)(await (0, promises_1.readFile)("src-ts/plugins/abweb/public/app.js", "utf8"), {
|
|
32
|
+
document: { querySelector: node, createElement: () => new Element(), hidden: false },
|
|
33
|
+
localStorage: { getItem: () => null, removeItem() { } }, window: { addEventListener() { } },
|
|
34
|
+
structuredClone, setInterval() { }, confirm: () => false,
|
|
35
|
+
fetch: async (path, options) => {
|
|
36
|
+
let data = {};
|
|
37
|
+
if (path === "/api/plugins")
|
|
38
|
+
data = { plugins: [{ id: "codex-cli", displayName: "Codex CLI", type: "agent", version: "1.0.0", status: "installed", path: "plugins/codex-cli", manifest: { executionMode: "cli" } }] };
|
|
39
|
+
if (path.endsWith("/config")) {
|
|
40
|
+
if (options.method === "PATCH")
|
|
41
|
+
saved = JSON.parse(options.body);
|
|
42
|
+
data = { cwd: "/原目录", aid: "existing.agentid.cn" };
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, status: 200, json: async () => data };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
48
|
+
const row = node("#plugins").children[0];
|
|
49
|
+
strict_1.default.match(row.children[1].textContent, /CLI/);
|
|
50
|
+
const configure = row.children.at(-1).children.find(child => child.textContent === "配置");
|
|
51
|
+
strict_1.default.ok(configure, "CLI 插件必须提供配置按钮");
|
|
52
|
+
await configure.onclick();
|
|
53
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
54
|
+
strict_1.default.equal(node("#cli-config-cwd").value, "/原目录");
|
|
55
|
+
node("#cli-config-cwd").value = "/新目录";
|
|
56
|
+
node("#cli-config-executable").value = "/路径 空格/codex";
|
|
57
|
+
node("#cli-config-timeout").value = "240000";
|
|
58
|
+
node("#cli-config-form").onsubmit({ preventDefault() { } });
|
|
59
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
60
|
+
strict_1.default.equal(saved.cwd, "/新目录");
|
|
61
|
+
strict_1.default.equal(saved.executablePath, "/路径 空格/codex");
|
|
62
|
+
strict_1.default.equal(saved.timeoutMs, 240000);
|
|
63
|
+
strict_1.default.equal(saved.aid, undefined, "通过 PATCH 合并配置,不能改写通道关联 AID");
|
|
64
|
+
strict_1.default.equal(node("#cli-config-dialog").open, false);
|
|
65
|
+
});
|