weacpx 0.1.7 → 0.2.1
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 +88 -9
- package/config.example.json +4 -1
- package/dist/bridge/bridge-main.js +235 -17
- package/dist/cli.js +2469 -520
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -45,8 +45,501 @@ var __export = (target, all) => {
|
|
|
45
45
|
});
|
|
46
46
|
};
|
|
47
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
48
|
+
var __promiseAll = (args) => Promise.all(args);
|
|
48
49
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
49
50
|
|
|
51
|
+
// src/daemon/daemon-status.ts
|
|
52
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
53
|
+
import { dirname } from "node:path";
|
|
54
|
+
|
|
55
|
+
class DaemonStatusStore {
|
|
56
|
+
path;
|
|
57
|
+
constructor(path) {
|
|
58
|
+
this.path = path;
|
|
59
|
+
}
|
|
60
|
+
async load() {
|
|
61
|
+
try {
|
|
62
|
+
const content = await readFile(this.path, "utf8");
|
|
63
|
+
if (content.trim() === "") {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return JSON.parse(content);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error.code === "ENOENT") {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async save(status) {
|
|
75
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
76
|
+
await writeFile(this.path, JSON.stringify(status, null, 2));
|
|
77
|
+
}
|
|
78
|
+
async clear() {
|
|
79
|
+
await rm(this.path, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
var init_daemon_status = () => {};
|
|
83
|
+
|
|
84
|
+
// src/daemon/daemon-controller.ts
|
|
85
|
+
import { mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
86
|
+
import { dirname as dirname2 } from "node:path";
|
|
87
|
+
|
|
88
|
+
class DaemonController {
|
|
89
|
+
paths;
|
|
90
|
+
deps;
|
|
91
|
+
statusStore;
|
|
92
|
+
startupPollIntervalMs;
|
|
93
|
+
startupTimeoutMs;
|
|
94
|
+
onStartupPoll;
|
|
95
|
+
shutdownPollIntervalMs;
|
|
96
|
+
shutdownTimeoutMs;
|
|
97
|
+
onShutdownPoll;
|
|
98
|
+
constructor(paths, deps) {
|
|
99
|
+
this.paths = paths;
|
|
100
|
+
this.deps = deps;
|
|
101
|
+
this.statusStore = new DaemonStatusStore(paths.statusFile);
|
|
102
|
+
this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
|
|
103
|
+
this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
|
|
104
|
+
this.shutdownPollIntervalMs = deps.shutdownPollIntervalMs ?? 50;
|
|
105
|
+
this.shutdownTimeoutMs = deps.shutdownTimeoutMs ?? 5000;
|
|
106
|
+
this.onStartupPoll = deps.onStartupPoll ?? (async () => {
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, this.startupPollIntervalMs));
|
|
108
|
+
});
|
|
109
|
+
this.onShutdownPoll = deps.onShutdownPoll ?? (async () => {
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, this.shutdownPollIntervalMs));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async getStatus() {
|
|
114
|
+
const pid = await this.loadPid();
|
|
115
|
+
const status = await this.statusStore.load();
|
|
116
|
+
if (!pid) {
|
|
117
|
+
return { state: "stopped" };
|
|
118
|
+
}
|
|
119
|
+
if (!this.deps.isProcessRunning(pid)) {
|
|
120
|
+
await this.clearRuntimeFiles();
|
|
121
|
+
return { state: "stopped", stale: true };
|
|
122
|
+
}
|
|
123
|
+
if (!status) {
|
|
124
|
+
return { state: "indeterminate", pid, reason: "missing-status" };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
state: "running",
|
|
128
|
+
pid,
|
|
129
|
+
status
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async start() {
|
|
133
|
+
const current = await this.getStatus();
|
|
134
|
+
if (current.state === "running") {
|
|
135
|
+
return { state: "already-running", pid: current.pid };
|
|
136
|
+
}
|
|
137
|
+
if (current.state === "indeterminate") {
|
|
138
|
+
throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
|
|
139
|
+
}
|
|
140
|
+
await this.statusStore.clear();
|
|
141
|
+
const pid = await this.deps.spawnDetached();
|
|
142
|
+
await this.writePid(pid);
|
|
143
|
+
await this.waitForStartupMetadata(pid);
|
|
144
|
+
return { state: "started", pid };
|
|
145
|
+
}
|
|
146
|
+
async stop() {
|
|
147
|
+
const pid = await this.loadPid();
|
|
148
|
+
if (!pid) {
|
|
149
|
+
return { state: "stopped", detail: "not-running" };
|
|
150
|
+
}
|
|
151
|
+
if (this.deps.isProcessRunning(pid)) {
|
|
152
|
+
await this.deps.terminateProcess(pid);
|
|
153
|
+
await this.waitForShutdown(pid);
|
|
154
|
+
}
|
|
155
|
+
await this.clearRuntimeFiles();
|
|
156
|
+
return { state: "stopped", detail: "stopped" };
|
|
157
|
+
}
|
|
158
|
+
async loadPid() {
|
|
159
|
+
try {
|
|
160
|
+
const content = await readFile2(this.paths.pidFile, "utf8");
|
|
161
|
+
const pid = Number(content.trim());
|
|
162
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error.code === "ENOENT") {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async writePid(pid) {
|
|
171
|
+
await mkdir2(dirname2(this.paths.pidFile), { recursive: true });
|
|
172
|
+
await writeFile2(this.paths.pidFile, `${pid}
|
|
173
|
+
`);
|
|
174
|
+
}
|
|
175
|
+
async clearRuntimeFiles() {
|
|
176
|
+
await rm2(this.paths.pidFile, { force: true });
|
|
177
|
+
await this.statusStore.clear();
|
|
178
|
+
}
|
|
179
|
+
async waitForStartupMetadata(pid) {
|
|
180
|
+
const deadline = Date.now() + this.startupTimeoutMs;
|
|
181
|
+
while (Date.now() < deadline) {
|
|
182
|
+
const status = await this.statusStore.load();
|
|
183
|
+
if (status?.pid === pid) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!this.deps.isProcessRunning(pid)) {
|
|
187
|
+
await this.clearRuntimeFiles();
|
|
188
|
+
throw new Error(`weacpx daemon exited before reporting ready state (pid ${pid})`);
|
|
189
|
+
}
|
|
190
|
+
await this.onStartupPoll();
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`weacpx daemon did not report ready state within ${this.startupTimeoutMs}ms (pid ${pid})`);
|
|
193
|
+
}
|
|
194
|
+
async waitForShutdown(pid) {
|
|
195
|
+
const deadline = Date.now() + this.shutdownTimeoutMs;
|
|
196
|
+
while (Date.now() < deadline) {
|
|
197
|
+
if (!this.deps.isProcessRunning(pid)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await this.onShutdownPoll();
|
|
201
|
+
}
|
|
202
|
+
if (!this.deps.isProcessRunning(pid)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`weacpx daemon did not exit within ${this.shutdownTimeoutMs}ms (pid ${pid})`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
var init_daemon_controller = __esm(() => {
|
|
209
|
+
init_daemon_status();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/daemon/create-daemon-controller.ts
|
|
213
|
+
import { mkdir as mkdir3, open } from "node:fs/promises";
|
|
214
|
+
import { spawn } from "node:child_process";
|
|
215
|
+
function createDaemonController(paths, options) {
|
|
216
|
+
return new DaemonController(paths, {
|
|
217
|
+
isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning,
|
|
218
|
+
spawnDetached: async () => {
|
|
219
|
+
await mkdir3(paths.runtimeDir, { recursive: true });
|
|
220
|
+
const stdoutHandle = await open(paths.stdoutLog, "a");
|
|
221
|
+
const stderrHandle = await open(paths.stderrLog, "a");
|
|
222
|
+
try {
|
|
223
|
+
return await (options.spawnProcess ?? defaultSpawnProcess)(buildSpawnRequest(paths, options, stdoutHandle.fd, stderrHandle.fd));
|
|
224
|
+
} finally {
|
|
225
|
+
await stdoutHandle.close();
|
|
226
|
+
await stderrHandle.close();
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
terminateProcess: options.terminateProcess ?? defaultTerminateProcess
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function defaultIsProcessRunning(pid) {
|
|
233
|
+
try {
|
|
234
|
+
process.kill(pid, 0);
|
|
235
|
+
return true;
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function buildSpawnRequest(paths, options, stdoutFd, stderrFd) {
|
|
241
|
+
const platform = options.platform ?? process.platform;
|
|
242
|
+
if (platform === "win32") {
|
|
243
|
+
return {
|
|
244
|
+
mode: "windows-hidden",
|
|
245
|
+
command: "powershell.exe",
|
|
246
|
+
args: [
|
|
247
|
+
"-NoProfile",
|
|
248
|
+
"-NonInteractive",
|
|
249
|
+
"-EncodedCommand",
|
|
250
|
+
buildWindowsLauncherScript()
|
|
251
|
+
],
|
|
252
|
+
options: {
|
|
253
|
+
cwd: options.cwd,
|
|
254
|
+
env: {
|
|
255
|
+
...options.env,
|
|
256
|
+
WEACPX_DAEMON_COMMAND: options.processExecPath,
|
|
257
|
+
WEACPX_DAEMON_ARG0: options.cliEntryPath,
|
|
258
|
+
WEACPX_DAEMON_ARG1: "run",
|
|
259
|
+
WEACPX_DAEMON_CWD: options.cwd,
|
|
260
|
+
WEACPX_DAEMON_STDOUT: paths.stdoutLog,
|
|
261
|
+
WEACPX_DAEMON_STDERR: paths.stderrLog
|
|
262
|
+
},
|
|
263
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
264
|
+
windowsHide: true
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
mode: "direct",
|
|
270
|
+
command: options.processExecPath,
|
|
271
|
+
args: [options.cliEntryPath, "run"],
|
|
272
|
+
options: {
|
|
273
|
+
cwd: options.cwd,
|
|
274
|
+
detached: true,
|
|
275
|
+
env: options.env,
|
|
276
|
+
stdio: ["ignore", stdoutFd, stderrFd]
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function buildWindowsLauncherScript() {
|
|
281
|
+
const script = [
|
|
282
|
+
"$process = Start-Process -FilePath $env:WEACPX_DAEMON_COMMAND `",
|
|
283
|
+
" -ArgumentList @($env:WEACPX_DAEMON_ARG0, $env:WEACPX_DAEMON_ARG1) `",
|
|
284
|
+
" -WorkingDirectory $env:WEACPX_DAEMON_CWD `",
|
|
285
|
+
" -RedirectStandardOutput $env:WEACPX_DAEMON_STDOUT `",
|
|
286
|
+
" -RedirectStandardError $env:WEACPX_DAEMON_STDERR `",
|
|
287
|
+
" -WindowStyle Hidden `",
|
|
288
|
+
" -PassThru",
|
|
289
|
+
"[Console]::Out.WriteLine($process.Id)"
|
|
290
|
+
].join(`
|
|
291
|
+
`);
|
|
292
|
+
return Buffer.from(script, "utf16le").toString("base64");
|
|
293
|
+
}
|
|
294
|
+
async function defaultSpawnProcess(request) {
|
|
295
|
+
if (request.mode === "windows-hidden") {
|
|
296
|
+
return await spawnWindowsHiddenProcess(request);
|
|
297
|
+
}
|
|
298
|
+
const child = spawn(request.command, request.args, request.options);
|
|
299
|
+
child.unref();
|
|
300
|
+
return child.pid ?? 0;
|
|
301
|
+
}
|
|
302
|
+
async function spawnWindowsHiddenProcess(request) {
|
|
303
|
+
return await new Promise((resolve, reject) => {
|
|
304
|
+
const child = spawn(request.command, request.args, request.options);
|
|
305
|
+
let stdout = "";
|
|
306
|
+
let settled = false;
|
|
307
|
+
child.stdout?.setEncoding("utf8");
|
|
308
|
+
child.stdout?.on("data", (chunk) => {
|
|
309
|
+
stdout += chunk;
|
|
310
|
+
if (settled) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const pid = Number.parseInt(stdout.trim(), 10);
|
|
314
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
settled = true;
|
|
318
|
+
child.stdout?.destroy();
|
|
319
|
+
child.unref();
|
|
320
|
+
resolve(pid);
|
|
321
|
+
});
|
|
322
|
+
child.on("error", (error) => {
|
|
323
|
+
if (settled) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
settled = true;
|
|
327
|
+
reject(error);
|
|
328
|
+
});
|
|
329
|
+
child.on("close", (code) => {
|
|
330
|
+
if (settled) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (code !== 0) {
|
|
334
|
+
settled = true;
|
|
335
|
+
reject(new Error(`Failed to launch hidden Windows daemon process (exit ${code ?? 1})`));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const pid = Number.parseInt(stdout.trim(), 10);
|
|
339
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
340
|
+
settled = true;
|
|
341
|
+
reject(new Error("Failed to read daemon pid from hidden Windows launcher"));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
settled = true;
|
|
345
|
+
resolve(pid);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async function defaultTerminateProcess(pid) {
|
|
350
|
+
await terminateProcessTree(pid);
|
|
351
|
+
}
|
|
352
|
+
async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
|
|
353
|
+
process.kill(targetPid, signal);
|
|
354
|
+
}, isProcessRunning = defaultIsProcessRunning) {
|
|
355
|
+
if (platform === "win32") {
|
|
356
|
+
try {
|
|
357
|
+
await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
|
|
358
|
+
} catch {}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const targetPid = pid > 0 ? -pid : pid;
|
|
362
|
+
try {
|
|
363
|
+
killProcess(targetPid, "SIGTERM");
|
|
364
|
+
} catch {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const deadline = Date.now() + 5000;
|
|
368
|
+
while (Date.now() < deadline) {
|
|
369
|
+
if (!isProcessRunning(targetPid)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
killProcess(targetPid, "SIGKILL");
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
378
|
+
async function defaultRunProcessCommand(command, args) {
|
|
379
|
+
return await new Promise((resolve, reject) => {
|
|
380
|
+
const child = spawn(command, args, { stdio: "ignore" });
|
|
381
|
+
child.on("error", reject);
|
|
382
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
var init_create_daemon_controller = __esm(() => {
|
|
386
|
+
init_daemon_controller();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// src/daemon/daemon-files.ts
|
|
390
|
+
import { join } from "node:path";
|
|
391
|
+
function resolveDaemonPaths(options) {
|
|
392
|
+
const runtimeDir = options.runtimeDir ?? join(options.home, ".weacpx", "runtime");
|
|
393
|
+
return {
|
|
394
|
+
runtimeDir,
|
|
395
|
+
pidFile: join(runtimeDir, "daemon.pid"),
|
|
396
|
+
statusFile: join(runtimeDir, "status.json"),
|
|
397
|
+
stdoutLog: join(runtimeDir, "stdout.log"),
|
|
398
|
+
stderrLog: join(runtimeDir, "stderr.log"),
|
|
399
|
+
appLog: join(runtimeDir, "app.log")
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
var init_daemon_files = () => {};
|
|
403
|
+
|
|
404
|
+
// src/version.ts
|
|
405
|
+
import fs from "node:fs";
|
|
406
|
+
import path from "node:path";
|
|
407
|
+
import { fileURLToPath } from "node:url";
|
|
408
|
+
function readVersion() {
|
|
409
|
+
try {
|
|
410
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
411
|
+
const pkgPath = path.resolve(dir, "..", "..", "package.json");
|
|
412
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
413
|
+
return pkg.version ?? "unknown";
|
|
414
|
+
} catch {
|
|
415
|
+
return "unknown";
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
var init_version = () => {};
|
|
419
|
+
|
|
420
|
+
// src/weixin/monitor/consumer-lock.ts
|
|
421
|
+
import { mkdir as mkdir5, open as open2, readFile as readFile3, rm as rm4 } from "node:fs/promises";
|
|
422
|
+
import { dirname as dirname4, join as join2 } from "node:path";
|
|
423
|
+
import { homedir } from "node:os";
|
|
424
|
+
function createWeixinConsumerLock(options = {}) {
|
|
425
|
+
const lockFilePath = options.lockFilePath ?? join2(homedir(), ".weacpx", "runtime", "weixin-consumer.lock.json");
|
|
426
|
+
const isProcessRunning = options.isProcessRunning ?? defaultIsProcessRunning2;
|
|
427
|
+
const onDiagnostic = options.onDiagnostic;
|
|
428
|
+
return {
|
|
429
|
+
async acquire(meta) {
|
|
430
|
+
await mkdir5(dirname4(lockFilePath), { recursive: true });
|
|
431
|
+
while (true) {
|
|
432
|
+
try {
|
|
433
|
+
const handle = await open2(lockFilePath, "wx");
|
|
434
|
+
try {
|
|
435
|
+
await handle.writeFile(`${JSON.stringify(meta, null, 2)}
|
|
436
|
+
`, "utf8");
|
|
437
|
+
} finally {
|
|
438
|
+
await handle.close();
|
|
439
|
+
}
|
|
440
|
+
await onDiagnostic?.("lock_acquired", {
|
|
441
|
+
lockFilePath,
|
|
442
|
+
pid: meta.pid,
|
|
443
|
+
mode: meta.mode,
|
|
444
|
+
configPath: meta.configPath,
|
|
445
|
+
statePath: meta.statePath,
|
|
446
|
+
hostname: meta.hostname
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
const code = error.code;
|
|
451
|
+
if (code !== "EEXIST") {
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
await onDiagnostic?.("lock_exists", {
|
|
455
|
+
lockFilePath,
|
|
456
|
+
pid: meta.pid,
|
|
457
|
+
mode: meta.mode
|
|
458
|
+
});
|
|
459
|
+
const existing = await loadLockMetadata(lockFilePath);
|
|
460
|
+
if (!existing) {
|
|
461
|
+
await rm4(lockFilePath, { force: true });
|
|
462
|
+
await onDiagnostic?.("lock_invalid_removed", {
|
|
463
|
+
lockFilePath,
|
|
464
|
+
reason: "invalid_or_unreadable_metadata"
|
|
465
|
+
});
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (!isProcessRunning(existing.pid)) {
|
|
469
|
+
await rm4(lockFilePath, { force: true });
|
|
470
|
+
await onDiagnostic?.("lock_stale_removed", {
|
|
471
|
+
lockFilePath,
|
|
472
|
+
stalePid: existing.pid,
|
|
473
|
+
staleMode: existing.mode,
|
|
474
|
+
staleConfigPath: existing.configPath,
|
|
475
|
+
staleStatePath: existing.statePath,
|
|
476
|
+
reason: "owner_process_not_running"
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
await onDiagnostic?.("lock_active_conflict", {
|
|
481
|
+
lockFilePath,
|
|
482
|
+
activePid: existing.pid,
|
|
483
|
+
activeMode: existing.mode,
|
|
484
|
+
activeConfigPath: existing.configPath,
|
|
485
|
+
activeStatePath: existing.statePath,
|
|
486
|
+
requestedPid: meta.pid,
|
|
487
|
+
requestedMode: meta.mode
|
|
488
|
+
});
|
|
489
|
+
throw new ActiveWeixinConsumerLockError(lockFilePath, existing);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
async release() {
|
|
494
|
+
await rm4(lockFilePath, { force: true });
|
|
495
|
+
await onDiagnostic?.("lock_released", {
|
|
496
|
+
lockFilePath
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async function loadLockMetadata(path2) {
|
|
502
|
+
try {
|
|
503
|
+
const raw = await readFile3(path2, "utf8");
|
|
504
|
+
const parsed = JSON.parse(raw);
|
|
505
|
+
if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return parsed;
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function defaultIsProcessRunning2(pid) {
|
|
514
|
+
try {
|
|
515
|
+
process.kill(pid, 0);
|
|
516
|
+
return true;
|
|
517
|
+
} catch {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
var ActiveWeixinConsumerLockError;
|
|
522
|
+
var init_consumer_lock = __esm(() => {
|
|
523
|
+
ActiveWeixinConsumerLockError = class ActiveWeixinConsumerLockError extends Error {
|
|
524
|
+
existing;
|
|
525
|
+
lockFilePath;
|
|
526
|
+
constructor(lockFilePath, existing) {
|
|
527
|
+
super([
|
|
528
|
+
"weacpx Weixin consumer is already running.",
|
|
529
|
+
`pid: ${existing.pid}`,
|
|
530
|
+
`mode: ${existing.mode}`,
|
|
531
|
+
`config: ${existing.configPath}`,
|
|
532
|
+
`state: ${existing.statePath}`,
|
|
533
|
+
"Try stopping the existing instance or close the foreground `weacpx run` process before starting a new one."
|
|
534
|
+
].join(`
|
|
535
|
+
`));
|
|
536
|
+
this.name = "ActiveWeixinConsumerLockError";
|
|
537
|
+
this.lockFilePath = lockFilePath;
|
|
538
|
+
this.existing = existing;
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
});
|
|
542
|
+
|
|
50
543
|
// node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
|
|
51
544
|
var require_QRMode = __commonJS((exports, module) => {
|
|
52
545
|
module.exports = {
|
|
@@ -1066,15 +1559,15 @@ var require_main = __commonJS((exports, module) => {
|
|
|
1066
1559
|
|
|
1067
1560
|
// src/weixin/storage/state-dir.ts
|
|
1068
1561
|
import os from "node:os";
|
|
1069
|
-
import
|
|
1562
|
+
import path2 from "node:path";
|
|
1070
1563
|
function resolveStateDir() {
|
|
1071
|
-
return process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
|
1564
|
+
return process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path2.join(os.homedir(), ".openclaw");
|
|
1072
1565
|
}
|
|
1073
1566
|
var init_state_dir = () => {};
|
|
1074
1567
|
|
|
1075
1568
|
// src/weixin/auth/accounts.ts
|
|
1076
|
-
import
|
|
1077
|
-
import
|
|
1569
|
+
import fs2 from "node:fs";
|
|
1570
|
+
import path3 from "node:path";
|
|
1078
1571
|
function normalizeAccountId(raw) {
|
|
1079
1572
|
return raw.trim().toLowerCase().replace(/[@.]/g, "-");
|
|
1080
1573
|
}
|
|
@@ -1088,17 +1581,17 @@ function deriveRawAccountId(normalizedId) {
|
|
|
1088
1581
|
return;
|
|
1089
1582
|
}
|
|
1090
1583
|
function resolveWeixinStateDir() {
|
|
1091
|
-
return
|
|
1584
|
+
return path3.join(resolveStateDir(), "openclaw-weixin");
|
|
1092
1585
|
}
|
|
1093
1586
|
function resolveAccountIndexPath() {
|
|
1094
|
-
return
|
|
1587
|
+
return path3.join(resolveWeixinStateDir(), "accounts.json");
|
|
1095
1588
|
}
|
|
1096
1589
|
function listIndexedWeixinAccountIds() {
|
|
1097
1590
|
const filePath = resolveAccountIndexPath();
|
|
1098
1591
|
try {
|
|
1099
|
-
if (!
|
|
1592
|
+
if (!fs2.existsSync(filePath))
|
|
1100
1593
|
return [];
|
|
1101
|
-
const raw =
|
|
1594
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
1102
1595
|
const parsed = JSON.parse(raw);
|
|
1103
1596
|
if (!Array.isArray(parsed))
|
|
1104
1597
|
return [];
|
|
@@ -1109,21 +1602,21 @@ function listIndexedWeixinAccountIds() {
|
|
|
1109
1602
|
}
|
|
1110
1603
|
function registerWeixinAccountId(accountId) {
|
|
1111
1604
|
const dir = resolveWeixinStateDir();
|
|
1112
|
-
|
|
1113
|
-
|
|
1605
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
1606
|
+
fs2.writeFileSync(resolveAccountIndexPath(), JSON.stringify([accountId], null, 2), "utf-8");
|
|
1114
1607
|
}
|
|
1115
1608
|
function resolveAccountsDir() {
|
|
1116
|
-
return
|
|
1609
|
+
return path3.join(resolveWeixinStateDir(), "accounts");
|
|
1117
1610
|
}
|
|
1118
1611
|
function resolveAccountPath(accountId) {
|
|
1119
|
-
return
|
|
1612
|
+
return path3.join(resolveAccountsDir(), `${accountId}.json`);
|
|
1120
1613
|
}
|
|
1121
1614
|
function loadLegacyToken() {
|
|
1122
|
-
const legacyPath =
|
|
1615
|
+
const legacyPath = path3.join(resolveStateDir(), "credentials", "openclaw-weixin", "credentials.json");
|
|
1123
1616
|
try {
|
|
1124
|
-
if (!
|
|
1617
|
+
if (!fs2.existsSync(legacyPath))
|
|
1125
1618
|
return;
|
|
1126
|
-
const raw =
|
|
1619
|
+
const raw = fs2.readFileSync(legacyPath, "utf-8");
|
|
1127
1620
|
const parsed = JSON.parse(raw);
|
|
1128
1621
|
return typeof parsed.token === "string" ? parsed.token : undefined;
|
|
1129
1622
|
} catch {
|
|
@@ -1132,8 +1625,8 @@ function loadLegacyToken() {
|
|
|
1132
1625
|
}
|
|
1133
1626
|
function readAccountFile(filePath) {
|
|
1134
1627
|
try {
|
|
1135
|
-
if (
|
|
1136
|
-
return JSON.parse(
|
|
1628
|
+
if (fs2.existsSync(filePath)) {
|
|
1629
|
+
return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
1137
1630
|
}
|
|
1138
1631
|
} catch {}
|
|
1139
1632
|
return null;
|
|
@@ -1155,7 +1648,7 @@ function loadWeixinAccount(accountId) {
|
|
|
1155
1648
|
}
|
|
1156
1649
|
function saveWeixinAccount(accountId, update) {
|
|
1157
1650
|
const dir = resolveAccountsDir();
|
|
1158
|
-
|
|
1651
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
1159
1652
|
const existing = loadWeixinAccount(accountId) ?? {};
|
|
1160
1653
|
const token = update.token?.trim() || existing.token;
|
|
1161
1654
|
const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
|
|
@@ -1166,14 +1659,14 @@ function saveWeixinAccount(accountId, update) {
|
|
|
1166
1659
|
...userId ? { userId } : {}
|
|
1167
1660
|
};
|
|
1168
1661
|
const filePath = resolveAccountPath(accountId);
|
|
1169
|
-
|
|
1662
|
+
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
1170
1663
|
try {
|
|
1171
|
-
|
|
1664
|
+
fs2.chmodSync(filePath, 384);
|
|
1172
1665
|
} catch {}
|
|
1173
1666
|
}
|
|
1174
1667
|
function clearWeixinAccount(accountId) {
|
|
1175
1668
|
try {
|
|
1176
|
-
|
|
1669
|
+
fs2.unlinkSync(resolveAccountPath(accountId));
|
|
1177
1670
|
} catch {}
|
|
1178
1671
|
}
|
|
1179
1672
|
function clearAllWeixinAccounts() {
|
|
@@ -1182,21 +1675,21 @@ function clearAllWeixinAccounts() {
|
|
|
1182
1675
|
clearWeixinAccount(id);
|
|
1183
1676
|
}
|
|
1184
1677
|
try {
|
|
1185
|
-
|
|
1678
|
+
fs2.writeFileSync(resolveAccountIndexPath(), "[]", "utf-8");
|
|
1186
1679
|
} catch {}
|
|
1187
1680
|
}
|
|
1188
1681
|
function resolveConfigPath() {
|
|
1189
1682
|
const envPath = process.env.OPENCLAW_CONFIG?.trim();
|
|
1190
1683
|
if (envPath)
|
|
1191
1684
|
return envPath;
|
|
1192
|
-
return
|
|
1685
|
+
return path3.join(resolveStateDir(), "openclaw.json");
|
|
1193
1686
|
}
|
|
1194
1687
|
function loadConfigRouteTag(accountId) {
|
|
1195
1688
|
try {
|
|
1196
1689
|
const configPath = resolveConfigPath();
|
|
1197
|
-
if (!
|
|
1690
|
+
if (!fs2.existsSync(configPath))
|
|
1198
1691
|
return;
|
|
1199
|
-
const raw =
|
|
1692
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
1200
1693
|
const cfg = JSON.parse(raw);
|
|
1201
1694
|
const channels = cfg.channels;
|
|
1202
1695
|
const section = channels?.["openclaw-weixin"];
|
|
@@ -1244,9 +1737,9 @@ var init_accounts = __esm(() => {
|
|
|
1244
1737
|
});
|
|
1245
1738
|
|
|
1246
1739
|
// src/weixin/util/logger.ts
|
|
1247
|
-
import
|
|
1740
|
+
import fs3 from "node:fs";
|
|
1248
1741
|
import os2 from "node:os";
|
|
1249
|
-
import
|
|
1742
|
+
import path4 from "node:path";
|
|
1250
1743
|
function resolveMinLevel() {
|
|
1251
1744
|
const env = process.env.OPENCLAW_LOG_LEVEL?.toUpperCase();
|
|
1252
1745
|
if (env && env in LEVEL_IDS)
|
|
@@ -1265,7 +1758,7 @@ function localDateKey(now) {
|
|
|
1265
1758
|
}
|
|
1266
1759
|
function resolveMainLogPath() {
|
|
1267
1760
|
const dateKey = localDateKey(new Date);
|
|
1268
|
-
return
|
|
1761
|
+
return path4.join(MAIN_LOG_DIR, `openclaw-${dateKey}.log`);
|
|
1269
1762
|
}
|
|
1270
1763
|
function buildLoggerName(accountId) {
|
|
1271
1764
|
return accountId ? `${SUBSYSTEM}/${accountId}` : SUBSYSTEM;
|
|
@@ -1294,10 +1787,10 @@ function writeLog(level, message, accountId) {
|
|
|
1294
1787
|
});
|
|
1295
1788
|
try {
|
|
1296
1789
|
if (!logDirEnsured) {
|
|
1297
|
-
|
|
1790
|
+
fs3.mkdirSync(MAIN_LOG_DIR, { recursive: true });
|
|
1298
1791
|
logDirEnsured = true;
|
|
1299
1792
|
}
|
|
1300
|
-
|
|
1793
|
+
fs3.appendFileSync(resolveMainLogPath(), `${entry}
|
|
1301
1794
|
`, "utf-8");
|
|
1302
1795
|
} catch {}
|
|
1303
1796
|
}
|
|
@@ -1326,7 +1819,7 @@ function createLogger(accountId) {
|
|
|
1326
1819
|
}
|
|
1327
1820
|
var MAIN_LOG_DIR, SUBSYSTEM = "gateway/channels/openclaw-weixin", RUNTIME = "node", RUNTIME_VERSION, HOSTNAME, PARENT_NAMES, LEVEL_IDS, DEFAULT_LOG_LEVEL = "INFO", minLevelId, logDirEnsured = false, logger;
|
|
1328
1821
|
var init_logger = __esm(() => {
|
|
1329
|
-
MAIN_LOG_DIR =
|
|
1822
|
+
MAIN_LOG_DIR = path4.join("/tmp", "openclaw");
|
|
1330
1823
|
RUNTIME_VERSION = process.versions.node;
|
|
1331
1824
|
HOSTNAME = os2.hostname() || "unknown";
|
|
1332
1825
|
PARENT_NAMES = ["openclaw"];
|
|
@@ -1377,19 +1870,6 @@ var DEFAULT_BODY_MAX_LEN = 200, DEFAULT_TOKEN_PREFIX_LEN = 6;
|
|
|
1377
1870
|
|
|
1378
1871
|
// src/weixin/api/api.ts
|
|
1379
1872
|
import crypto from "node:crypto";
|
|
1380
|
-
import fs3 from "node:fs";
|
|
1381
|
-
import path4 from "node:path";
|
|
1382
|
-
import { fileURLToPath } from "node:url";
|
|
1383
|
-
function readChannelVersion() {
|
|
1384
|
-
try {
|
|
1385
|
-
const dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1386
|
-
const pkgPath = path4.resolve(dir, "..", "..", "package.json");
|
|
1387
|
-
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
1388
|
-
return pkg.version ?? "unknown";
|
|
1389
|
-
} catch {
|
|
1390
|
-
return "unknown";
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
1873
|
function buildBaseInfo() {
|
|
1394
1874
|
return { channel_version: CHANNEL_VERSION };
|
|
1395
1875
|
}
|
|
@@ -1565,9 +2045,10 @@ async function sendTyping(params) {
|
|
|
1565
2045
|
}
|
|
1566
2046
|
var CHANNEL_VERSION, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
|
|
1567
2047
|
var init_api = __esm(() => {
|
|
2048
|
+
init_version();
|
|
1568
2049
|
init_accounts();
|
|
1569
2050
|
init_logger();
|
|
1570
|
-
CHANNEL_VERSION =
|
|
2051
|
+
CHANNEL_VERSION = readVersion();
|
|
1571
2052
|
});
|
|
1572
2053
|
|
|
1573
2054
|
// src/weixin/auth/login-qr.ts
|
|
@@ -2428,6 +2909,25 @@ var init_media_download = __esm(() => {
|
|
|
2428
2909
|
WEIXIN_MEDIA_MAX_BYTES = 100 * 1024 * 1024;
|
|
2429
2910
|
});
|
|
2430
2911
|
|
|
2912
|
+
// src/weixin/messaging/execute-chat-turn.ts
|
|
2913
|
+
async function executeChatTurn(params) {
|
|
2914
|
+
let usedReply = false;
|
|
2915
|
+
const response = await params.agent.chat({
|
|
2916
|
+
...params.request,
|
|
2917
|
+
reply: async (text) => {
|
|
2918
|
+
const delivered = await params.onReplySegment?.(text);
|
|
2919
|
+
if (delivered !== false) {
|
|
2920
|
+
usedReply = true;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
return {
|
|
2925
|
+
text: usedReply ? undefined : response.text,
|
|
2926
|
+
media: response.media,
|
|
2927
|
+
usedReply
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2431
2931
|
// src/weixin/messaging/inbound.ts
|
|
2432
2932
|
function contextTokenKey(accountId, userId) {
|
|
2433
2933
|
return `${accountId}:${userId}`;
|
|
@@ -2495,7 +2995,7 @@ function markdownToPlainText(text) {
|
|
|
2495
2995
|
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
2496
2996
|
result = result.replace(/^\|[\s:|-]+\|$/gm, "");
|
|
2497
2997
|
result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split("|").map((cell) => cell.trim()).join(" "));
|
|
2498
|
-
result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(
|
|
2998
|
+
result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
|
|
2499
2999
|
return result;
|
|
2500
3000
|
}
|
|
2501
3001
|
function buildTextMessageReq(params) {
|
|
@@ -2826,7 +3326,7 @@ var init_slash_commands = __esm(() => {
|
|
|
2826
3326
|
init_send();
|
|
2827
3327
|
});
|
|
2828
3328
|
|
|
2829
|
-
// src/weixin/messaging/
|
|
3329
|
+
// src/weixin/messaging/handle-weixin-message-turn.ts
|
|
2830
3330
|
import crypto4 from "node:crypto";
|
|
2831
3331
|
import fs6 from "node:fs/promises";
|
|
2832
3332
|
import { tmpdir } from "node:os";
|
|
@@ -2869,7 +3369,7 @@ function findMediaItem(itemList) {
|
|
|
2869
3369
|
const refItem = itemList.find((item) => item.type === MessageItemType.TEXT && item.ref_msg?.message_item && isMediaItem(item.ref_msg.message_item));
|
|
2870
3370
|
return refItem?.ref_msg?.message_item ?? undefined;
|
|
2871
3371
|
}
|
|
2872
|
-
async function
|
|
3372
|
+
async function handleWeixinMessageTurn(full, deps) {
|
|
2873
3373
|
const receivedAt = Date.now();
|
|
2874
3374
|
const textBody = extractTextBody(full.item_list);
|
|
2875
3375
|
if (textBody.startsWith("/")) {
|
|
@@ -2924,22 +3424,27 @@ async function processOneMessage(full, deps) {
|
|
|
2924
3424
|
}
|
|
2925
3425
|
}
|
|
2926
3426
|
const to = full.from_user_id ?? "";
|
|
2927
|
-
const
|
|
3427
|
+
const sendReplySegment = async (text) => {
|
|
3428
|
+
const plainText = markdownToPlainText(text).trim();
|
|
3429
|
+
if (plainText.length === 0) {
|
|
3430
|
+
return false;
|
|
3431
|
+
}
|
|
2928
3432
|
try {
|
|
2929
3433
|
await sendMessageWeixin({
|
|
2930
3434
|
to,
|
|
2931
|
-
text:
|
|
3435
|
+
text: plainText,
|
|
2932
3436
|
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
|
|
2933
3437
|
});
|
|
3438
|
+
return true;
|
|
2934
3439
|
} catch (err) {
|
|
2935
3440
|
deps.errLog(`intermediate reply failed: ${String(err)}`);
|
|
3441
|
+
return false;
|
|
2936
3442
|
}
|
|
2937
3443
|
};
|
|
2938
3444
|
const request = {
|
|
2939
3445
|
conversationId: full.from_user_id ?? "",
|
|
2940
3446
|
text: bodyFromItemList(full.item_list),
|
|
2941
|
-
media
|
|
2942
|
-
reply
|
|
3447
|
+
media
|
|
2943
3448
|
};
|
|
2944
3449
|
let typingTimer;
|
|
2945
3450
|
const startTyping = () => {
|
|
@@ -2960,10 +3465,14 @@ async function processOneMessage(full, deps) {
|
|
|
2960
3465
|
typingTimer = setInterval(startTyping, 1e4);
|
|
2961
3466
|
}
|
|
2962
3467
|
try {
|
|
2963
|
-
const
|
|
2964
|
-
|
|
3468
|
+
const turn = await executeChatTurn({
|
|
3469
|
+
agent: deps.agent,
|
|
3470
|
+
request,
|
|
3471
|
+
onReplySegment: sendReplySegment
|
|
3472
|
+
});
|
|
3473
|
+
if (turn.media) {
|
|
2965
3474
|
let filePath;
|
|
2966
|
-
const mediaUrl =
|
|
3475
|
+
const mediaUrl = turn.media.url;
|
|
2967
3476
|
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
2968
3477
|
filePath = await downloadRemoteImageToTemp(mediaUrl, path9.join(resolveMediaTempDir(deps.mediaTempDir), "outbound"));
|
|
2969
3478
|
} else {
|
|
@@ -2972,20 +3481,24 @@ async function processOneMessage(full, deps) {
|
|
|
2972
3481
|
await sendWeixinMediaFile({
|
|
2973
3482
|
filePath,
|
|
2974
3483
|
to,
|
|
2975
|
-
text:
|
|
3484
|
+
text: turn.text ? markdownToPlainText(turn.text) : "",
|
|
2976
3485
|
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
|
|
2977
3486
|
cdnBaseUrl: deps.cdnBaseUrl
|
|
2978
3487
|
});
|
|
2979
|
-
} else if (
|
|
3488
|
+
} else if (turn.text) {
|
|
3489
|
+
const finalText = markdownToPlainText(turn.text).trim();
|
|
3490
|
+
if (finalText.length === 0) {
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
2980
3493
|
await sendMessageWeixin({
|
|
2981
3494
|
to,
|
|
2982
|
-
text:
|
|
3495
|
+
text: finalText,
|
|
2983
3496
|
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
|
|
2984
3497
|
});
|
|
2985
3498
|
}
|
|
2986
3499
|
} catch (err) {
|
|
2987
3500
|
const errorText = err instanceof Error ? err.stack ?? err.message : JSON.stringify(err);
|
|
2988
|
-
deps.errLog(`
|
|
3501
|
+
deps.errLog(`handleWeixinMessageTurn: agent or send failed: ${errorText}`);
|
|
2989
3502
|
sendWeixinErrorNotice({
|
|
2990
3503
|
to,
|
|
2991
3504
|
contextToken,
|
|
@@ -3011,7 +3524,7 @@ async function processOneMessage(full, deps) {
|
|
|
3011
3524
|
}
|
|
3012
3525
|
}
|
|
3013
3526
|
var hasDownloadableMedia = (media) => media?.encrypt_query_param || media?.full_url;
|
|
3014
|
-
var
|
|
3527
|
+
var init_handle_weixin_message_turn = __esm(() => {
|
|
3015
3528
|
init_api();
|
|
3016
3529
|
init_types();
|
|
3017
3530
|
init_upload();
|
|
@@ -3098,6 +3611,9 @@ async function monitorWeixinProvider(opts) {
|
|
|
3098
3611
|
log(`[weixin] no previous sync buf, starting fresh`);
|
|
3099
3612
|
}
|
|
3100
3613
|
const configManager = new WeixinConfigManager({ baseUrl, token }, log);
|
|
3614
|
+
const seenMessageIds = new Set;
|
|
3615
|
+
const messageIdOrder = [];
|
|
3616
|
+
const DEDUP_WINDOW = 100;
|
|
3101
3617
|
let nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
|
|
3102
3618
|
let consecutiveFailures = 0;
|
|
3103
3619
|
while (!abortSignal?.aborted) {
|
|
@@ -3141,10 +3657,22 @@ async function monitorWeixinProvider(opts) {
|
|
|
3141
3657
|
}
|
|
3142
3658
|
const list = resp.msgs ?? [];
|
|
3143
3659
|
for (const full of list) {
|
|
3660
|
+
const msgId = full.message_id;
|
|
3661
|
+
if (msgId != null) {
|
|
3662
|
+
if (seenMessageIds.has(msgId)) {
|
|
3663
|
+
aLog.info(`duplicate message skipped: message_id=${msgId}`);
|
|
3664
|
+
continue;
|
|
3665
|
+
}
|
|
3666
|
+
seenMessageIds.add(msgId);
|
|
3667
|
+
messageIdOrder.push(msgId);
|
|
3668
|
+
if (messageIdOrder.length > DEDUP_WINDOW) {
|
|
3669
|
+
seenMessageIds.delete(messageIdOrder.shift());
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3144
3672
|
aLog.info(`inbound: from=${full.from_user_id} types=${full.item_list?.map((i) => i.type).join(",") ?? "none"}`);
|
|
3145
3673
|
const fromUserId = full.from_user_id ?? "";
|
|
3146
3674
|
const cachedConfig = await configManager.getForUser(fromUserId, full.context_token);
|
|
3147
|
-
await
|
|
3675
|
+
await handleWeixinMessageTurn(full, {
|
|
3148
3676
|
accountId,
|
|
3149
3677
|
agent,
|
|
3150
3678
|
baseUrl,
|
|
@@ -3186,7 +3714,7 @@ var init_monitor = __esm(() => {
|
|
|
3186
3714
|
init_api();
|
|
3187
3715
|
init_config_cache();
|
|
3188
3716
|
init_session_guard();
|
|
3189
|
-
|
|
3717
|
+
init_handle_weixin_message_turn();
|
|
3190
3718
|
init_sync_buf();
|
|
3191
3719
|
init_logger();
|
|
3192
3720
|
});
|
|
@@ -3334,8 +3862,8 @@ var init_weixin_sdk = __esm(() => {
|
|
|
3334
3862
|
});
|
|
3335
3863
|
|
|
3336
3864
|
// src/logging/app-logger.ts
|
|
3337
|
-
import { appendFile, mkdir as
|
|
3338
|
-
import { basename, dirname as
|
|
3865
|
+
import { appendFile, mkdir as mkdir6, readdir, rename, rm as rm5, stat } from "node:fs/promises";
|
|
3866
|
+
import { basename, dirname as dirname5, join as join3 } from "node:path";
|
|
3339
3867
|
function createNoopAppLogger() {
|
|
3340
3868
|
return {
|
|
3341
3869
|
debug: async () => {},
|
|
@@ -3365,7 +3893,7 @@ function createAppLogger(options) {
|
|
|
3365
3893
|
return;
|
|
3366
3894
|
}
|
|
3367
3895
|
const line = formatLogLine(now(), level, event, message, context);
|
|
3368
|
-
await
|
|
3896
|
+
await mkdir6(dirname5(options.filePath), { recursive: true });
|
|
3369
3897
|
await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
|
|
3370
3898
|
await appendFile(options.filePath, line, "utf8");
|
|
3371
3899
|
}
|
|
@@ -3386,10 +3914,10 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
|
|
|
3386
3914
|
return;
|
|
3387
3915
|
}
|
|
3388
3916
|
if (maxFiles <= 0) {
|
|
3389
|
-
await
|
|
3917
|
+
await rm5(filePath, { force: true });
|
|
3390
3918
|
return;
|
|
3391
3919
|
}
|
|
3392
|
-
await
|
|
3920
|
+
await rm5(`${filePath}.${maxFiles}`, { force: true });
|
|
3393
3921
|
for (let index = maxFiles - 1;index >= 1; index -= 1) {
|
|
3394
3922
|
const source = `${filePath}.${index}`;
|
|
3395
3923
|
try {
|
|
@@ -3403,7 +3931,7 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
|
|
|
3403
3931
|
await rename(filePath, `${filePath}.1`);
|
|
3404
3932
|
}
|
|
3405
3933
|
async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
|
|
3406
|
-
const parentDir =
|
|
3934
|
+
const parentDir = dirname5(filePath);
|
|
3407
3935
|
const prefix = `${basename(filePath)}.`;
|
|
3408
3936
|
const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
|
|
3409
3937
|
let files = [];
|
|
@@ -3419,10 +3947,10 @@ async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
|
|
|
3419
3947
|
if (!file.startsWith(prefix) || !/^\d+$/.test(file.slice(prefix.length))) {
|
|
3420
3948
|
continue;
|
|
3421
3949
|
}
|
|
3422
|
-
const candidate =
|
|
3950
|
+
const candidate = join3(parentDir, file);
|
|
3423
3951
|
const details = await stat(candidate);
|
|
3424
3952
|
if (details.mtime.getTime() < cutoff) {
|
|
3425
|
-
await
|
|
3953
|
+
await rm5(candidate, { force: true });
|
|
3426
3954
|
}
|
|
3427
3955
|
}
|
|
3428
3956
|
}
|
|
@@ -3454,16 +3982,16 @@ var init_app_logger = __esm(() => {
|
|
|
3454
3982
|
});
|
|
3455
3983
|
|
|
3456
3984
|
// src/transport/acpx-session-index.ts
|
|
3457
|
-
import { readFile as
|
|
3458
|
-
import { homedir } from "node:os";
|
|
3985
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
3986
|
+
import { homedir as homedir2 } from "node:os";
|
|
3459
3987
|
import { resolve } from "node:path";
|
|
3460
3988
|
async function resolveSessionAgentCommandFromIndex(session) {
|
|
3461
|
-
const home = process.env.HOME ??
|
|
3989
|
+
const home = process.env.HOME ?? homedir2();
|
|
3462
3990
|
if (!home) {
|
|
3463
3991
|
return;
|
|
3464
3992
|
}
|
|
3465
3993
|
try {
|
|
3466
|
-
const raw = await
|
|
3994
|
+
const raw = await readFile4(resolve(home, ".acpx", "sessions", "index.json"), "utf8");
|
|
3467
3995
|
const parsed = JSON.parse(raw);
|
|
3468
3996
|
const targetCwd = resolve(session.cwd);
|
|
3469
3997
|
const match = parsed.entries?.find((entry) => entry.name === session.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
|
|
@@ -3614,8 +4142,10 @@ function parseCommand(input) {
|
|
|
3614
4142
|
}
|
|
3615
4143
|
const parts = tokenizeCommand(trimmed);
|
|
3616
4144
|
const command = normalizeCommand(parts[0] ?? "");
|
|
3617
|
-
if (command === "/help")
|
|
4145
|
+
if (command === "/help" && parts.length === 1)
|
|
3618
4146
|
return { kind: "help" };
|
|
4147
|
+
if (command === "/help" && parts.length === 2)
|
|
4148
|
+
return { kind: "help", topic: parts[1] };
|
|
3619
4149
|
if (command === "/agents")
|
|
3620
4150
|
return { kind: "agents" };
|
|
3621
4151
|
if (command === "/workspaces")
|
|
@@ -3630,6 +4160,10 @@ function parseCommand(input) {
|
|
|
3630
4160
|
return { kind: "session.reset" };
|
|
3631
4161
|
if (command === "/mode" && parts.length === 1)
|
|
3632
4162
|
return { kind: "mode.show" };
|
|
4163
|
+
if (command === "/replymode" && parts.length === 1)
|
|
4164
|
+
return { kind: "replymode.show" };
|
|
4165
|
+
if (command === "/config" && parts.length === 1)
|
|
4166
|
+
return { kind: "config.show" };
|
|
3633
4167
|
if (command === "/permission" && parts.length === 1)
|
|
3634
4168
|
return { kind: "permission.status" };
|
|
3635
4169
|
if (command === "/session" && parts.length === 1)
|
|
@@ -3653,12 +4187,21 @@ function parseCommand(input) {
|
|
|
3653
4187
|
return { kind: "permission.auto.set", policy };
|
|
3654
4188
|
}
|
|
3655
4189
|
}
|
|
4190
|
+
if (command === "/config" && parts[1] === "set" && parts.length === 4) {
|
|
4191
|
+
return { kind: "config.set", path: parts[2] ?? "", value: parts[3] ?? "" };
|
|
4192
|
+
}
|
|
3656
4193
|
if (command === "/use" && parts[1]) {
|
|
3657
4194
|
return { kind: "session.use", alias: parts[1] };
|
|
3658
4195
|
}
|
|
3659
4196
|
if (command === "/mode" && parts[1]) {
|
|
3660
4197
|
return { kind: "mode.set", modeId: parts[1] };
|
|
3661
4198
|
}
|
|
4199
|
+
if (command === "/replymode" && parts[1] === "reset" && parts.length === 2) {
|
|
4200
|
+
return { kind: "replymode.reset" };
|
|
4201
|
+
}
|
|
4202
|
+
if (command === "/replymode" && (parts[1] === "stream" || parts[1] === "final") && parts.length === 2) {
|
|
4203
|
+
return { kind: "replymode.set", replyMode: parts[1] };
|
|
4204
|
+
}
|
|
3662
4205
|
if (command === "/agent" && parts[1] === "add" && parts[2]) {
|
|
3663
4206
|
return { kind: "agent.add", template: parts[2] };
|
|
3664
4207
|
}
|
|
@@ -3668,13 +4211,23 @@ function parseCommand(input) {
|
|
|
3668
4211
|
if (command === "/workspace" && parts[1] === "new" && parts[2]) {
|
|
3669
4212
|
const name = parts[2];
|
|
3670
4213
|
let cwd = "";
|
|
4214
|
+
let invalid = false;
|
|
3671
4215
|
for (let index = 3;index < parts.length; index += 1) {
|
|
3672
4216
|
if (parts[index] === "--cwd" || parts[index] === "-d") {
|
|
4217
|
+
if (index + 1 >= parts.length) {
|
|
4218
|
+
invalid = true;
|
|
4219
|
+
break;
|
|
4220
|
+
}
|
|
3673
4221
|
cwd = parts[index + 1] ?? "";
|
|
3674
4222
|
index += 1;
|
|
4223
|
+
continue;
|
|
3675
4224
|
}
|
|
4225
|
+
invalid = true;
|
|
4226
|
+
break;
|
|
4227
|
+
}
|
|
4228
|
+
if (!invalid && name.trim().length > 0 && cwd.trim().length > 0) {
|
|
4229
|
+
return { kind: "workspace.new", name, cwd };
|
|
3676
4230
|
}
|
|
3677
|
-
return { kind: "workspace.new", name, cwd };
|
|
3678
4231
|
}
|
|
3679
4232
|
if (command === "/workspace" && parts[1] === "rm" && parts[2]) {
|
|
3680
4233
|
return { kind: "workspace.rm", name: parts[2] };
|
|
@@ -3684,26 +4237,41 @@ function parseCommand(input) {
|
|
|
3684
4237
|
const alias = parts[2];
|
|
3685
4238
|
let agent = "";
|
|
3686
4239
|
let workspace = "";
|
|
4240
|
+
let invalid = false;
|
|
3687
4241
|
for (let index = 3;index < parts.length; index += 1) {
|
|
3688
4242
|
if (parts[index] === "--agent" || parts[index] === "-a") {
|
|
4243
|
+
if (index + 1 >= parts.length) {
|
|
4244
|
+
invalid = true;
|
|
4245
|
+
break;
|
|
4246
|
+
}
|
|
3689
4247
|
agent = parts[index + 1] ?? "";
|
|
3690
4248
|
index += 1;
|
|
4249
|
+
continue;
|
|
3691
4250
|
} else if (parts[index] === "--ws" || parts[index] === "-ws") {
|
|
4251
|
+
if (index + 1 >= parts.length) {
|
|
4252
|
+
invalid = true;
|
|
4253
|
+
break;
|
|
4254
|
+
}
|
|
3692
4255
|
workspace = parts[index + 1] ?? "";
|
|
3693
4256
|
index += 1;
|
|
4257
|
+
continue;
|
|
3694
4258
|
}
|
|
4259
|
+
invalid = true;
|
|
4260
|
+
break;
|
|
4261
|
+
}
|
|
4262
|
+
if (!invalid && alias.trim().length > 0 && agent.trim().length > 0 && workspace.trim().length > 0) {
|
|
4263
|
+
return { kind: "session.new", alias, agent, workspace };
|
|
3695
4264
|
}
|
|
3696
|
-
return { kind: "session.new", alias, agent, workspace };
|
|
3697
4265
|
}
|
|
3698
|
-
const
|
|
3699
|
-
if (
|
|
3700
|
-
return { kind: "session.shortcut.new", agent: parts[2],
|
|
4266
|
+
const shortcutTarget = readSessionShortcutTarget(parts, 3);
|
|
4267
|
+
if (shortcutTarget) {
|
|
4268
|
+
return { kind: "session.shortcut.new", agent: parts[2], ...shortcutTarget };
|
|
3701
4269
|
}
|
|
3702
4270
|
}
|
|
3703
4271
|
if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach" && parts[1] !== "reset") {
|
|
3704
|
-
const
|
|
3705
|
-
if (
|
|
3706
|
-
return { kind: "session.shortcut", agent: parts[1],
|
|
4272
|
+
const shortcutTarget = readSessionShortcutTarget(parts, 2);
|
|
4273
|
+
if (shortcutTarget) {
|
|
4274
|
+
return { kind: "session.shortcut", agent: parts[1], ...shortcutTarget };
|
|
3707
4275
|
}
|
|
3708
4276
|
}
|
|
3709
4277
|
if (command === "/session" && parts[1] === "attach" && parts[2]) {
|
|
@@ -3711,19 +4279,39 @@ function parseCommand(input) {
|
|
|
3711
4279
|
let agent = "";
|
|
3712
4280
|
let workspace = "";
|
|
3713
4281
|
let transportSession = "";
|
|
4282
|
+
let invalid = false;
|
|
3714
4283
|
for (let index = 3;index < parts.length; index += 1) {
|
|
3715
4284
|
if (parts[index] === "--agent" || parts[index] === "-a") {
|
|
4285
|
+
if (index + 1 >= parts.length) {
|
|
4286
|
+
invalid = true;
|
|
4287
|
+
break;
|
|
4288
|
+
}
|
|
3716
4289
|
agent = parts[index + 1] ?? "";
|
|
3717
4290
|
index += 1;
|
|
4291
|
+
continue;
|
|
3718
4292
|
} else if (parts[index] === "--ws" || parts[index] === "-ws") {
|
|
4293
|
+
if (index + 1 >= parts.length) {
|
|
4294
|
+
invalid = true;
|
|
4295
|
+
break;
|
|
4296
|
+
}
|
|
3719
4297
|
workspace = parts[index + 1] ?? "";
|
|
3720
4298
|
index += 1;
|
|
4299
|
+
continue;
|
|
3721
4300
|
} else if (parts[index] === "--name") {
|
|
4301
|
+
if (index + 1 >= parts.length) {
|
|
4302
|
+
invalid = true;
|
|
4303
|
+
break;
|
|
4304
|
+
}
|
|
3722
4305
|
transportSession = parts[index + 1] ?? "";
|
|
3723
4306
|
index += 1;
|
|
4307
|
+
continue;
|
|
3724
4308
|
}
|
|
4309
|
+
invalid = true;
|
|
4310
|
+
break;
|
|
4311
|
+
}
|
|
4312
|
+
if (!invalid && alias.trim().length > 0 && agent.trim().length > 0 && workspace.trim().length > 0 && transportSession.trim().length > 0) {
|
|
4313
|
+
return { kind: "session.attach", alias, agent, workspace, transportSession };
|
|
3725
4314
|
}
|
|
3726
|
-
return { kind: "session.attach", alias, agent, workspace, transportSession };
|
|
3727
4315
|
}
|
|
3728
4316
|
if (command.startsWith("/") && isRecognizedCommand(command)) {
|
|
3729
4317
|
return { kind: "invalid", text: trimmed, recognizedCommand: command };
|
|
@@ -3733,13 +4321,42 @@ function parseCommand(input) {
|
|
|
3733
4321
|
function hasAnyFlag(parts, flags) {
|
|
3734
4322
|
return parts.some((part) => flags.includes(part));
|
|
3735
4323
|
}
|
|
3736
|
-
function
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
4324
|
+
function readSessionShortcutTarget(parts, startIndex) {
|
|
4325
|
+
let cwd = "";
|
|
4326
|
+
let workspace = "";
|
|
4327
|
+
let invalid = false;
|
|
4328
|
+
for (let index = startIndex;index < parts.length; index += 1) {
|
|
4329
|
+
if (parts[index] === "--cwd" || parts[index] === "-d") {
|
|
4330
|
+
if (index + 1 >= parts.length || workspace) {
|
|
4331
|
+
invalid = true;
|
|
4332
|
+
break;
|
|
4333
|
+
}
|
|
4334
|
+
cwd = parts[index + 1] ?? "";
|
|
4335
|
+
index += 1;
|
|
4336
|
+
continue;
|
|
3740
4337
|
}
|
|
4338
|
+
if (parts[index] === "--ws" || parts[index] === "-ws") {
|
|
4339
|
+
if (index + 1 >= parts.length || cwd) {
|
|
4340
|
+
invalid = true;
|
|
4341
|
+
break;
|
|
4342
|
+
}
|
|
4343
|
+
workspace = parts[index + 1] ?? "";
|
|
4344
|
+
index += 1;
|
|
4345
|
+
continue;
|
|
4346
|
+
}
|
|
4347
|
+
invalid = true;
|
|
4348
|
+
break;
|
|
3741
4349
|
}
|
|
3742
|
-
|
|
4350
|
+
if (invalid) {
|
|
4351
|
+
return null;
|
|
4352
|
+
}
|
|
4353
|
+
if (cwd.trim().length > 0) {
|
|
4354
|
+
return { cwd };
|
|
4355
|
+
}
|
|
4356
|
+
if (workspace.trim().length > 0) {
|
|
4357
|
+
return { workspace };
|
|
4358
|
+
}
|
|
4359
|
+
return null;
|
|
3743
4360
|
}
|
|
3744
4361
|
function normalizeCommand(command) {
|
|
3745
4362
|
if (command === "/ss")
|
|
@@ -3765,7 +4382,7 @@ function toPermissionMode(value) {
|
|
|
3765
4382
|
return null;
|
|
3766
4383
|
}
|
|
3767
4384
|
function toNonInteractivePermission(value) {
|
|
3768
|
-
if (value === "
|
|
4385
|
+
if (value === "deny" || value === "fail") {
|
|
3769
4386
|
return value;
|
|
3770
4387
|
}
|
|
3771
4388
|
return null;
|
|
@@ -3812,6 +4429,8 @@ var init_parse_command = __esm(() => {
|
|
|
3812
4429
|
"/cancel",
|
|
3813
4430
|
"/clear",
|
|
3814
4431
|
"/mode",
|
|
4432
|
+
"/replymode",
|
|
4433
|
+
"/config",
|
|
3815
4434
|
"/permission",
|
|
3816
4435
|
"/session",
|
|
3817
4436
|
"/workspace",
|
|
@@ -3820,6 +4439,17 @@ var init_parse_command = __esm(() => {
|
|
|
3820
4439
|
]);
|
|
3821
4440
|
});
|
|
3822
4441
|
|
|
4442
|
+
// src/commands/config-clone.ts
|
|
4443
|
+
function cloneAppConfig(config) {
|
|
4444
|
+
return {
|
|
4445
|
+
transport: { ...config.transport },
|
|
4446
|
+
logging: { ...config.logging },
|
|
4447
|
+
wechat: { ...config.wechat },
|
|
4448
|
+
agents: Object.fromEntries(Object.entries(config.agents).map(([name, agent]) => [name, { ...agent }])),
|
|
4449
|
+
workspaces: Object.fromEntries(Object.entries(config.workspaces).map(([name, workspace]) => [name, { ...workspace }]))
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
|
|
3823
4453
|
// src/commands/handlers/permission-handler.ts
|
|
3824
4454
|
function handlePermissionStatus(context, title) {
|
|
3825
4455
|
return { text: renderPermissionStatus(context.config, title) };
|
|
@@ -3828,9 +4458,17 @@ async function handlePermissionModeSet(context, mode) {
|
|
|
3828
4458
|
if (!context.config || !context.configStore) {
|
|
3829
4459
|
return { text: "当前没有加载可写入的配置。" };
|
|
3830
4460
|
}
|
|
4461
|
+
const previous = cloneAppConfig(context.config);
|
|
3831
4462
|
const updated = await context.configStore.updateTransport({
|
|
3832
4463
|
permissionMode: mode
|
|
3833
4464
|
});
|
|
4465
|
+
try {
|
|
4466
|
+
await context.transport.updatePermissionPolicy?.(updated.transport);
|
|
4467
|
+
} catch (error) {
|
|
4468
|
+
await context.configStore.save(previous);
|
|
4469
|
+
context.replaceConfig(previous);
|
|
4470
|
+
throw error;
|
|
4471
|
+
}
|
|
3834
4472
|
context.replaceConfig(updated);
|
|
3835
4473
|
return { text: renderPermissionStatus(context.config, "权限模式已更新:") };
|
|
3836
4474
|
}
|
|
@@ -3841,18 +4479,226 @@ async function handlePermissionAutoSet(context, policy) {
|
|
|
3841
4479
|
if (!context.config || !context.configStore) {
|
|
3842
4480
|
return { text: "当前没有加载可写入的配置。" };
|
|
3843
4481
|
}
|
|
4482
|
+
const previous = cloneAppConfig(context.config);
|
|
3844
4483
|
const updated = await context.configStore.updateTransport({
|
|
3845
4484
|
nonInteractivePermissions: policy
|
|
3846
4485
|
});
|
|
4486
|
+
try {
|
|
4487
|
+
await context.transport.updatePermissionPolicy?.(updated.transport);
|
|
4488
|
+
} catch (error) {
|
|
4489
|
+
await context.configStore.save(previous);
|
|
4490
|
+
context.replaceConfig(previous);
|
|
4491
|
+
throw error;
|
|
4492
|
+
}
|
|
3847
4493
|
context.replaceConfig(updated);
|
|
3848
4494
|
return { text: renderPermissionStatus(context.config, "非交互策略已更新:") };
|
|
3849
4495
|
}
|
|
3850
4496
|
function renderPermissionStatus(config, title) {
|
|
3851
4497
|
const permissionMode = config?.transport.permissionMode ?? "approve-all";
|
|
3852
|
-
const nonInteractivePermissions = config?.transport.nonInteractivePermissions ?? "
|
|
4498
|
+
const nonInteractivePermissions = config?.transport.nonInteractivePermissions ?? "deny";
|
|
3853
4499
|
return [title, `- mode: ${permissionMode}`, `- auto: ${nonInteractivePermissions}`].join(`
|
|
3854
4500
|
`);
|
|
3855
4501
|
}
|
|
4502
|
+
var permissionHelp;
|
|
4503
|
+
var init_permission_handler = __esm(() => {
|
|
4504
|
+
permissionHelp = {
|
|
4505
|
+
topic: "permission",
|
|
4506
|
+
aliases: ["pm"],
|
|
4507
|
+
summary: "查看和修改 transport 权限策略。",
|
|
4508
|
+
commands: [
|
|
4509
|
+
{ usage: "/pm 或 /permission", description: "查看当前权限模式" },
|
|
4510
|
+
{ usage: "/pm set <allow|read|deny>", description: "设置审批级别" },
|
|
4511
|
+
{ usage: "/pm auto", description: "查看当前非交互策略" },
|
|
4512
|
+
{ usage: "/pm auto <deny|fail>", description: "设置非交互策略" }
|
|
4513
|
+
],
|
|
4514
|
+
examples: ["/pm set read", "/pm auto deny"]
|
|
4515
|
+
};
|
|
4516
|
+
});
|
|
4517
|
+
|
|
4518
|
+
// src/commands/handlers/config-handler.ts
|
|
4519
|
+
function handleConfigShow(context) {
|
|
4520
|
+
const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path11) => `- ${path11}`)];
|
|
4521
|
+
if (context.config) {
|
|
4522
|
+
lines.push("", "示例:", "- /config set wechat.replyMode final", "- /config set logging.level debug");
|
|
4523
|
+
}
|
|
4524
|
+
return { text: lines.join(`
|
|
4525
|
+
`) };
|
|
4526
|
+
}
|
|
4527
|
+
async function handleConfigSet(context, path11, rawValue) {
|
|
4528
|
+
if (!context.config || !context.configStore) {
|
|
4529
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
4530
|
+
}
|
|
4531
|
+
const previous = cloneAppConfig(context.config);
|
|
4532
|
+
const updated = cloneAppConfig(context.config);
|
|
4533
|
+
const result = applySupportedConfigUpdate(updated, path11, rawValue);
|
|
4534
|
+
if ("error" in result) {
|
|
4535
|
+
return { text: result.error };
|
|
4536
|
+
}
|
|
4537
|
+
await context.configStore.save(updated);
|
|
4538
|
+
if (path11 === "transport.permissionMode" || path11 === "transport.nonInteractivePermissions") {
|
|
4539
|
+
try {
|
|
4540
|
+
await context.transport.updatePermissionPolicy?.(updated.transport);
|
|
4541
|
+
} catch (error) {
|
|
4542
|
+
await context.configStore.save(previous);
|
|
4543
|
+
context.replaceConfig(previous);
|
|
4544
|
+
throw error;
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
context.replaceConfig(updated);
|
|
4548
|
+
return { text: `配置已更新:${path11} = ${result.renderedValue}` };
|
|
4549
|
+
}
|
|
4550
|
+
function applySupportedConfigUpdate(config, path11, rawValue) {
|
|
4551
|
+
switch (path11) {
|
|
4552
|
+
case "transport.type": {
|
|
4553
|
+
const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
|
|
4554
|
+
if (!parsed)
|
|
4555
|
+
return { error: "transport.type 只支持:acpx-cli、acpx-bridge" };
|
|
4556
|
+
config.transport.type = parsed;
|
|
4557
|
+
return { renderedValue: parsed };
|
|
4558
|
+
}
|
|
4559
|
+
case "transport.command":
|
|
4560
|
+
if (!rawValue.trim())
|
|
4561
|
+
return { error: "transport.command 不能为空。" };
|
|
4562
|
+
config.transport.command = rawValue;
|
|
4563
|
+
return { renderedValue: rawValue };
|
|
4564
|
+
case "transport.sessionInitTimeoutMs": {
|
|
4565
|
+
const parsed = parsePositiveNumber(rawValue, "transport.sessionInitTimeoutMs");
|
|
4566
|
+
if ("error" in parsed)
|
|
4567
|
+
return parsed;
|
|
4568
|
+
config.transport.sessionInitTimeoutMs = parsed.value;
|
|
4569
|
+
return { renderedValue: String(parsed.value) };
|
|
4570
|
+
}
|
|
4571
|
+
case "transport.permissionMode": {
|
|
4572
|
+
const parsed = parseEnum(rawValue, ["approve-all", "approve-reads", "deny-all"]);
|
|
4573
|
+
if (!parsed)
|
|
4574
|
+
return { error: "transport.permissionMode 只支持:approve-all、approve-reads、deny-all" };
|
|
4575
|
+
config.transport.permissionMode = parsed;
|
|
4576
|
+
return { renderedValue: parsed };
|
|
4577
|
+
}
|
|
4578
|
+
case "transport.nonInteractivePermissions": {
|
|
4579
|
+
const parsed = parseEnum(rawValue, ["deny", "fail"]);
|
|
4580
|
+
if (!parsed)
|
|
4581
|
+
return { error: "transport.nonInteractivePermissions 只支持:deny、fail" };
|
|
4582
|
+
config.transport.nonInteractivePermissions = parsed;
|
|
4583
|
+
return { renderedValue: parsed };
|
|
4584
|
+
}
|
|
4585
|
+
case "logging.level": {
|
|
4586
|
+
const parsed = parseEnum(rawValue, ["error", "info", "debug"]);
|
|
4587
|
+
if (!parsed)
|
|
4588
|
+
return { error: "logging.level 只支持:error、info、debug" };
|
|
4589
|
+
config.logging.level = parsed;
|
|
4590
|
+
return { renderedValue: parsed };
|
|
4591
|
+
}
|
|
4592
|
+
case "logging.maxSizeBytes": {
|
|
4593
|
+
const parsed = parsePositiveNumber(rawValue, "logging.maxSizeBytes");
|
|
4594
|
+
if ("error" in parsed)
|
|
4595
|
+
return parsed;
|
|
4596
|
+
config.logging.maxSizeBytes = parsed.value;
|
|
4597
|
+
return { renderedValue: String(parsed.value) };
|
|
4598
|
+
}
|
|
4599
|
+
case "logging.maxFiles": {
|
|
4600
|
+
const parsed = parsePositiveNumber(rawValue, "logging.maxFiles");
|
|
4601
|
+
if ("error" in parsed)
|
|
4602
|
+
return parsed;
|
|
4603
|
+
config.logging.maxFiles = parsed.value;
|
|
4604
|
+
return { renderedValue: String(parsed.value) };
|
|
4605
|
+
}
|
|
4606
|
+
case "logging.retentionDays": {
|
|
4607
|
+
const parsed = parsePositiveNumber(rawValue, "logging.retentionDays");
|
|
4608
|
+
if ("error" in parsed)
|
|
4609
|
+
return parsed;
|
|
4610
|
+
config.logging.retentionDays = parsed.value;
|
|
4611
|
+
return { renderedValue: String(parsed.value) };
|
|
4612
|
+
}
|
|
4613
|
+
case "wechat.replyMode": {
|
|
4614
|
+
const parsed = parseEnum(rawValue, ["stream", "final"]);
|
|
4615
|
+
if (!parsed)
|
|
4616
|
+
return { error: "wechat.replyMode 只支持:stream、final" };
|
|
4617
|
+
config.wechat.replyMode = parsed;
|
|
4618
|
+
return { renderedValue: parsed };
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
const agentMatch = path11.match(/^agents\.([^.]+)\.(driver|command)$/);
|
|
4622
|
+
if (agentMatch) {
|
|
4623
|
+
const [, name, field] = agentMatch;
|
|
4624
|
+
if (!name || !field) {
|
|
4625
|
+
return { error: `不支持修改这个配置路径:${path11}` };
|
|
4626
|
+
}
|
|
4627
|
+
const agent = config.agents[name];
|
|
4628
|
+
if (!agent) {
|
|
4629
|
+
return { error: `Agent「${name}」不存在,请先创建。` };
|
|
4630
|
+
}
|
|
4631
|
+
if (!rawValue.trim()) {
|
|
4632
|
+
return { error: `${path11} 不能为空。` };
|
|
4633
|
+
}
|
|
4634
|
+
if (field === "driver") {
|
|
4635
|
+
agent.driver = rawValue;
|
|
4636
|
+
} else {
|
|
4637
|
+
agent.command = rawValue;
|
|
4638
|
+
}
|
|
4639
|
+
return { renderedValue: rawValue };
|
|
4640
|
+
}
|
|
4641
|
+
const workspaceMatch = path11.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
|
|
4642
|
+
if (workspaceMatch) {
|
|
4643
|
+
const [, name, field] = workspaceMatch;
|
|
4644
|
+
if (!name || !field) {
|
|
4645
|
+
return { error: `不支持修改这个配置路径:${path11}` };
|
|
4646
|
+
}
|
|
4647
|
+
const workspace = config.workspaces[name];
|
|
4648
|
+
if (!workspace) {
|
|
4649
|
+
return { error: `工作区「${name}」不存在,请先创建。` };
|
|
4650
|
+
}
|
|
4651
|
+
if (!rawValue.trim()) {
|
|
4652
|
+
return { error: `${path11} 不能为空。` };
|
|
4653
|
+
}
|
|
4654
|
+
if (field === "cwd") {
|
|
4655
|
+
workspace.cwd = rawValue;
|
|
4656
|
+
} else {
|
|
4657
|
+
workspace.description = rawValue;
|
|
4658
|
+
}
|
|
4659
|
+
return { renderedValue: rawValue };
|
|
4660
|
+
}
|
|
4661
|
+
return { error: `不支持修改这个配置路径:${path11}` };
|
|
4662
|
+
}
|
|
4663
|
+
function parseEnum(value, allowed) {
|
|
4664
|
+
return allowed.includes(value) ? value : null;
|
|
4665
|
+
}
|
|
4666
|
+
function parsePositiveNumber(rawValue, path11) {
|
|
4667
|
+
const value = Number(rawValue);
|
|
4668
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
4669
|
+
return { error: `${path11} 必须是正数。` };
|
|
4670
|
+
}
|
|
4671
|
+
return { value };
|
|
4672
|
+
}
|
|
4673
|
+
var SUPPORTED_CONFIG_PATHS, configHelp;
|
|
4674
|
+
var init_config_handler = __esm(() => {
|
|
4675
|
+
SUPPORTED_CONFIG_PATHS = [
|
|
4676
|
+
"transport.type",
|
|
4677
|
+
"transport.command",
|
|
4678
|
+
"transport.sessionInitTimeoutMs",
|
|
4679
|
+
"transport.permissionMode",
|
|
4680
|
+
"transport.nonInteractivePermissions",
|
|
4681
|
+
"logging.level",
|
|
4682
|
+
"logging.maxSizeBytes",
|
|
4683
|
+
"logging.maxFiles",
|
|
4684
|
+
"logging.retentionDays",
|
|
4685
|
+
"wechat.replyMode",
|
|
4686
|
+
"agents.<name>.driver",
|
|
4687
|
+
"agents.<name>.command",
|
|
4688
|
+
"workspaces.<name>.cwd",
|
|
4689
|
+
"workspaces.<name>.description"
|
|
4690
|
+
];
|
|
4691
|
+
configHelp = {
|
|
4692
|
+
topic: "config",
|
|
4693
|
+
aliases: [],
|
|
4694
|
+
summary: "查看和修改受支持的配置字段。",
|
|
4695
|
+
commands: [
|
|
4696
|
+
{ usage: "/config", description: "查看当前支持修改的配置路径" },
|
|
4697
|
+
{ usage: "/config set <path> <value>", description: "修改一个受支持的配置值" }
|
|
4698
|
+
],
|
|
4699
|
+
examples: ["/config set wechat.replyMode final", "/config set logging.level debug"]
|
|
4700
|
+
};
|
|
4701
|
+
});
|
|
3856
4702
|
|
|
3857
4703
|
// src/commands/handlers/session-handler.ts
|
|
3858
4704
|
async function handleSessions(context, chatKey) {
|
|
@@ -3889,8 +4735,8 @@ async function handleSessionNew(context, chatKey, alias, agent, workspace) {
|
|
|
3889
4735
|
});
|
|
3890
4736
|
return { text: `会话「${alias}」已创建并切换` };
|
|
3891
4737
|
}
|
|
3892
|
-
async function handleSessionShortcut(context, chatKey, agent,
|
|
3893
|
-
return await context.lifecycle.handleSessionShortcut(chatKey, agent,
|
|
4738
|
+
async function handleSessionShortcut(context, chatKey, agent, target, createNew) {
|
|
4739
|
+
return await context.lifecycle.handleSessionShortcut(chatKey, agent, target, createNew);
|
|
3894
4740
|
}
|
|
3895
4741
|
async function handleSessionAttach(context, chatKey, alias, agent, workspace, transportSession) {
|
|
3896
4742
|
const attached = context.lifecycle.resolveSession(alias, agent, workspace, transportSession);
|
|
@@ -3923,28 +4769,64 @@ async function handleSessionUse(context, chatKey, alias) {
|
|
|
3923
4769
|
});
|
|
3924
4770
|
return { text: `已切换到会话「${alias}」` };
|
|
3925
4771
|
}
|
|
3926
|
-
async function handleModeShow(context, chatKey) {
|
|
4772
|
+
async function handleModeShow(context, chatKey) {
|
|
4773
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4774
|
+
if (!session) {
|
|
4775
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4776
|
+
}
|
|
4777
|
+
return {
|
|
4778
|
+
text: [
|
|
4779
|
+
"当前 mode:",
|
|
4780
|
+
`- 会话:${session.alias}`,
|
|
4781
|
+
`- mode:${session.modeId ?? "未设置"}`
|
|
4782
|
+
].join(`
|
|
4783
|
+
`)
|
|
4784
|
+
};
|
|
4785
|
+
}
|
|
4786
|
+
async function handleModeSet(context, chatKey, modeId) {
|
|
4787
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4788
|
+
if (!session) {
|
|
4789
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4790
|
+
}
|
|
4791
|
+
await context.interaction.setModeTransportSession(session, modeId);
|
|
4792
|
+
await context.sessions.setCurrentSessionMode(chatKey, modeId);
|
|
4793
|
+
return { text: `已设置当前会话 mode:${modeId}` };
|
|
4794
|
+
}
|
|
4795
|
+
async function handleReplyModeShow(context, chatKey) {
|
|
3927
4796
|
const session = await context.sessions.getCurrentSession(chatKey);
|
|
3928
4797
|
if (!session) {
|
|
3929
4798
|
return { text: NO_CURRENT_SESSION_TEXT };
|
|
3930
4799
|
}
|
|
4800
|
+
const globalDefault = context.config?.wechat.replyMode ?? "stream";
|
|
4801
|
+
const sessionOverride = session.replyMode;
|
|
4802
|
+
const effective = sessionOverride ?? globalDefault;
|
|
3931
4803
|
return {
|
|
3932
4804
|
text: [
|
|
3933
|
-
"当前 mode:",
|
|
4805
|
+
"当前 reply mode:",
|
|
3934
4806
|
`- 会话:${session.alias}`,
|
|
3935
|
-
`-
|
|
4807
|
+
`- 全局默认:${globalDefault}`,
|
|
4808
|
+
`- 当前会话覆盖:${sessionOverride ?? "未设置"}`,
|
|
4809
|
+
`- 当前生效:${effective}`
|
|
3936
4810
|
].join(`
|
|
3937
4811
|
`)
|
|
3938
4812
|
};
|
|
3939
4813
|
}
|
|
3940
|
-
async function
|
|
4814
|
+
async function handleReplyModeSet(context, chatKey, replyMode) {
|
|
3941
4815
|
const session = await context.sessions.getCurrentSession(chatKey);
|
|
3942
4816
|
if (!session) {
|
|
3943
4817
|
return { text: NO_CURRENT_SESSION_TEXT };
|
|
3944
4818
|
}
|
|
3945
|
-
await context.
|
|
3946
|
-
|
|
3947
|
-
|
|
4819
|
+
await context.sessions.setCurrentSessionReplyMode(chatKey, replyMode);
|
|
4820
|
+
return { text: `已设置当前会话 reply mode:${replyMode}` };
|
|
4821
|
+
}
|
|
4822
|
+
async function handleReplyModeReset(context, chatKey) {
|
|
4823
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4824
|
+
if (!session) {
|
|
4825
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4826
|
+
}
|
|
4827
|
+
await context.sessions.setCurrentSessionReplyMode(chatKey, undefined);
|
|
4828
|
+
const globalDefault = context.config?.wechat.replyMode ?? "stream";
|
|
4829
|
+
return { text: `已重置当前会话 reply mode,当前回退到全局默认:${globalDefault}` };
|
|
3948
4830
|
}
|
|
3949
4831
|
async function handleStatus(context, chatKey) {
|
|
3950
4832
|
const session = await context.sessions.getCurrentSession(chatKey);
|
|
@@ -3976,24 +4858,85 @@ async function handleCancel(context, chatKey) {
|
|
|
3976
4858
|
async function handleSessionReset(context, chatKey) {
|
|
3977
4859
|
return await context.lifecycle.resetCurrentSession(chatKey);
|
|
3978
4860
|
}
|
|
4861
|
+
async function promptWithSession(context, session, text, reply) {
|
|
4862
|
+
const effectiveReplyMode = session.replyMode ?? context.config?.wechat.replyMode ?? "stream";
|
|
4863
|
+
const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
|
|
4864
|
+
const result = await context.interaction.promptTransportSession(session, text, transportReply);
|
|
4865
|
+
return { text: transportReply ? undefined : result.text };
|
|
4866
|
+
}
|
|
3979
4867
|
async function handlePrompt(context, chatKey, text, reply) {
|
|
3980
4868
|
const session = await context.sessions.getCurrentSession(chatKey);
|
|
3981
4869
|
if (!session) {
|
|
3982
4870
|
return { text: NO_CURRENT_SESSION_TEXT };
|
|
3983
4871
|
}
|
|
3984
4872
|
try {
|
|
3985
|
-
|
|
3986
|
-
return { text: result.text };
|
|
4873
|
+
return await promptWithSession(context, session, text, reply);
|
|
3987
4874
|
} catch (error) {
|
|
3988
4875
|
const recovered = await context.recovery.tryRecoverMissingSession(session, error);
|
|
3989
4876
|
if (recovered) {
|
|
3990
|
-
|
|
3991
|
-
return { text: result.text };
|
|
4877
|
+
return await promptWithSession(context, recovered, text, reply);
|
|
3992
4878
|
}
|
|
3993
4879
|
return context.recovery.renderTransportError(session, error);
|
|
3994
4880
|
}
|
|
3995
4881
|
}
|
|
3996
|
-
var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。";
|
|
4882
|
+
var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。", sessionHelp, modeHelp, replyModeHelp, statusHelp, cancelHelp;
|
|
4883
|
+
var init_session_handler = __esm(() => {
|
|
4884
|
+
sessionHelp = {
|
|
4885
|
+
topic: "session",
|
|
4886
|
+
aliases: ["ss", "sessions"],
|
|
4887
|
+
summary: "创建、恢复、切换和重置逻辑会话。",
|
|
4888
|
+
commands: [
|
|
4889
|
+
{ usage: "/sessions", description: "查看当前会话列表" },
|
|
4890
|
+
{ usage: "/session 或 /ss", description: "查看会话列表" },
|
|
4891
|
+
{ usage: "/ss <agent> (-d <path> | --ws <name>)", description: "快速新建或复用一个会话" },
|
|
4892
|
+
{ usage: "/ss new <agent> (-d <path> | --ws <name>)", description: "强制新建会话" },
|
|
4893
|
+
{ usage: "/ss new <alias> -a <name> --ws <name>", description: "按指定配置新建会话" },
|
|
4894
|
+
{ usage: "/ss attach <alias> -a <name> --ws <name> --name <transport-session>", description: "绑定已有会话" },
|
|
4895
|
+
{ usage: "/use <alias>", description: "切换当前会话" },
|
|
4896
|
+
{ usage: "/session reset 或 /clear", description: "重置当前会话上下文" }
|
|
4897
|
+
],
|
|
4898
|
+
examples: ["/ss codex -d /absolute/path/to/repo", "/use backend-fix", "/session reset"]
|
|
4899
|
+
};
|
|
4900
|
+
modeHelp = {
|
|
4901
|
+
topic: "mode",
|
|
4902
|
+
aliases: [],
|
|
4903
|
+
summary: "查看或设置当前会话 mode。",
|
|
4904
|
+
commands: [
|
|
4905
|
+
{ usage: "/mode", description: "查看当前会话已保存的 mode" },
|
|
4906
|
+
{ usage: "/mode <id>", description: "设置当前会话 mode" }
|
|
4907
|
+
],
|
|
4908
|
+
examples: ["/mode", "/mode plan"]
|
|
4909
|
+
};
|
|
4910
|
+
replyModeHelp = {
|
|
4911
|
+
topic: "replymode",
|
|
4912
|
+
aliases: [],
|
|
4913
|
+
summary: "查看或设置当前逻辑会话的回复输出模式。",
|
|
4914
|
+
commands: [
|
|
4915
|
+
{ usage: "/replymode", description: "查看全局默认、当前覆盖和实际生效值" },
|
|
4916
|
+
{ usage: "/replymode stream", description: "当前会话使用流式回复" },
|
|
4917
|
+
{ usage: "/replymode final", description: "当前会话只发送最终文本" },
|
|
4918
|
+
{ usage: "/replymode reset", description: "清除当前会话覆盖并回退到全局默认" }
|
|
4919
|
+
],
|
|
4920
|
+
examples: ["/replymode", "/replymode final"]
|
|
4921
|
+
};
|
|
4922
|
+
statusHelp = {
|
|
4923
|
+
topic: "status",
|
|
4924
|
+
aliases: [],
|
|
4925
|
+
summary: "查看当前选中会话的状态。",
|
|
4926
|
+
commands: [{ usage: "/status", description: "查看当前会话状态" }],
|
|
4927
|
+
examples: ["/status"]
|
|
4928
|
+
};
|
|
4929
|
+
cancelHelp = {
|
|
4930
|
+
topic: "cancel",
|
|
4931
|
+
aliases: ["stop"],
|
|
4932
|
+
summary: "取消当前会话里正在执行的任务。",
|
|
4933
|
+
commands: [
|
|
4934
|
+
{ usage: "/cancel", description: "取消当前任务" },
|
|
4935
|
+
{ usage: "/stop", description: "取消当前任务(/cancel 别名)" }
|
|
4936
|
+
],
|
|
4937
|
+
examples: ["/cancel"]
|
|
4938
|
+
};
|
|
4939
|
+
});
|
|
3997
4940
|
|
|
3998
4941
|
// src/commands/transport-diagnostics.ts
|
|
3999
4942
|
function summarizeTransportError(message) {
|
|
@@ -4057,47 +5000,6 @@ function isPartialPromptOutputError(message) {
|
|
|
4057
5000
|
}
|
|
4058
5001
|
|
|
4059
5002
|
// src/formatting/render-text.ts
|
|
4060
|
-
function renderHelpText() {
|
|
4061
|
-
return [
|
|
4062
|
-
"可用命令:",
|
|
4063
|
-
"",
|
|
4064
|
-
"先看这 3 个:",
|
|
4065
|
-
"/ss new <agent> -d <path> - 新建会话",
|
|
4066
|
-
"/use <alias> - 切会话",
|
|
4067
|
-
"/status - 看状态",
|
|
4068
|
-
"",
|
|
4069
|
-
"Agent:",
|
|
4070
|
-
"/agents - 看 Agent",
|
|
4071
|
-
"/agent add <codex|claude> - 加 Agent",
|
|
4072
|
-
"/agent rm <name> - 删 Agent",
|
|
4073
|
-
"",
|
|
4074
|
-
"工作区:",
|
|
4075
|
-
"/workspaces - 看工作区",
|
|
4076
|
-
"/workspace 或 /ws - 工作区命令",
|
|
4077
|
-
"/ws new <name> -d <path> - 加工作区",
|
|
4078
|
-
"/workspace rm <name> - 删工作区",
|
|
4079
|
-
"",
|
|
4080
|
-
"会话:",
|
|
4081
|
-
"/sessions - 看会话",
|
|
4082
|
-
"/session 或 /ss - 会话命令",
|
|
4083
|
-
"/ss <agent> -d <path> - 快速新建",
|
|
4084
|
-
"/ss new <agent> -d <path> - 新建会话",
|
|
4085
|
-
"/ss new <alias> -a <name> --ws <name> - 指定配置新建",
|
|
4086
|
-
"/ss attach <alias> -a <name> --ws <name> --name <transport-session> - 挂已有会话",
|
|
4087
|
-
"/use <alias> - 切会话",
|
|
4088
|
-
"/session reset 或 /clear - 清上下文",
|
|
4089
|
-
"",
|
|
4090
|
-
"权限:",
|
|
4091
|
-
"/pm 或 /permission - 权限设置",
|
|
4092
|
-
"/pm set <allow|read|deny> - 设审批级别",
|
|
4093
|
-
"/pm auto [allow|deny|fail] - 设自动处理",
|
|
4094
|
-
"",
|
|
4095
|
-
"常用:",
|
|
4096
|
-
"/status - 看状态",
|
|
4097
|
-
"/cancel 或 /stop - 停当前任务"
|
|
4098
|
-
].join(`
|
|
4099
|
-
`);
|
|
4100
|
-
}
|
|
4101
5003
|
function renderAgents(config) {
|
|
4102
5004
|
const names = Object.keys(config.agents);
|
|
4103
5005
|
if (names.length === 0) {
|
|
@@ -4115,12 +5017,6 @@ function renderWorkspaces(config) {
|
|
|
4115
5017
|
`);
|
|
4116
5018
|
}
|
|
4117
5019
|
|
|
4118
|
-
// src/commands/handlers/help-handler.ts
|
|
4119
|
-
function handleHelp() {
|
|
4120
|
-
return { text: renderHelpText() };
|
|
4121
|
-
}
|
|
4122
|
-
var init_help_handler = () => {};
|
|
4123
|
-
|
|
4124
5020
|
// src/config/agent-templates.ts
|
|
4125
5021
|
function getAgentTemplate(name) {
|
|
4126
5022
|
const template = TEMPLATES[name];
|
|
@@ -4173,13 +5069,25 @@ async function handleAgentRemove(context, agentName) {
|
|
|
4173
5069
|
context.replaceConfig(updated);
|
|
4174
5070
|
return { text: `Agent「${agentName}」已删除` };
|
|
4175
5071
|
}
|
|
5072
|
+
var agentHelp;
|
|
4176
5073
|
var init_agent_handler = __esm(() => {
|
|
4177
5074
|
init_agent_templates();
|
|
5075
|
+
agentHelp = {
|
|
5076
|
+
topic: "agent",
|
|
5077
|
+
aliases: ["agents"],
|
|
5078
|
+
summary: "管理已注册的 Agent。",
|
|
5079
|
+
commands: [
|
|
5080
|
+
{ usage: "/agents", description: "查看当前已注册的 Agent" },
|
|
5081
|
+
{ usage: "/agent add <codex|claude>", description: "添加内置 Agent 模板" },
|
|
5082
|
+
{ usage: "/agent rm <name>", description: "删除一个 Agent" }
|
|
5083
|
+
],
|
|
5084
|
+
examples: ["/agent add claude", "/agent rm codex"]
|
|
5085
|
+
};
|
|
4178
5086
|
});
|
|
4179
5087
|
|
|
4180
5088
|
// src/commands/handlers/workspace-handler.ts
|
|
4181
5089
|
import { access } from "node:fs/promises";
|
|
4182
|
-
import { homedir as
|
|
5090
|
+
import { homedir as homedir3 } from "node:os";
|
|
4183
5091
|
import { normalize } from "node:path";
|
|
4184
5092
|
function handleWorkspaces(context) {
|
|
4185
5093
|
return { text: context.config ? renderWorkspaces(context.config) : "No config loaded." };
|
|
@@ -4213,24 +5121,127 @@ async function pathExists(path11) {
|
|
|
4213
5121
|
}
|
|
4214
5122
|
}
|
|
4215
5123
|
function normalizePathForWorkspace(path11) {
|
|
4216
|
-
const expanded = path11.startsWith("~") ?
|
|
5124
|
+
const expanded = path11.startsWith("~") ? homedir3() + path11.slice(1) : path11;
|
|
4217
5125
|
return normalize(expanded);
|
|
4218
5126
|
}
|
|
4219
|
-
var
|
|
5127
|
+
var workspaceHelp;
|
|
5128
|
+
var init_workspace_handler = __esm(() => {
|
|
5129
|
+
workspaceHelp = {
|
|
5130
|
+
topic: "workspace",
|
|
5131
|
+
aliases: ["ws", "workspaces"],
|
|
5132
|
+
summary: "管理已注册的工作区。",
|
|
5133
|
+
commands: [
|
|
5134
|
+
{ usage: "/workspaces", description: "查看当前已注册的工作区" },
|
|
5135
|
+
{ usage: "/workspace 或 /ws", description: "查看工作区列表" },
|
|
5136
|
+
{ usage: "/ws new <name> -d <path>", description: "添加工作区" },
|
|
5137
|
+
{ usage: "/workspace rm <name>", description: "删除工作区" }
|
|
5138
|
+
],
|
|
5139
|
+
examples: ['/ws new backend -d "/tmp/backend"', "/workspace rm backend"]
|
|
5140
|
+
};
|
|
5141
|
+
});
|
|
5142
|
+
|
|
5143
|
+
// src/commands/help/help-registry.ts
|
|
5144
|
+
function getHelpTopic(topic) {
|
|
5145
|
+
return HELP_TOPIC_MAP.get(topic) ?? null;
|
|
5146
|
+
}
|
|
5147
|
+
function listHelpTopics() {
|
|
5148
|
+
return HELP_TOPICS;
|
|
5149
|
+
}
|
|
5150
|
+
var HELP_TOPICS, HELP_TOPIC_MAP;
|
|
5151
|
+
var init_help_registry = __esm(() => {
|
|
5152
|
+
init_agent_handler();
|
|
5153
|
+
init_config_handler();
|
|
5154
|
+
init_permission_handler();
|
|
5155
|
+
init_session_handler();
|
|
5156
|
+
init_workspace_handler();
|
|
5157
|
+
HELP_TOPICS = [
|
|
5158
|
+
sessionHelp,
|
|
5159
|
+
workspaceHelp,
|
|
5160
|
+
agentHelp,
|
|
5161
|
+
permissionHelp,
|
|
5162
|
+
configHelp,
|
|
5163
|
+
modeHelp,
|
|
5164
|
+
replyModeHelp,
|
|
5165
|
+
statusHelp,
|
|
5166
|
+
cancelHelp
|
|
5167
|
+
];
|
|
5168
|
+
HELP_TOPIC_MAP = new Map;
|
|
5169
|
+
for (const topic of HELP_TOPICS) {
|
|
5170
|
+
HELP_TOPIC_MAP.set(topic.topic, topic);
|
|
5171
|
+
for (const alias of topic.aliases) {
|
|
5172
|
+
HELP_TOPIC_MAP.set(alias, topic);
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
});
|
|
5176
|
+
|
|
5177
|
+
// src/commands/handlers/help-handler.ts
|
|
5178
|
+
function handleHelp(topic) {
|
|
5179
|
+
if (!topic) {
|
|
5180
|
+
return { text: renderHelpIndex() };
|
|
5181
|
+
}
|
|
5182
|
+
const entry = getHelpTopic(topic);
|
|
5183
|
+
if (!entry) {
|
|
5184
|
+
return { text: renderUnknownHelpTopic(topic) };
|
|
5185
|
+
}
|
|
5186
|
+
return { text: renderHelpTopic(entry) };
|
|
5187
|
+
}
|
|
5188
|
+
function renderHelpIndex() {
|
|
5189
|
+
const topics = listHelpTopics();
|
|
5190
|
+
return [
|
|
5191
|
+
"常用入口:",
|
|
5192
|
+
"- /ss <agent> (-d <path> | --ws <name>) - 快速新建或切到会话",
|
|
5193
|
+
"- /use <alias> - 切换当前会话",
|
|
5194
|
+
"- /status - 查看当前会话状态",
|
|
5195
|
+
"",
|
|
5196
|
+
"顶级命令:",
|
|
5197
|
+
...topics.map((topic) => `- ${topic.topic} - ${topic.summary}`),
|
|
5198
|
+
"",
|
|
5199
|
+
"查看专题说明:",
|
|
5200
|
+
"- /help <topic>",
|
|
5201
|
+
"- 例如:/help ss、/help ws、/help pm"
|
|
5202
|
+
].join(`
|
|
5203
|
+
`);
|
|
5204
|
+
}
|
|
5205
|
+
function renderHelpTopic(topic) {
|
|
5206
|
+
return [
|
|
5207
|
+
`帮助主题:${topic.topic}`,
|
|
5208
|
+
`说明:${topic.summary}`,
|
|
5209
|
+
...topic.aliases.length > 0 ? [`别名:${topic.aliases.join("、")}`] : [],
|
|
5210
|
+
"",
|
|
5211
|
+
"命令:",
|
|
5212
|
+
...topic.commands.map((command) => `- ${command.usage} - ${command.description}`),
|
|
5213
|
+
...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : []
|
|
5214
|
+
].join(`
|
|
5215
|
+
`);
|
|
5216
|
+
}
|
|
5217
|
+
function renderUnknownHelpTopic(topic) {
|
|
5218
|
+
return [
|
|
5219
|
+
`未知帮助主题:${topic}`,
|
|
5220
|
+
"",
|
|
5221
|
+
"可用主题:",
|
|
5222
|
+
...listHelpTopics().map((entry) => `- ${entry.topic}`)
|
|
5223
|
+
].join(`
|
|
5224
|
+
`);
|
|
5225
|
+
}
|
|
5226
|
+
var init_help_handler = __esm(() => {
|
|
5227
|
+
init_help_registry();
|
|
5228
|
+
});
|
|
4220
5229
|
|
|
4221
5230
|
// src/commands/handlers/session-shortcut-handler.ts
|
|
4222
5231
|
import { access as access2 } from "node:fs/promises";
|
|
4223
5232
|
import { basename as basename2, normalize as normalize2 } from "node:path";
|
|
4224
|
-
import { homedir as
|
|
4225
|
-
async function handleSessionShortcutCommand(context, ops, chatKey, agent,
|
|
5233
|
+
import { homedir as homedir4 } from "node:os";
|
|
5234
|
+
async function handleSessionShortcutCommand(context, ops, chatKey, agent, target, createNew) {
|
|
4226
5235
|
if (!context.config || !context.configStore) {
|
|
4227
5236
|
return { text: "当前没有加载可写入的配置。" };
|
|
4228
5237
|
}
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
5238
|
+
if (!context.config.agents[agent]) {
|
|
5239
|
+
return { text: `agent "${agent}" is not registered` };
|
|
5240
|
+
}
|
|
5241
|
+
const workspace = await resolveShortcutWorkspace(context, target);
|
|
5242
|
+
if ("error" in workspace) {
|
|
5243
|
+
return { text: workspace.error };
|
|
4232
5244
|
}
|
|
4233
|
-
const workspace = await resolveShortcutWorkspace(context, cwd);
|
|
4234
5245
|
await context.logger.info("session.shortcut.workspace", "resolved shortcut workspace", {
|
|
4235
5246
|
workspace: workspace.name,
|
|
4236
5247
|
cwd: workspace.cwd,
|
|
@@ -4254,7 +5265,7 @@ async function handleSessionShortcutCommand(context, ops, chatKey, agent, cwdInp
|
|
|
4254
5265
|
`)
|
|
4255
5266
|
};
|
|
4256
5267
|
}
|
|
4257
|
-
const session = ops.resolveSession(alias, agent, workspace.name,
|
|
5268
|
+
const session = ops.resolveSession(alias, agent, workspace.name, alias);
|
|
4258
5269
|
try {
|
|
4259
5270
|
await ops.ensureTransportSession(session);
|
|
4260
5271
|
const exists = await ops.checkTransportSession(session);
|
|
@@ -4282,7 +5293,23 @@ async function handleSessionShortcutCommand(context, ops, chatKey, agent, cwdInp
|
|
|
4282
5293
|
`)
|
|
4283
5294
|
};
|
|
4284
5295
|
}
|
|
4285
|
-
async function resolveShortcutWorkspace(context,
|
|
5296
|
+
async function resolveShortcutWorkspace(context, target) {
|
|
5297
|
+
if (target.workspace) {
|
|
5298
|
+
const workspace = context.config?.workspaces[target.workspace];
|
|
5299
|
+
if (!workspace) {
|
|
5300
|
+
return { error: `workspace "${target.workspace}" is not registered` };
|
|
5301
|
+
}
|
|
5302
|
+
return {
|
|
5303
|
+
name: target.workspace,
|
|
5304
|
+
cwd: workspace.cwd,
|
|
5305
|
+
reused: true
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
const cwdInput = target.cwd ?? "";
|
|
5309
|
+
const cwd = normalizePathForWorkspace2(cwdInput);
|
|
5310
|
+
if (!await pathExists2(cwd)) {
|
|
5311
|
+
return { error: `工作区路径不存在:${cwdInput}` };
|
|
5312
|
+
}
|
|
4286
5313
|
const existingByPath = Object.entries(context.config?.workspaces ?? {}).find(([, workspace]) => sameWorkspacePath(workspace.cwd, cwd));
|
|
4287
5314
|
if (existingByPath) {
|
|
4288
5315
|
return {
|
|
@@ -4343,7 +5370,7 @@ async function pathExists2(path11) {
|
|
|
4343
5370
|
}
|
|
4344
5371
|
}
|
|
4345
5372
|
function normalizePathForWorkspace2(path11) {
|
|
4346
|
-
const expanded = path11.startsWith("~") ?
|
|
5373
|
+
const expanded = path11.startsWith("~") ? homedir4() + path11.slice(1) : path11;
|
|
4347
5374
|
return normalize2(expanded);
|
|
4348
5375
|
}
|
|
4349
5376
|
function sameWorkspacePath(left, right) {
|
|
@@ -4384,15 +5411,19 @@ function renderTransportError(session, error) {
|
|
|
4384
5411
|
function renderSessionCreationError(session, error) {
|
|
4385
5412
|
const message = error instanceof Error ? error.message : String(error);
|
|
4386
5413
|
if (message.includes("timed out") && message.includes("sessions new")) {
|
|
4387
|
-
return
|
|
5414
|
+
return renderSessionCreationFailure(session, message);
|
|
4388
5415
|
}
|
|
4389
5416
|
throw error;
|
|
4390
5417
|
}
|
|
4391
5418
|
function renderSessionCreationVerificationError(session) {
|
|
5419
|
+
return renderSessionCreationFailure(session, "未检测到可用的后端会话。");
|
|
5420
|
+
}
|
|
5421
|
+
function renderSessionCreationFailure(session, detail) {
|
|
4392
5422
|
return {
|
|
4393
5423
|
text: [
|
|
4394
|
-
"
|
|
4395
|
-
|
|
5424
|
+
"会话创建失败。",
|
|
5425
|
+
`错误信息:${summarizeTransportError(detail)}`,
|
|
5426
|
+
`如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
|
|
4396
5427
|
].join(`
|
|
4397
5428
|
`)
|
|
4398
5429
|
};
|
|
@@ -4492,7 +5523,7 @@ class CommandRouter {
|
|
|
4492
5523
|
`)
|
|
4493
5524
|
};
|
|
4494
5525
|
case "help":
|
|
4495
|
-
return handleHelp();
|
|
5526
|
+
return handleHelp(command.topic);
|
|
4496
5527
|
case "agents":
|
|
4497
5528
|
return handleAgents(this.createHandlerContext());
|
|
4498
5529
|
case "agent.add":
|
|
@@ -4507,6 +5538,10 @@ class CommandRouter {
|
|
|
4507
5538
|
return handlePermissionAutoStatus(this.createHandlerContext(), "当前非交互策略:");
|
|
4508
5539
|
case "permission.auto.set":
|
|
4509
5540
|
return await handlePermissionAutoSet(this.createHandlerContext(), command.policy);
|
|
5541
|
+
case "config.show":
|
|
5542
|
+
return handleConfigShow(this.createHandlerContext());
|
|
5543
|
+
case "config.set":
|
|
5544
|
+
return await handleConfigSet(this.createHandlerContext(), command.path, command.value);
|
|
4510
5545
|
case "workspaces":
|
|
4511
5546
|
return handleWorkspaces(this.createHandlerContext());
|
|
4512
5547
|
case "workspace.new":
|
|
@@ -4518,9 +5553,9 @@ class CommandRouter {
|
|
|
4518
5553
|
case "session.new":
|
|
4519
5554
|
return await handleSessionNew(this.createSessionHandlerContext(), chatKey, command.alias, command.agent, command.workspace);
|
|
4520
5555
|
case "session.shortcut":
|
|
4521
|
-
return await handleSessionShortcut(this.createSessionHandlerContext(), chatKey, command.agent, command
|
|
5556
|
+
return await handleSessionShortcut(this.createSessionHandlerContext(), chatKey, command.agent, command, false);
|
|
4522
5557
|
case "session.shortcut.new":
|
|
4523
|
-
return await handleSessionShortcut(this.createSessionHandlerContext(), chatKey, command.agent, command
|
|
5558
|
+
return await handleSessionShortcut(this.createSessionHandlerContext(), chatKey, command.agent, command, true);
|
|
4524
5559
|
case "session.attach":
|
|
4525
5560
|
return await handleSessionAttach(this.createSessionHandlerContext(), chatKey, command.alias, command.agent, command.workspace, command.transportSession);
|
|
4526
5561
|
case "session.use":
|
|
@@ -4529,6 +5564,12 @@ class CommandRouter {
|
|
|
4529
5564
|
return await handleModeShow(this.createSessionHandlerContext(), chatKey);
|
|
4530
5565
|
case "mode.set":
|
|
4531
5566
|
return await handleModeSet(this.createSessionHandlerContext(), chatKey, command.modeId);
|
|
5567
|
+
case "replymode.show":
|
|
5568
|
+
return await handleReplyModeShow(this.createSessionHandlerContext(), chatKey);
|
|
5569
|
+
case "replymode.set":
|
|
5570
|
+
return await handleReplyModeSet(this.createSessionHandlerContext(), chatKey, command.replyMode);
|
|
5571
|
+
case "replymode.reset":
|
|
5572
|
+
return await handleReplyModeReset(this.createSessionHandlerContext(), chatKey);
|
|
4532
5573
|
case "status":
|
|
4533
5574
|
return await handleStatus(this.createSessionHandlerContext(), chatKey);
|
|
4534
5575
|
case "cancel":
|
|
@@ -4566,7 +5607,7 @@ class CommandRouter {
|
|
|
4566
5607
|
resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
|
|
4567
5608
|
ensureTransportSession: (session) => this.ensureTransportSession(session),
|
|
4568
5609
|
checkTransportSession: (session) => this.checkTransportSession(session),
|
|
4569
|
-
handleSessionShortcut: (chatKey, agent,
|
|
5610
|
+
handleSessionShortcut: (chatKey, agent, target, createNew) => handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(), chatKey, agent, target, createNew),
|
|
4570
5611
|
resetCurrentSession: (chatKey) => handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(), chatKey),
|
|
4571
5612
|
refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
|
|
4572
5613
|
};
|
|
@@ -4615,6 +5656,8 @@ class CommandRouter {
|
|
|
4615
5656
|
return;
|
|
4616
5657
|
}
|
|
4617
5658
|
this.config.transport = { ...updated.transport };
|
|
5659
|
+
this.config.logging = { ...updated.logging };
|
|
5660
|
+
this.config.wechat = { ...updated.wechat };
|
|
4618
5661
|
this.config.agents = { ...updated.agents };
|
|
4619
5662
|
this.config.workspaces = { ...updated.workspaces };
|
|
4620
5663
|
}
|
|
@@ -4705,6 +5748,9 @@ var init_command_router = __esm(() => {
|
|
|
4705
5748
|
init_acpx_session_index();
|
|
4706
5749
|
init_prompt_output();
|
|
4707
5750
|
init_parse_command();
|
|
5751
|
+
init_permission_handler();
|
|
5752
|
+
init_config_handler();
|
|
5753
|
+
init_session_handler();
|
|
4708
5754
|
init_help_handler();
|
|
4709
5755
|
init_agent_handler();
|
|
4710
5756
|
init_workspace_handler();
|
|
@@ -4729,12 +5775,12 @@ function isLegacyCodexCommand(command) {
|
|
|
4729
5775
|
}
|
|
4730
5776
|
|
|
4731
5777
|
// src/config/load-config.ts
|
|
4732
|
-
import { readFile as
|
|
5778
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
4733
5779
|
function isRecord(value) {
|
|
4734
5780
|
return typeof value === "object" && value !== null;
|
|
4735
5781
|
}
|
|
4736
5782
|
async function loadConfig(path11, options = {}) {
|
|
4737
|
-
const raw = JSON.parse(await
|
|
5783
|
+
const raw = JSON.parse(await readFile5(path11, "utf8"));
|
|
4738
5784
|
return parseConfig(raw, options);
|
|
4739
5785
|
}
|
|
4740
5786
|
function parseConfig(raw, options = {}) {
|
|
@@ -4754,8 +5800,8 @@ function parseConfig(raw, options = {}) {
|
|
|
4754
5800
|
if ("permissionMode" in transport && transport.permissionMode !== "approve-all" && transport.permissionMode !== "approve-reads" && transport.permissionMode !== "deny-all") {
|
|
4755
5801
|
throw new Error("transport.permissionMode must be approve-all, approve-reads, or deny-all");
|
|
4756
5802
|
}
|
|
4757
|
-
if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "
|
|
4758
|
-
throw new Error("transport.nonInteractivePermissions must be
|
|
5803
|
+
if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "deny" && transport.nonInteractivePermissions !== "fail") {
|
|
5804
|
+
throw new Error("transport.nonInteractivePermissions must be deny or fail");
|
|
4759
5805
|
}
|
|
4760
5806
|
if (!isRecord(raw.agents)) {
|
|
4761
5807
|
throw new Error("agents must be an object");
|
|
@@ -4764,9 +5810,13 @@ function parseConfig(raw, options = {}) {
|
|
|
4764
5810
|
throw new Error("workspaces must be an object");
|
|
4765
5811
|
}
|
|
4766
5812
|
const logging = raw.logging;
|
|
5813
|
+
const wechat = raw.wechat;
|
|
4767
5814
|
if (logging !== undefined && !isRecord(logging)) {
|
|
4768
5815
|
throw new Error("logging must be an object");
|
|
4769
5816
|
}
|
|
5817
|
+
if (wechat !== undefined && !isRecord(wechat)) {
|
|
5818
|
+
throw new Error("wechat must be an object");
|
|
5819
|
+
}
|
|
4770
5820
|
if (isRecord(logging) && "level" in logging && logging.level !== "error" && logging.level !== "info" && logging.level !== "debug") {
|
|
4771
5821
|
throw new Error("logging.level must be error, info, or debug");
|
|
4772
5822
|
}
|
|
@@ -4775,6 +5825,9 @@ function parseConfig(raw, options = {}) {
|
|
|
4775
5825
|
throw new Error(`logging.${field} must be a positive number`);
|
|
4776
5826
|
}
|
|
4777
5827
|
}
|
|
5828
|
+
if (isRecord(wechat) && "replyMode" in wechat && wechat.replyMode !== "stream" && wechat.replyMode !== "final") {
|
|
5829
|
+
throw new Error("wechat.replyMode must be stream or final");
|
|
5830
|
+
}
|
|
4778
5831
|
for (const [name, agent] of Object.entries(raw.agents)) {
|
|
4779
5832
|
if (!isRecord(agent) || typeof agent.driver !== "string" || agent.driver.length === 0) {
|
|
4780
5833
|
throw new Error(`agent "${name}" must define a non-empty driver`);
|
|
@@ -4811,9 +5864,10 @@ function parseConfig(raw, options = {}) {
|
|
|
4811
5864
|
}
|
|
4812
5865
|
const transportType = transport.type === "acpx-cli" || transport.type === "acpx-bridge" ? transport.type : "acpx-bridge";
|
|
4813
5866
|
const permissionMode = transport.permissionMode === "approve-all" || transport.permissionMode === "approve-reads" || transport.permissionMode === "deny-all" ? transport.permissionMode : DEFAULT_PERMISSION_MODE;
|
|
4814
|
-
const nonInteractivePermissions = transport.nonInteractivePermissions === "
|
|
5867
|
+
const nonInteractivePermissions = transport.nonInteractivePermissions === "deny" || transport.nonInteractivePermissions === "fail" ? transport.nonInteractivePermissions : DEFAULT_NON_INTERACTIVE_PERMISSIONS;
|
|
4815
5868
|
const loggingLevel = logging?.level;
|
|
4816
5869
|
const resolvedLoggingLevel = loggingLevel === "error" || loggingLevel === "info" || loggingLevel === "debug" ? loggingLevel : options.defaultLoggingLevel ?? DEFAULT_LOGGING_CONFIG.level;
|
|
5870
|
+
const replyMode = wechat?.replyMode === "stream" || wechat?.replyMode === "final" ? wechat.replyMode : DEFAULT_WECHAT_REPLY_MODE;
|
|
4817
5871
|
return {
|
|
4818
5872
|
transport: {
|
|
4819
5873
|
...typeof transport.command === "string" ? { command: transport.command } : {},
|
|
@@ -4828,11 +5882,14 @@ function parseConfig(raw, options = {}) {
|
|
|
4828
5882
|
maxFiles: typeof logging?.maxFiles === "number" ? logging.maxFiles : DEFAULT_LOGGING_CONFIG.maxFiles,
|
|
4829
5883
|
retentionDays: typeof logging?.retentionDays === "number" ? logging.retentionDays : DEFAULT_LOGGING_CONFIG.retentionDays
|
|
4830
5884
|
},
|
|
5885
|
+
wechat: {
|
|
5886
|
+
replyMode
|
|
5887
|
+
},
|
|
4831
5888
|
agents,
|
|
4832
5889
|
workspaces
|
|
4833
5890
|
};
|
|
4834
5891
|
}
|
|
4835
|
-
var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "
|
|
5892
|
+
var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_WECHAT_REPLY_MODE = "stream";
|
|
4836
5893
|
var init_load_config = __esm(() => {
|
|
4837
5894
|
DEFAULT_LOGGING_CONFIG = {
|
|
4838
5895
|
level: "info",
|
|
@@ -4843,8 +5900,8 @@ var init_load_config = __esm(() => {
|
|
|
4843
5900
|
});
|
|
4844
5901
|
|
|
4845
5902
|
// src/config/config-store.ts
|
|
4846
|
-
import { mkdir as
|
|
4847
|
-
import { dirname as
|
|
5903
|
+
import { mkdir as mkdir7, writeFile as writeFile5 } from "node:fs/promises";
|
|
5904
|
+
import { dirname as dirname6 } from "node:path";
|
|
4848
5905
|
|
|
4849
5906
|
class ConfigStore {
|
|
4850
5907
|
path;
|
|
@@ -4855,8 +5912,8 @@ class ConfigStore {
|
|
|
4855
5912
|
return await loadConfig(this.path);
|
|
4856
5913
|
}
|
|
4857
5914
|
async save(config) {
|
|
4858
|
-
await
|
|
4859
|
-
await
|
|
5915
|
+
await mkdir7(dirname6(this.path), { recursive: true });
|
|
5916
|
+
await writeFile5(this.path, `${JSON.stringify(config, null, 2)}
|
|
4860
5917
|
`, "utf8");
|
|
4861
5918
|
}
|
|
4862
5919
|
async upsertWorkspace(name, cwd, description) {
|
|
@@ -4896,13 +5953,22 @@ class ConfigStore {
|
|
|
4896
5953
|
await this.save(config);
|
|
4897
5954
|
return config;
|
|
4898
5955
|
}
|
|
5956
|
+
async updateWechat(wechat) {
|
|
5957
|
+
const config = await this.load();
|
|
5958
|
+
config.wechat = {
|
|
5959
|
+
...config.wechat,
|
|
5960
|
+
...wechat
|
|
5961
|
+
};
|
|
5962
|
+
await this.save(config);
|
|
5963
|
+
return config;
|
|
5964
|
+
}
|
|
4899
5965
|
}
|
|
4900
5966
|
var init_config_store = __esm(() => {
|
|
4901
5967
|
init_load_config();
|
|
4902
5968
|
});
|
|
4903
5969
|
|
|
4904
5970
|
// src/config/ensure-config.ts
|
|
4905
|
-
import { readFile as
|
|
5971
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
4906
5972
|
async function ensureConfigExists(path11) {
|
|
4907
5973
|
try {
|
|
4908
5974
|
await loadConfig(path11);
|
|
@@ -4916,7 +5982,10 @@ async function ensureConfigExists(path11) {
|
|
|
4916
5982
|
}
|
|
4917
5983
|
async function loadDefaultConfigTemplate() {
|
|
4918
5984
|
const templatePath = new URL("../../config.example.json", import.meta.url);
|
|
4919
|
-
|
|
5985
|
+
return normalizeDefaultConfigTemplate(JSON.parse(await readFile6(templatePath, "utf8")));
|
|
5986
|
+
}
|
|
5987
|
+
function normalizeDefaultConfigTemplate(raw) {
|
|
5988
|
+
const template = parseConfig(raw);
|
|
4920
5989
|
return {
|
|
4921
5990
|
...template,
|
|
4922
5991
|
agents: Object.fromEntries(Object.entries(template.agents).map(([name, agent]) => [
|
|
@@ -4939,11 +6008,18 @@ var init_ensure_config = __esm(() => {
|
|
|
4939
6008
|
|
|
4940
6009
|
// src/config/resolve-acpx-command.ts
|
|
4941
6010
|
import { readFileSync } from "node:fs";
|
|
4942
|
-
import { posix, win32 } from "node:path";
|
|
4943
6011
|
import { createRequire as createRequire2 } from "node:module";
|
|
6012
|
+
import { posix, win32 } from "node:path";
|
|
4944
6013
|
function resolveAcpxCommand(options = {}) {
|
|
6014
|
+
return resolveAcpxCommandMetadata(options).command;
|
|
6015
|
+
}
|
|
6016
|
+
function resolveAcpxCommandMetadata(options = {}) {
|
|
4945
6017
|
if (options.configuredCommand) {
|
|
4946
|
-
return
|
|
6018
|
+
return {
|
|
6019
|
+
command: options.configuredCommand,
|
|
6020
|
+
source: "config",
|
|
6021
|
+
explanation: "transport.command is set, so the configured command wins."
|
|
6022
|
+
};
|
|
4947
6023
|
}
|
|
4948
6024
|
const platform = options.platform ?? process.platform;
|
|
4949
6025
|
const resolvePackageJson = options.resolvePackageJson ?? ((id) => require2.resolve(id));
|
|
@@ -4955,10 +6031,18 @@ function resolveAcpxCommand(options = {}) {
|
|
|
4955
6031
|
const packageDir = pathApi.dirname(packageJsonPath);
|
|
4956
6032
|
const binPath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && typeof pkg.bin.acpx === "string" ? pkg.bin.acpx : null;
|
|
4957
6033
|
if (binPath) {
|
|
4958
|
-
return
|
|
6034
|
+
return {
|
|
6035
|
+
command: pathApi.resolve(packageDir, binPath),
|
|
6036
|
+
source: "bundled",
|
|
6037
|
+
explanation: "transport.command is unset, so the bundled acpx dependency is used."
|
|
6038
|
+
};
|
|
4959
6039
|
}
|
|
4960
6040
|
} catch {}
|
|
4961
|
-
return
|
|
6041
|
+
return {
|
|
6042
|
+
command: "acpx",
|
|
6043
|
+
source: "PATH",
|
|
6044
|
+
explanation: "transport.command is unset and no bundled acpx was found, so PATH is the fallback."
|
|
6045
|
+
};
|
|
4962
6046
|
}
|
|
4963
6047
|
var require2;
|
|
4964
6048
|
var init_resolve_acpx_command = __esm(() => {
|
|
@@ -5061,6 +6145,23 @@ class SessionService {
|
|
|
5061
6145
|
session.last_used_at = new Date().toISOString();
|
|
5062
6146
|
await this.persist();
|
|
5063
6147
|
}
|
|
6148
|
+
async setCurrentSessionReplyMode(chatKey, replyMode) {
|
|
6149
|
+
const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
|
|
6150
|
+
if (!currentAlias) {
|
|
6151
|
+
throw new Error("no current session selected");
|
|
6152
|
+
}
|
|
6153
|
+
const session = this.state.sessions[currentAlias];
|
|
6154
|
+
if (!session) {
|
|
6155
|
+
throw new Error("no current session selected");
|
|
6156
|
+
}
|
|
6157
|
+
if (replyMode) {
|
|
6158
|
+
session.reply_mode = replyMode;
|
|
6159
|
+
} else {
|
|
6160
|
+
delete session.reply_mode;
|
|
6161
|
+
}
|
|
6162
|
+
session.last_used_at = new Date().toISOString();
|
|
6163
|
+
await this.persist();
|
|
6164
|
+
}
|
|
5064
6165
|
async getCurrentSession(chatKey) {
|
|
5065
6166
|
const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
|
|
5066
6167
|
if (!currentAlias) {
|
|
@@ -5085,6 +6186,13 @@ class SessionService {
|
|
|
5085
6186
|
}
|
|
5086
6187
|
toResolvedSession(session) {
|
|
5087
6188
|
const agentConfig = this.config.agents[session.agent];
|
|
6189
|
+
if (!agentConfig) {
|
|
6190
|
+
throw new Error(`session "${session.alias}" references agent "${session.agent}", but that agent is no longer registered`);
|
|
6191
|
+
}
|
|
6192
|
+
const workspaceConfig = this.config.workspaces[session.workspace];
|
|
6193
|
+
if (!workspaceConfig) {
|
|
6194
|
+
throw new Error(`session "${session.alias}" references workspace "${session.workspace}", but that workspace is no longer registered`);
|
|
6195
|
+
}
|
|
5088
6196
|
return {
|
|
5089
6197
|
alias: session.alias,
|
|
5090
6198
|
agent: session.agent,
|
|
@@ -5092,7 +6200,8 @@ class SessionService {
|
|
|
5092
6200
|
workspace: session.workspace,
|
|
5093
6201
|
transportSession: session.transport_session,
|
|
5094
6202
|
modeId: session.mode_id,
|
|
5095
|
-
|
|
6203
|
+
replyMode: session.reply_mode,
|
|
6204
|
+
cwd: workspaceConfig.cwd
|
|
5096
6205
|
};
|
|
5097
6206
|
}
|
|
5098
6207
|
async setSessionTransportAgentCommand(alias, transportAgentCommand) {
|
|
@@ -5124,6 +6233,7 @@ class SessionService {
|
|
|
5124
6233
|
transport_session: transportSession,
|
|
5125
6234
|
...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : existingSession?.transport_agent_command ? { transport_agent_command: existingSession.transport_agent_command } : {},
|
|
5126
6235
|
mode_id: existingSession?.mode_id,
|
|
6236
|
+
reply_mode: existingSession?.reply_mode,
|
|
5127
6237
|
created_at: existingSession?.created_at ?? now,
|
|
5128
6238
|
last_used_at: now
|
|
5129
6239
|
};
|
|
@@ -5132,6 +6242,15 @@ class SessionService {
|
|
|
5132
6242
|
return this.toResolvedSession(session);
|
|
5133
6243
|
}
|
|
5134
6244
|
validateSession(alias, agent, workspace) {
|
|
6245
|
+
if (alias.trim().length === 0) {
|
|
6246
|
+
throw new Error("session alias must be a non-empty string");
|
|
6247
|
+
}
|
|
6248
|
+
if (agent.trim().length === 0) {
|
|
6249
|
+
throw new Error("agent must be a non-empty string");
|
|
6250
|
+
}
|
|
6251
|
+
if (workspace.trim().length === 0) {
|
|
6252
|
+
throw new Error("workspace must be a non-empty string");
|
|
6253
|
+
}
|
|
5135
6254
|
if (!this.config.workspaces[workspace]) {
|
|
5136
6255
|
throw new Error(`workspace "${workspace}" is not registered`);
|
|
5137
6256
|
}
|
|
@@ -5151,8 +6270,28 @@ function createEmptyState() {
|
|
|
5151
6270
|
}
|
|
5152
6271
|
|
|
5153
6272
|
// src/state/state-store.ts
|
|
5154
|
-
import { mkdir as
|
|
5155
|
-
import { dirname as
|
|
6273
|
+
import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
|
|
6274
|
+
import { dirname as dirname7 } from "node:path";
|
|
6275
|
+
function isRecord2(value) {
|
|
6276
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6277
|
+
}
|
|
6278
|
+
function parseState(raw, path11) {
|
|
6279
|
+
if (!isRecord2(raw)) {
|
|
6280
|
+
throw new Error(`state file "${path11}" must contain a JSON object`);
|
|
6281
|
+
}
|
|
6282
|
+
const sessions = raw.sessions;
|
|
6283
|
+
if (!isRecord2(sessions)) {
|
|
6284
|
+
throw new Error(`state file "${path11}" must contain an object field "sessions"`);
|
|
6285
|
+
}
|
|
6286
|
+
const chatContexts = raw.chat_contexts;
|
|
6287
|
+
if (!isRecord2(chatContexts)) {
|
|
6288
|
+
throw new Error(`state file "${path11}" must contain an object field "chat_contexts"`);
|
|
6289
|
+
}
|
|
6290
|
+
return {
|
|
6291
|
+
sessions,
|
|
6292
|
+
chat_contexts: chatContexts
|
|
6293
|
+
};
|
|
6294
|
+
}
|
|
5156
6295
|
|
|
5157
6296
|
class StateStore {
|
|
5158
6297
|
path;
|
|
@@ -5161,11 +6300,19 @@ class StateStore {
|
|
|
5161
6300
|
}
|
|
5162
6301
|
async load() {
|
|
5163
6302
|
try {
|
|
5164
|
-
const content = await
|
|
6303
|
+
const content = await readFile7(this.path, "utf8");
|
|
5165
6304
|
if (content.trim() === "") {
|
|
5166
6305
|
return createEmptyState();
|
|
5167
6306
|
}
|
|
5168
|
-
|
|
6307
|
+
let parsed;
|
|
6308
|
+
try {
|
|
6309
|
+
parsed = JSON.parse(content);
|
|
6310
|
+
} catch (error) {
|
|
6311
|
+
throw new Error(`failed to parse state file "${this.path}"`, {
|
|
6312
|
+
cause: error
|
|
6313
|
+
});
|
|
6314
|
+
}
|
|
6315
|
+
return parseState(parsed, this.path);
|
|
5169
6316
|
} catch (error) {
|
|
5170
6317
|
if (error.code === "ENOENT") {
|
|
5171
6318
|
return createEmptyState();
|
|
@@ -5174,8 +6321,8 @@ class StateStore {
|
|
|
5174
6321
|
}
|
|
5175
6322
|
}
|
|
5176
6323
|
async save(state) {
|
|
5177
|
-
await
|
|
5178
|
-
await
|
|
6324
|
+
await mkdir8(dirname7(this.path), { recursive: true });
|
|
6325
|
+
await writeFile6(this.path, JSON.stringify(state, null, 2));
|
|
5179
6326
|
}
|
|
5180
6327
|
}
|
|
5181
6328
|
var init_state_store = () => {};
|
|
@@ -5187,12 +6334,17 @@ __export(exports_run_console, {
|
|
|
5187
6334
|
});
|
|
5188
6335
|
async function runConsole(paths, deps) {
|
|
5189
6336
|
const runtime = await deps.buildApp(paths);
|
|
6337
|
+
const consumerLock = deps.consumerLock ?? deps.consumerLockFactory?.(runtime);
|
|
5190
6338
|
const sdk = await deps.loadWeixinSdk();
|
|
5191
6339
|
const setIntervalFn = deps.setInterval ?? ((fn, delay) => setInterval(fn, delay));
|
|
5192
6340
|
const clearIntervalFn = deps.clearInterval ?? ((timer) => clearInterval(timer));
|
|
5193
6341
|
const addProcessListener = deps.addProcessListener ?? ((signal, handler) => process.on(signal, handler));
|
|
5194
6342
|
const removeProcessListener = deps.removeProcessListener ?? ((signal, handler) => process.off(signal, handler));
|
|
6343
|
+
const processPid = deps.processPid ?? process.pid;
|
|
6344
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
6345
|
+
const hostname = deps.hostname ?? (() => "");
|
|
5195
6346
|
let heartbeatTimer = null;
|
|
6347
|
+
let consumerLockAcquired = false;
|
|
5196
6348
|
const shutdownController = new AbortController;
|
|
5197
6349
|
const signalHandler = () => {
|
|
5198
6350
|
shutdownController.abort();
|
|
@@ -5209,6 +6361,53 @@ async function runConsole(paths, deps) {
|
|
|
5209
6361
|
deps.daemonRuntime?.heartbeat().catch(() => {});
|
|
5210
6362
|
}, deps.heartbeatIntervalMs ?? 30000);
|
|
5211
6363
|
}
|
|
6364
|
+
if (consumerLock) {
|
|
6365
|
+
const lockMeta = {
|
|
6366
|
+
pid: processPid,
|
|
6367
|
+
mode: deps.daemonRuntime ? "daemon" : "foreground",
|
|
6368
|
+
startedAt: now(),
|
|
6369
|
+
configPath: paths.configPath,
|
|
6370
|
+
statePath: paths.statePath,
|
|
6371
|
+
hostname: hostname() || undefined
|
|
6372
|
+
};
|
|
6373
|
+
await runtime.logger.info("weixin.consumer_lock.acquire_attempt", "attempting to acquire weixin consumer lock", {
|
|
6374
|
+
pid: lockMeta.pid,
|
|
6375
|
+
mode: lockMeta.mode,
|
|
6376
|
+
configPath: lockMeta.configPath,
|
|
6377
|
+
statePath: lockMeta.statePath,
|
|
6378
|
+
hostname: lockMeta.hostname
|
|
6379
|
+
});
|
|
6380
|
+
try {
|
|
6381
|
+
await consumerLock.acquire(lockMeta);
|
|
6382
|
+
consumerLockAcquired = true;
|
|
6383
|
+
await runtime.logger.info("weixin.consumer_lock.acquired", "acquired weixin consumer lock", {
|
|
6384
|
+
pid: lockMeta.pid,
|
|
6385
|
+
mode: lockMeta.mode,
|
|
6386
|
+
configPath: lockMeta.configPath,
|
|
6387
|
+
statePath: lockMeta.statePath
|
|
6388
|
+
});
|
|
6389
|
+
} catch (error) {
|
|
6390
|
+
if (error instanceof ActiveWeixinConsumerLockError) {
|
|
6391
|
+
await runtime.logger.error("weixin.consumer_lock.acquire_failed", "weixin consumer lock is already held by another process", {
|
|
6392
|
+
conflictType: "active_lock_holder",
|
|
6393
|
+
activePid: error.existing.pid,
|
|
6394
|
+
activeMode: error.existing.mode,
|
|
6395
|
+
activeConfigPath: error.existing.configPath,
|
|
6396
|
+
activeStatePath: error.existing.statePath,
|
|
6397
|
+
requestedPid: lockMeta.pid,
|
|
6398
|
+
requestedMode: lockMeta.mode
|
|
6399
|
+
});
|
|
6400
|
+
} else {
|
|
6401
|
+
await runtime.logger.error("weixin.consumer_lock.acquire_failed", "failed to acquire weixin consumer lock", {
|
|
6402
|
+
conflictType: deps.daemonRuntime ? "daemon_startup_lock_failure" : "foreground_startup_lock_failure",
|
|
6403
|
+
requestedPid: lockMeta.pid,
|
|
6404
|
+
requestedMode: lockMeta.mode,
|
|
6405
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6406
|
+
});
|
|
6407
|
+
}
|
|
6408
|
+
throw error;
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
5212
6411
|
if (!sdk.isLoggedIn()) {
|
|
5213
6412
|
console.log("[weacpx] 未检测到登录凭证,正在启动扫码登录...");
|
|
5214
6413
|
await sdk.login();
|
|
@@ -5229,17 +6428,30 @@ async function runConsole(paths, deps) {
|
|
|
5229
6428
|
if (deps.daemonRuntime) {
|
|
5230
6429
|
await deps.daemonRuntime.stop();
|
|
5231
6430
|
}
|
|
6431
|
+
if (consumerLockAcquired) {
|
|
6432
|
+
await consumerLock?.release();
|
|
6433
|
+
await runtime.logger.info("weixin.consumer_lock.released", "released weixin consumer lock", {
|
|
6434
|
+
pid: processPid
|
|
6435
|
+
});
|
|
6436
|
+
}
|
|
5232
6437
|
if (disposeError) {
|
|
5233
6438
|
throw disposeError;
|
|
5234
6439
|
}
|
|
5235
6440
|
}
|
|
5236
6441
|
}
|
|
6442
|
+
var init_run_console = __esm(() => {
|
|
6443
|
+
init_consumer_lock();
|
|
6444
|
+
});
|
|
5237
6445
|
|
|
5238
6446
|
// src/transport/acpx-bridge/acpx-bridge-protocol.ts
|
|
5239
6447
|
function encodeBridgeRequest(request) {
|
|
5240
6448
|
return `${JSON.stringify(request)}
|
|
5241
6449
|
`;
|
|
5242
6450
|
}
|
|
6451
|
+
function encodeBridgePromptSegmentEvent(event) {
|
|
6452
|
+
return `${JSON.stringify(event)}
|
|
6453
|
+
`;
|
|
6454
|
+
}
|
|
5243
6455
|
|
|
5244
6456
|
// src/transport/acpx-bridge/acpx-bridge-client.ts
|
|
5245
6457
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -5250,30 +6462,59 @@ class AcpxBridgeClient {
|
|
|
5250
6462
|
writeLine;
|
|
5251
6463
|
nextId = 1;
|
|
5252
6464
|
pending = new Map;
|
|
6465
|
+
terminalError = null;
|
|
5253
6466
|
constructor(writeLine) {
|
|
5254
6467
|
this.writeLine = writeLine;
|
|
5255
6468
|
}
|
|
5256
|
-
request(method, params) {
|
|
6469
|
+
request(method, params, onEvent) {
|
|
6470
|
+
if (this.terminalError) {
|
|
6471
|
+
return Promise.reject(this.terminalError);
|
|
6472
|
+
}
|
|
5257
6473
|
const id = String(this.nextId);
|
|
5258
6474
|
this.nextId += 1;
|
|
5259
6475
|
return awaitable((resolve2, reject) => {
|
|
5260
6476
|
this.pending.set(id, {
|
|
5261
6477
|
resolve: (value) => resolve2(value),
|
|
5262
|
-
reject
|
|
6478
|
+
reject,
|
|
6479
|
+
onEvent
|
|
5263
6480
|
});
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
6481
|
+
try {
|
|
6482
|
+
const didWrite = this.writeLine(encodeBridgeRequest({
|
|
6483
|
+
id,
|
|
6484
|
+
method,
|
|
6485
|
+
params
|
|
6486
|
+
}));
|
|
6487
|
+
if (didWrite === false) {
|
|
6488
|
+
this.pending.delete(id);
|
|
6489
|
+
reject(new Error("bridge write buffer is full"));
|
|
6490
|
+
}
|
|
6491
|
+
} catch (error) {
|
|
6492
|
+
this.pending.delete(id);
|
|
6493
|
+
reject(error);
|
|
6494
|
+
}
|
|
5269
6495
|
});
|
|
5270
6496
|
}
|
|
5271
6497
|
handleLine(line) {
|
|
5272
|
-
|
|
5273
|
-
|
|
6498
|
+
let message;
|
|
6499
|
+
try {
|
|
6500
|
+
message = JSON.parse(line);
|
|
6501
|
+
} catch {
|
|
6502
|
+
return;
|
|
6503
|
+
}
|
|
6504
|
+
const pending = this.pending.get(message.id);
|
|
5274
6505
|
if (!pending) {
|
|
5275
6506
|
return;
|
|
5276
6507
|
}
|
|
6508
|
+
if ("event" in message) {
|
|
6509
|
+
if (message.event === "prompt.segment") {
|
|
6510
|
+
pending.onEvent?.({
|
|
6511
|
+
type: "prompt.segment",
|
|
6512
|
+
text: message.text
|
|
6513
|
+
});
|
|
6514
|
+
}
|
|
6515
|
+
return;
|
|
6516
|
+
}
|
|
6517
|
+
const response = message;
|
|
5277
6518
|
this.pending.delete(response.id);
|
|
5278
6519
|
if (response.ok) {
|
|
5279
6520
|
pending.resolve(response.result);
|
|
@@ -5290,6 +6531,7 @@ class AcpxBridgeClient {
|
|
|
5290
6531
|
pending.reject(new Error(response.error.message));
|
|
5291
6532
|
}
|
|
5292
6533
|
handleExit(error) {
|
|
6534
|
+
this.terminalError = error;
|
|
5293
6535
|
const pendingRequests = [...this.pending.values()];
|
|
5294
6536
|
this.pending.clear();
|
|
5295
6537
|
for (const pending of pendingRequests) {
|
|
@@ -5321,13 +6563,11 @@ async function spawnAcpxBridgeClient(options = {}) {
|
|
|
5321
6563
|
...process.env,
|
|
5322
6564
|
WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
|
|
5323
6565
|
WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
|
|
5324
|
-
WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "
|
|
6566
|
+
WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny"
|
|
5325
6567
|
},
|
|
5326
6568
|
stdio: ["pipe", "pipe", "inherit"]
|
|
5327
6569
|
});
|
|
5328
|
-
const client = new AcpxBridgeClient((line) =>
|
|
5329
|
-
child.stdin.write(line);
|
|
5330
|
-
});
|
|
6570
|
+
const client = new AcpxBridgeClient((line) => child.stdin.write(line));
|
|
5331
6571
|
const output = createInterface({
|
|
5332
6572
|
input: child.stdout,
|
|
5333
6573
|
crlfDelay: Infinity
|
|
@@ -5374,10 +6614,14 @@ class AcpxBridgeTransport {
|
|
|
5374
6614
|
async ensureSession(session) {
|
|
5375
6615
|
await this.client.request("ensureSession", this.toParams(session));
|
|
5376
6616
|
}
|
|
5377
|
-
async prompt(session, text,
|
|
6617
|
+
async prompt(session, text, reply) {
|
|
5378
6618
|
return await this.client.request("prompt", {
|
|
5379
6619
|
...this.toParams(session),
|
|
5380
6620
|
text
|
|
6621
|
+
}, (event) => {
|
|
6622
|
+
if (event.type === "prompt.segment") {
|
|
6623
|
+
reply?.(event.text);
|
|
6624
|
+
}
|
|
5381
6625
|
});
|
|
5382
6626
|
}
|
|
5383
6627
|
async setMode(session, modeId) {
|
|
@@ -5393,6 +6637,9 @@ class AcpxBridgeTransport {
|
|
|
5393
6637
|
const result = await this.client.request("hasSession", this.toParams(session));
|
|
5394
6638
|
return result.exists;
|
|
5395
6639
|
}
|
|
6640
|
+
async updatePermissionPolicy(policy) {
|
|
6641
|
+
await this.client.request("updatePermissionPolicy", { ...policy });
|
|
6642
|
+
}
|
|
5396
6643
|
async dispose() {
|
|
5397
6644
|
await this.client.dispose?.();
|
|
5398
6645
|
}
|
|
@@ -5481,12 +6728,12 @@ function parseStreamingChunks(state, line) {
|
|
|
5481
6728
|
|
|
5482
6729
|
// src/transport/acpx-cli/node-pty-helper.ts
|
|
5483
6730
|
import { chmod as chmodFs } from "node:fs/promises";
|
|
5484
|
-
import { dirname as
|
|
6731
|
+
import { dirname as dirname8, join as join4 } from "node:path";
|
|
5485
6732
|
function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
|
|
5486
6733
|
if (platform === "win32") {
|
|
5487
6734
|
return null;
|
|
5488
6735
|
}
|
|
5489
|
-
return
|
|
6736
|
+
return join4(dirname8(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
|
|
5490
6737
|
}
|
|
5491
6738
|
async function ensureNodePtyHelperExecutable(helperPath, chmod = chmodFs) {
|
|
5492
6739
|
if (!helperPath) {
|
|
@@ -5574,7 +6821,7 @@ class AcpxCliTransport {
|
|
|
5574
6821
|
this.command = options.command ?? "acpx";
|
|
5575
6822
|
this.sessionInitTimeoutMs = options.sessionInitTimeoutMs ?? 120000;
|
|
5576
6823
|
this.permissionMode = options.permissionMode ?? "approve-all";
|
|
5577
|
-
this.nonInteractivePermissions = options.nonInteractivePermissions ?? "
|
|
6824
|
+
this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
|
|
5578
6825
|
this.runCommand = runCommand;
|
|
5579
6826
|
this.runPtyCommand = runPtyCommand;
|
|
5580
6827
|
}
|
|
@@ -5618,6 +6865,10 @@ class AcpxCliTransport {
|
|
|
5618
6865
|
message: output.trim()
|
|
5619
6866
|
};
|
|
5620
6867
|
}
|
|
6868
|
+
async updatePermissionPolicy(policy) {
|
|
6869
|
+
this.permissionMode = policy.permissionMode;
|
|
6870
|
+
this.nonInteractivePermissions = policy.nonInteractivePermissions;
|
|
6871
|
+
}
|
|
5621
6872
|
async hasSession(session) {
|
|
5622
6873
|
const result = await this.runCommand(this.command, this.buildArgs(session, [
|
|
5623
6874
|
"sessions",
|
|
@@ -5771,11 +7022,12 @@ var init_acpx_cli_transport = __esm(() => {
|
|
|
5771
7022
|
var exports_main = {};
|
|
5772
7023
|
__export(exports_main, {
|
|
5773
7024
|
resolveRuntimePaths: () => resolveRuntimePaths,
|
|
7025
|
+
resolveBridgeEntryPath: () => resolveBridgeEntryPath,
|
|
5774
7026
|
main: () => main2,
|
|
5775
7027
|
buildApp: () => buildApp
|
|
5776
7028
|
});
|
|
5777
|
-
import { homedir as
|
|
5778
|
-
import { dirname as
|
|
7029
|
+
import { homedir as homedir5 } from "node:os";
|
|
7030
|
+
import { dirname as dirname9, join as join5 } from "node:path";
|
|
5779
7031
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5780
7032
|
async function buildApp(paths, deps = {}) {
|
|
5781
7033
|
await ensureConfigExists(paths.configPath);
|
|
@@ -5837,7 +7089,7 @@ async function main2() {
|
|
|
5837
7089
|
}
|
|
5838
7090
|
}
|
|
5839
7091
|
function resolveRuntimePaths() {
|
|
5840
|
-
const home = process.env.HOME ??
|
|
7092
|
+
const home = process.env.HOME ?? homedir5();
|
|
5841
7093
|
if (!home) {
|
|
5842
7094
|
throw new Error("Unable to resolve the current user home directory");
|
|
5843
7095
|
}
|
|
@@ -5853,9 +7105,9 @@ function resolveBridgeEntryPath() {
|
|
|
5853
7105
|
return fileURLToPath3(new URL("./bridge/bridge-main.ts", import.meta.url));
|
|
5854
7106
|
}
|
|
5855
7107
|
function resolveAppLogPath(configPath) {
|
|
5856
|
-
const rootDir =
|
|
5857
|
-
const runtimeDir =
|
|
5858
|
-
return
|
|
7108
|
+
const rootDir = dirname9(configPath);
|
|
7109
|
+
const runtimeDir = join5(rootDir, "runtime");
|
|
7110
|
+
return join5(runtimeDir, "app.log");
|
|
5859
7111
|
}
|
|
5860
7112
|
var init_main = __esm(async () => {
|
|
5861
7113
|
init_command_router();
|
|
@@ -5867,362 +7119,981 @@ var init_main = __esm(async () => {
|
|
|
5867
7119
|
init_app_logger();
|
|
5868
7120
|
init_session_service();
|
|
5869
7121
|
init_state_store();
|
|
7122
|
+
init_run_console();
|
|
5870
7123
|
init_acpx_bridge_client();
|
|
5871
7124
|
init_acpx_cli_transport();
|
|
5872
7125
|
init_weixin_sdk();
|
|
5873
7126
|
if (false) {}
|
|
5874
7127
|
});
|
|
5875
7128
|
|
|
5876
|
-
// src/
|
|
5877
|
-
import {
|
|
5878
|
-
|
|
7129
|
+
// src/doctor/checks/acpx-check.ts
|
|
7130
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
7131
|
+
async function checkAcpx(options = {}) {
|
|
7132
|
+
const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
|
|
7133
|
+
try {
|
|
7134
|
+
const config = await (options.loadConfig ?? loadConfig)(runtimePaths.configPath);
|
|
7135
|
+
const metadata = (options.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
|
|
7136
|
+
configuredCommand: config.transport.command
|
|
7137
|
+
});
|
|
7138
|
+
const version = await (options.runVersion ?? defaultRunVersion)(metadata.command);
|
|
7139
|
+
return {
|
|
7140
|
+
id: "acpx",
|
|
7141
|
+
label: "acpx",
|
|
7142
|
+
severity: "pass",
|
|
7143
|
+
summary: `resolved ${metadata.command} (${version})`,
|
|
7144
|
+
details: buildDetails(metadata, version, options.verbose),
|
|
7145
|
+
metadata: {
|
|
7146
|
+
command: metadata.command,
|
|
7147
|
+
source: metadata.source,
|
|
7148
|
+
version
|
|
7149
|
+
}
|
|
7150
|
+
};
|
|
7151
|
+
} catch (error) {
|
|
7152
|
+
const message = formatError(error);
|
|
7153
|
+
const details = [`config path: ${runtimePaths.configPath}`, `error: ${message}`];
|
|
7154
|
+
return {
|
|
7155
|
+
id: "acpx",
|
|
7156
|
+
label: "acpx",
|
|
7157
|
+
severity: "fail",
|
|
7158
|
+
summary: "acpx version check failed",
|
|
7159
|
+
details
|
|
7160
|
+
};
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
function buildDetails(metadata, version, verbose) {
|
|
7164
|
+
const details = [
|
|
7165
|
+
`command: ${metadata.command}`,
|
|
7166
|
+
`source: ${metadata.source}`,
|
|
7167
|
+
`version: ${version}`
|
|
7168
|
+
];
|
|
7169
|
+
if (verbose) {
|
|
7170
|
+
details.push(`resolution: ${metadata.explanation}`);
|
|
7171
|
+
}
|
|
7172
|
+
return details;
|
|
7173
|
+
}
|
|
7174
|
+
async function defaultRunVersion(command) {
|
|
7175
|
+
const spawnSpec = resolveSpawnCommand(command, ["--version"]);
|
|
7176
|
+
return await new Promise((resolve2, reject) => {
|
|
7177
|
+
const child = spawn4(spawnSpec.command, spawnSpec.args, {
|
|
7178
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7179
|
+
});
|
|
7180
|
+
let stdout = "";
|
|
7181
|
+
let stderr = "";
|
|
7182
|
+
child.stdout.on("data", (chunk) => {
|
|
7183
|
+
stdout += String(chunk);
|
|
7184
|
+
});
|
|
7185
|
+
child.stderr.on("data", (chunk) => {
|
|
7186
|
+
stderr += String(chunk);
|
|
7187
|
+
});
|
|
7188
|
+
child.on("error", reject);
|
|
7189
|
+
child.on("close", (code) => {
|
|
7190
|
+
if (code === 0) {
|
|
7191
|
+
const version = stdout.trim() || stderr.trim();
|
|
7192
|
+
if (version.length > 0) {
|
|
7193
|
+
resolve2(version);
|
|
7194
|
+
return;
|
|
7195
|
+
}
|
|
7196
|
+
}
|
|
7197
|
+
reject(new Error(stderr.trim() || stdout.trim() || `acpx --version exited with code ${code ?? 1}`));
|
|
7198
|
+
});
|
|
7199
|
+
});
|
|
7200
|
+
}
|
|
7201
|
+
function formatError(error) {
|
|
7202
|
+
return error instanceof Error ? error.message : String(error);
|
|
7203
|
+
}
|
|
7204
|
+
var init_acpx_check = __esm(async () => {
|
|
7205
|
+
init_load_config();
|
|
7206
|
+
init_resolve_acpx_command();
|
|
7207
|
+
init_spawn_command();
|
|
7208
|
+
await init_main();
|
|
7209
|
+
});
|
|
7210
|
+
|
|
7211
|
+
// src/doctor/checks/bridge-check.ts
|
|
7212
|
+
async function checkBridge(options = {}) {
|
|
7213
|
+
const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
|
|
7214
|
+
try {
|
|
7215
|
+
const config = await (options.loadConfig ?? loadConfig)(runtimePaths.configPath);
|
|
7216
|
+
if (config.transport.type === "acpx-cli") {
|
|
7217
|
+
return {
|
|
7218
|
+
id: "bridge",
|
|
7219
|
+
label: "Bridge",
|
|
7220
|
+
severity: "skip",
|
|
7221
|
+
summary: "bridge check skipped for acpx-cli transport"
|
|
7222
|
+
};
|
|
7223
|
+
}
|
|
7224
|
+
const metadata = (options.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
|
|
7225
|
+
configuredCommand: config.transport.command
|
|
7226
|
+
});
|
|
7227
|
+
const client = await (options.spawnAcpxBridgeClient ?? spawnAcpxBridgeClient)({
|
|
7228
|
+
acpxCommand: metadata.command,
|
|
7229
|
+
bridgeEntryPath: (options.resolveBridgeEntryPath ?? resolveBridgeEntryPath)(),
|
|
7230
|
+
cwd: options.cwd ?? process.cwd(),
|
|
7231
|
+
permissionMode: config.transport.permissionMode,
|
|
7232
|
+
nonInteractivePermissions: config.transport.nonInteractivePermissions
|
|
7233
|
+
});
|
|
7234
|
+
try {
|
|
7235
|
+
return {
|
|
7236
|
+
id: "bridge",
|
|
7237
|
+
label: "Bridge",
|
|
7238
|
+
severity: "pass",
|
|
7239
|
+
summary: "bridge responded to ping",
|
|
7240
|
+
details: buildDetails2(metadata, options.verbose),
|
|
7241
|
+
metadata: {
|
|
7242
|
+
acpxCommand: metadata.command,
|
|
7243
|
+
source: metadata.source
|
|
7244
|
+
}
|
|
7245
|
+
};
|
|
7246
|
+
} finally {
|
|
7247
|
+
await client.dispose();
|
|
7248
|
+
}
|
|
7249
|
+
} catch (error) {
|
|
7250
|
+
return {
|
|
7251
|
+
id: "bridge",
|
|
7252
|
+
label: "Bridge",
|
|
7253
|
+
severity: "fail",
|
|
7254
|
+
summary: "bridge startup failed",
|
|
7255
|
+
details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError2(error)}`]
|
|
7256
|
+
};
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
function buildDetails2(metadata, verbose) {
|
|
7260
|
+
const details = [`acpx command: ${metadata.command}`, `source: ${metadata.source}`];
|
|
7261
|
+
if (verbose) {
|
|
7262
|
+
details.push(`resolution: ${metadata.explanation}`);
|
|
7263
|
+
}
|
|
7264
|
+
return details;
|
|
7265
|
+
}
|
|
7266
|
+
function formatError2(error) {
|
|
7267
|
+
return error instanceof Error ? error.message : String(error);
|
|
7268
|
+
}
|
|
7269
|
+
var init_bridge_check = __esm(async () => {
|
|
7270
|
+
init_load_config();
|
|
7271
|
+
init_resolve_acpx_command();
|
|
7272
|
+
init_acpx_bridge_client();
|
|
7273
|
+
await init_main();
|
|
7274
|
+
});
|
|
7275
|
+
|
|
7276
|
+
// src/doctor/checks/config-check.ts
|
|
7277
|
+
async function checkConfig(options = {}) {
|
|
7278
|
+
const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
|
|
7279
|
+
const configPath = runtimePaths.configPath;
|
|
7280
|
+
try {
|
|
7281
|
+
const config = await (options.loadConfig ?? loadConfig)(configPath);
|
|
7282
|
+
return {
|
|
7283
|
+
id: "config",
|
|
7284
|
+
label: "Config",
|
|
7285
|
+
severity: "pass",
|
|
7286
|
+
summary: "configuration loaded",
|
|
7287
|
+
details: [`config path: ${configPath}`],
|
|
7288
|
+
metadata: {
|
|
7289
|
+
configPath,
|
|
7290
|
+
config
|
|
7291
|
+
}
|
|
7292
|
+
};
|
|
7293
|
+
} catch (error) {
|
|
7294
|
+
return {
|
|
7295
|
+
id: "config",
|
|
7296
|
+
label: "Config",
|
|
7297
|
+
severity: "fail",
|
|
7298
|
+
summary: "configuration is invalid",
|
|
7299
|
+
details: [`config path: ${configPath}`, `error: ${formatError3(error)}`]
|
|
7300
|
+
};
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
7303
|
+
function formatError3(error) {
|
|
7304
|
+
if (error instanceof Error) {
|
|
7305
|
+
return error.message;
|
|
7306
|
+
}
|
|
7307
|
+
return String(error);
|
|
7308
|
+
}
|
|
7309
|
+
var init_config_check = __esm(async () => {
|
|
7310
|
+
init_load_config();
|
|
7311
|
+
await init_main();
|
|
7312
|
+
});
|
|
7313
|
+
|
|
7314
|
+
// src/doctor/checks/daemon-check.ts
|
|
5879
7315
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
7316
|
+
import { homedir as homedir6 } from "node:os";
|
|
7317
|
+
async function checkDaemon(options = {}) {
|
|
7318
|
+
const home = options.home ?? process.env.HOME ?? homedir6();
|
|
7319
|
+
const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({ home });
|
|
7320
|
+
const controller = createDaemonController(paths, {
|
|
7321
|
+
processExecPath: options.processExecPath ?? process.execPath,
|
|
7322
|
+
cliEntryPath: options.cliEntryPath ?? resolveCliEntryPath(),
|
|
7323
|
+
cwd: options.cwd ?? process.cwd(),
|
|
7324
|
+
env: options.env ?? process.env,
|
|
7325
|
+
isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning3
|
|
7326
|
+
});
|
|
7327
|
+
try {
|
|
7328
|
+
const status = await controller.getStatus();
|
|
7329
|
+
switch (status.state) {
|
|
7330
|
+
case "running":
|
|
7331
|
+
return {
|
|
7332
|
+
id: "daemon",
|
|
7333
|
+
label: "Daemon",
|
|
7334
|
+
severity: "pass",
|
|
7335
|
+
summary: "daemon is running",
|
|
7336
|
+
details: [`pid: ${status.pid}`],
|
|
7337
|
+
metadata: {
|
|
7338
|
+
paths,
|
|
7339
|
+
status
|
|
7340
|
+
}
|
|
7341
|
+
};
|
|
7342
|
+
case "stopped":
|
|
7343
|
+
return {
|
|
7344
|
+
id: "daemon",
|
|
7345
|
+
label: "Daemon",
|
|
7346
|
+
severity: "warn",
|
|
7347
|
+
summary: status.stale ? "daemon was stopped and stale runtime files were cleared" : "daemon is not running",
|
|
7348
|
+
details: status.stale ? ["stale runtime files were cleared"] : undefined,
|
|
7349
|
+
suggestions: ["run: weacpx start"],
|
|
7350
|
+
metadata: {
|
|
7351
|
+
paths,
|
|
7352
|
+
status
|
|
7353
|
+
}
|
|
7354
|
+
};
|
|
7355
|
+
case "indeterminate":
|
|
7356
|
+
return {
|
|
7357
|
+
id: "daemon",
|
|
7358
|
+
label: "Daemon",
|
|
7359
|
+
severity: "fail",
|
|
7360
|
+
summary: "daemon status is indeterminate",
|
|
7361
|
+
details: [`pid: ${status.pid}`, `reason: ${status.reason}`],
|
|
7362
|
+
metadata: {
|
|
7363
|
+
paths,
|
|
7364
|
+
status
|
|
7365
|
+
}
|
|
7366
|
+
};
|
|
7367
|
+
}
|
|
7368
|
+
return {
|
|
7369
|
+
id: "daemon",
|
|
7370
|
+
label: "Daemon",
|
|
7371
|
+
severity: "fail",
|
|
7372
|
+
summary: "daemon status lookup returned an unknown state",
|
|
7373
|
+
details: [`state: ${status.state ?? "unknown"}`],
|
|
7374
|
+
metadata: {
|
|
7375
|
+
paths
|
|
5903
7376
|
}
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
7377
|
+
};
|
|
7378
|
+
} catch (error) {
|
|
7379
|
+
return {
|
|
7380
|
+
id: "daemon",
|
|
7381
|
+
label: "Daemon",
|
|
7382
|
+
severity: "fail",
|
|
7383
|
+
summary: "daemon status could not be read",
|
|
7384
|
+
details: [
|
|
7385
|
+
`runtime dir: ${paths.runtimeDir}`,
|
|
7386
|
+
`pid file: ${paths.pidFile}`,
|
|
7387
|
+
`status file: ${paths.statusFile}`,
|
|
7388
|
+
`error: ${formatError4(error)}`
|
|
7389
|
+
],
|
|
7390
|
+
metadata: {
|
|
7391
|
+
paths
|
|
5908
7392
|
}
|
|
5909
|
-
|
|
5910
|
-
}
|
|
5911
|
-
}
|
|
5912
|
-
async save(status) {
|
|
5913
|
-
await mkdir(dirname(this.path), { recursive: true });
|
|
5914
|
-
await writeFile(this.path, JSON.stringify(status, null, 2));
|
|
7393
|
+
};
|
|
5915
7394
|
}
|
|
5916
|
-
|
|
5917
|
-
|
|
7395
|
+
}
|
|
7396
|
+
function defaultIsProcessRunning3(pid) {
|
|
7397
|
+
try {
|
|
7398
|
+
process.kill(pid, 0);
|
|
7399
|
+
return true;
|
|
7400
|
+
} catch {
|
|
7401
|
+
return false;
|
|
5918
7402
|
}
|
|
5919
7403
|
}
|
|
7404
|
+
function resolveCliEntryPath() {
|
|
7405
|
+
return process.argv[1] ?? fileURLToPath4(import.meta.url);
|
|
7406
|
+
}
|
|
7407
|
+
function formatError4(error) {
|
|
7408
|
+
return error instanceof Error ? error.message : String(error);
|
|
7409
|
+
}
|
|
7410
|
+
var init_daemon_check = __esm(() => {
|
|
7411
|
+
init_create_daemon_controller();
|
|
7412
|
+
init_daemon_files();
|
|
7413
|
+
});
|
|
5920
7414
|
|
|
5921
|
-
// src/
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
7415
|
+
// src/doctor/checks/runtime-check.ts
|
|
7416
|
+
import { constants } from "node:fs";
|
|
7417
|
+
import { access as access3, stat as stat2 } from "node:fs/promises";
|
|
7418
|
+
import { dirname as dirname10 } from "node:path";
|
|
7419
|
+
import { homedir as homedir7 } from "node:os";
|
|
7420
|
+
async function checkRuntime(options = {}) {
|
|
7421
|
+
const home = options.home ?? process.env.HOME ?? homedir7();
|
|
7422
|
+
const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({ home });
|
|
7423
|
+
const probe = options.probe ?? createRuntimeFsProbe();
|
|
7424
|
+
const platform = options.platform ?? process.platform;
|
|
7425
|
+
const checks = [
|
|
7426
|
+
await checkDirectoryCreatable("runtimeDir", paths.runtimeDir, probe, platform),
|
|
7427
|
+
await checkFileCreatable("pidFile", paths.pidFile, probe, platform),
|
|
7428
|
+
await checkFileCreatable("statusFile", paths.statusFile, probe, platform),
|
|
7429
|
+
await checkFileCreatable("stdoutLog", paths.stdoutLog, probe, platform),
|
|
7430
|
+
await checkFileCreatable("stderrLog", paths.stderrLog, probe, platform),
|
|
7431
|
+
await checkFileCreatable("appLog", paths.appLog, probe, platform)
|
|
7432
|
+
];
|
|
7433
|
+
const failure = checks.find((check) => !check.ok);
|
|
7434
|
+
if (failure) {
|
|
7435
|
+
return {
|
|
7436
|
+
id: "runtime",
|
|
7437
|
+
label: "Runtime",
|
|
7438
|
+
severity: "fail",
|
|
7439
|
+
summary: "daemon runtime paths are not usable",
|
|
7440
|
+
details: checks.map((check) => check.detail)
|
|
7441
|
+
};
|
|
5946
7442
|
}
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
7443
|
+
return {
|
|
7444
|
+
id: "runtime",
|
|
7445
|
+
label: "Runtime",
|
|
7446
|
+
severity: "pass",
|
|
7447
|
+
summary: "daemon runtime paths are usable",
|
|
7448
|
+
details: checks.map((check) => check.detail),
|
|
7449
|
+
metadata: {
|
|
7450
|
+
paths
|
|
5952
7451
|
}
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
7452
|
+
};
|
|
7453
|
+
}
|
|
7454
|
+
function createRuntimeFsProbe() {
|
|
7455
|
+
return {
|
|
7456
|
+
stat: async (path11) => await stat2(path11),
|
|
7457
|
+
access: async (path11, mode) => await access3(path11, mode)
|
|
7458
|
+
};
|
|
7459
|
+
}
|
|
7460
|
+
async function checkDirectoryCreatable(label, path11, probe, platform) {
|
|
7461
|
+
try {
|
|
7462
|
+
const stats = await probe.stat(path11);
|
|
7463
|
+
if (!stats.isDirectory()) {
|
|
7464
|
+
return {
|
|
7465
|
+
ok: false,
|
|
7466
|
+
detail: `${label}: ${path11} (exists but is not a directory)`
|
|
7467
|
+
};
|
|
5956
7468
|
}
|
|
5957
|
-
|
|
5958
|
-
|
|
7469
|
+
await probe.access(path11, directoryAccessMode(platform));
|
|
7470
|
+
return {
|
|
7471
|
+
ok: true,
|
|
7472
|
+
detail: `${label}: ${path11} (writable)`
|
|
7473
|
+
};
|
|
7474
|
+
} catch (error) {
|
|
7475
|
+
if (!isMissingPathError(error)) {
|
|
7476
|
+
return {
|
|
7477
|
+
ok: false,
|
|
7478
|
+
detail: `${label}: ${path11} (unusable: ${formatError5(error)})`
|
|
7479
|
+
};
|
|
7480
|
+
}
|
|
7481
|
+
const parentCheck = await checkCreatableAncestorDirectory(path11, probe, platform);
|
|
7482
|
+
if (!parentCheck.ok) {
|
|
7483
|
+
return {
|
|
7484
|
+
ok: false,
|
|
7485
|
+
detail: `${label}: ${path11} (parent not writable: ${parentCheck.blockingPath})`
|
|
7486
|
+
};
|
|
5959
7487
|
}
|
|
5960
7488
|
return {
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
status
|
|
7489
|
+
ok: true,
|
|
7490
|
+
detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
|
|
5964
7491
|
};
|
|
5965
7492
|
}
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
7493
|
+
}
|
|
7494
|
+
async function checkFileCreatable(label, path11, probe, platform) {
|
|
7495
|
+
try {
|
|
7496
|
+
const stats = await probe.stat(path11);
|
|
7497
|
+
if (stats.isDirectory()) {
|
|
7498
|
+
return {
|
|
7499
|
+
ok: false,
|
|
7500
|
+
detail: `${label}: ${path11} (exists but is a directory)`
|
|
7501
|
+
};
|
|
5970
7502
|
}
|
|
5971
|
-
await
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
}
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
7503
|
+
await probe.access(path11, constants.W_OK);
|
|
7504
|
+
return {
|
|
7505
|
+
ok: true,
|
|
7506
|
+
detail: `${label}: ${path11} (writable)`
|
|
7507
|
+
};
|
|
7508
|
+
} catch (error) {
|
|
7509
|
+
if (!isMissingPathError(error)) {
|
|
7510
|
+
return {
|
|
7511
|
+
ok: false,
|
|
7512
|
+
detail: `${label}: ${path11} (unusable: ${formatError5(error)})`
|
|
7513
|
+
};
|
|
5981
7514
|
}
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
7515
|
+
const parentCheck = await checkCreatableAncestorDirectory(dirname10(path11), probe, platform);
|
|
7516
|
+
if (!parentCheck.ok) {
|
|
7517
|
+
return {
|
|
7518
|
+
ok: false,
|
|
7519
|
+
detail: `${label}: ${path11} (parent not writable: ${parentCheck.blockingPath})`
|
|
7520
|
+
};
|
|
5985
7521
|
}
|
|
5986
|
-
|
|
5987
|
-
|
|
7522
|
+
return {
|
|
7523
|
+
ok: true,
|
|
7524
|
+
detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
|
|
7525
|
+
};
|
|
5988
7526
|
}
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
7527
|
+
}
|
|
7528
|
+
async function checkCreatableAncestorDirectory(path11, probe, platform) {
|
|
7529
|
+
try {
|
|
7530
|
+
const stats = await probe.stat(path11);
|
|
7531
|
+
if (!stats.isDirectory()) {
|
|
7532
|
+
return {
|
|
7533
|
+
ok: false,
|
|
7534
|
+
creatableFrom: path11,
|
|
7535
|
+
blockingPath: path11
|
|
7536
|
+
};
|
|
5999
7537
|
}
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
}
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
while (Date.now() < deadline) {
|
|
6013
|
-
const status = await this.statusStore.load();
|
|
6014
|
-
if (status?.pid === pid) {
|
|
6015
|
-
return;
|
|
6016
|
-
}
|
|
6017
|
-
if (!this.deps.isProcessRunning(pid)) {
|
|
6018
|
-
await this.clearRuntimeFiles();
|
|
6019
|
-
throw new Error(`weacpx daemon exited before reporting ready state (pid ${pid})`);
|
|
6020
|
-
}
|
|
6021
|
-
await this.onStartupPoll();
|
|
7538
|
+
await probe.access(path11, directoryAccessMode(platform));
|
|
7539
|
+
return {
|
|
7540
|
+
ok: true,
|
|
7541
|
+
creatableFrom: path11
|
|
7542
|
+
};
|
|
7543
|
+
} catch (error) {
|
|
7544
|
+
if (!isMissingPathError(error)) {
|
|
7545
|
+
return {
|
|
7546
|
+
ok: false,
|
|
7547
|
+
creatableFrom: path11,
|
|
7548
|
+
blockingPath: path11
|
|
7549
|
+
};
|
|
6022
7550
|
}
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
}
|
|
6031
|
-
await this.onShutdownPoll();
|
|
7551
|
+
const parent = dirname10(path11);
|
|
7552
|
+
if (parent === path11) {
|
|
7553
|
+
return {
|
|
7554
|
+
ok: false,
|
|
7555
|
+
creatableFrom: path11,
|
|
7556
|
+
blockingPath: path11
|
|
7557
|
+
};
|
|
6032
7558
|
}
|
|
6033
|
-
|
|
6034
|
-
|
|
7559
|
+
const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
|
|
7560
|
+
if (!parentCheck.ok) {
|
|
7561
|
+
return parentCheck;
|
|
6035
7562
|
}
|
|
6036
|
-
|
|
7563
|
+
return {
|
|
7564
|
+
ok: true,
|
|
7565
|
+
creatableFrom: parentCheck.creatableFrom
|
|
7566
|
+
};
|
|
6037
7567
|
}
|
|
6038
7568
|
}
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
function createDaemonController(paths, options) {
|
|
6042
|
-
return new DaemonController(paths, {
|
|
6043
|
-
isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning,
|
|
6044
|
-
spawnDetached: async () => {
|
|
6045
|
-
await mkdir3(paths.runtimeDir, { recursive: true });
|
|
6046
|
-
const stdoutHandle = await open(paths.stdoutLog, "a");
|
|
6047
|
-
const stderrHandle = await open(paths.stderrLog, "a");
|
|
6048
|
-
try {
|
|
6049
|
-
return await (options.spawnProcess ?? defaultSpawnProcess)(buildSpawnRequest(paths, options, stdoutHandle.fd, stderrHandle.fd));
|
|
6050
|
-
} finally {
|
|
6051
|
-
await stdoutHandle.close();
|
|
6052
|
-
await stderrHandle.close();
|
|
6053
|
-
}
|
|
6054
|
-
},
|
|
6055
|
-
terminateProcess: options.terminateProcess ?? defaultTerminateProcess
|
|
6056
|
-
});
|
|
7569
|
+
function directoryAccessMode(platform) {
|
|
7570
|
+
return platform === "win32" ? constants.W_OK : DIRECTORY_USABLE;
|
|
6057
7571
|
}
|
|
6058
|
-
function
|
|
7572
|
+
function isMissingPathError(error) {
|
|
7573
|
+
return isErrnoError(error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
7574
|
+
}
|
|
7575
|
+
function isErrnoError(error) {
|
|
7576
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
7577
|
+
}
|
|
7578
|
+
function formatError5(error) {
|
|
7579
|
+
if (error instanceof Error) {
|
|
7580
|
+
return error.message;
|
|
7581
|
+
}
|
|
7582
|
+
return String(error);
|
|
7583
|
+
}
|
|
7584
|
+
var DIRECTORY_USABLE;
|
|
7585
|
+
var init_runtime_check = __esm(() => {
|
|
7586
|
+
init_daemon_files();
|
|
7587
|
+
DIRECTORY_USABLE = constants.W_OK | constants.X_OK;
|
|
7588
|
+
});
|
|
7589
|
+
|
|
7590
|
+
// src/doctor/checks/smoke-check.ts
|
|
7591
|
+
async function checkSmoke(options = {}, deps = {}) {
|
|
7592
|
+
const resolvedOptions = { ...options, ...deps };
|
|
7593
|
+
const runtimePaths = (resolvedOptions.resolveRuntimePaths ?? resolveRuntimePaths)();
|
|
6059
7594
|
try {
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
7595
|
+
const config = await (resolvedOptions.loadConfig ?? loadConfig)(runtimePaths.configPath);
|
|
7596
|
+
const agentSelection = selectAgent(config, resolvedOptions.agent);
|
|
7597
|
+
if (agentSelection.error) {
|
|
7598
|
+
return agentSelection.error;
|
|
7599
|
+
}
|
|
7600
|
+
const workspaceSelection = selectWorkspace(config, resolvedOptions.workspace);
|
|
7601
|
+
if (workspaceSelection.error) {
|
|
7602
|
+
return workspaceSelection.error;
|
|
7603
|
+
}
|
|
7604
|
+
const missingDefaults = [agentSelection.missingDefault, workspaceSelection.missingDefault].filter((value) => typeof value === "string");
|
|
7605
|
+
if (missingDefaults.length > 0) {
|
|
7606
|
+
return {
|
|
7607
|
+
id: "smoke",
|
|
7608
|
+
label: "Smoke",
|
|
7609
|
+
severity: "skip",
|
|
7610
|
+
summary: `smoke prerequisites missing: ${missingDefaults.join(", ")}`,
|
|
7611
|
+
details: [
|
|
7612
|
+
`config path: ${runtimePaths.configPath}`,
|
|
7613
|
+
...selectionDetails(agentSelection, workspaceSelection)
|
|
7614
|
+
],
|
|
7615
|
+
suggestions: ["configure at least one agent and one workspace before running --smoke"]
|
|
7616
|
+
};
|
|
7617
|
+
}
|
|
7618
|
+
const agent = agentSelection.value;
|
|
7619
|
+
const workspace = workspaceSelection.value;
|
|
7620
|
+
if (!agent || !workspace) {
|
|
7621
|
+
return {
|
|
7622
|
+
id: "smoke",
|
|
7623
|
+
label: "Smoke",
|
|
7624
|
+
severity: "skip",
|
|
7625
|
+
summary: "smoke prerequisites missing: agent, workspace",
|
|
7626
|
+
details: [
|
|
7627
|
+
`config path: ${runtimePaths.configPath}`,
|
|
7628
|
+
...selectionDetails(agentSelection, workspaceSelection)
|
|
7629
|
+
]
|
|
7630
|
+
};
|
|
7631
|
+
}
|
|
7632
|
+
const metadata = (resolvedOptions.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
|
|
7633
|
+
configuredCommand: config.transport.command
|
|
7634
|
+
});
|
|
7635
|
+
const transport = await (resolvedOptions.createTransport ?? defaultCreateTransport)({
|
|
7636
|
+
config,
|
|
7637
|
+
metadata,
|
|
7638
|
+
resolveBridgeEntryPath: resolvedOptions.resolveBridgeEntryPath,
|
|
7639
|
+
spawnAcpxBridgeClient: resolvedOptions.spawnAcpxBridgeClient
|
|
7640
|
+
});
|
|
7641
|
+
const session = buildSession({
|
|
7642
|
+
config,
|
|
7643
|
+
agent,
|
|
7644
|
+
workspace,
|
|
7645
|
+
now: resolvedOptions.now
|
|
7646
|
+
});
|
|
7647
|
+
try {
|
|
7648
|
+
await transport.ensureSession(session);
|
|
7649
|
+
const reply = await transport.prompt(session, SMOKE_PROMPT);
|
|
7650
|
+
const replyText = reply.text.trim();
|
|
7651
|
+
if (replyText.length === 0) {
|
|
7652
|
+
return {
|
|
7653
|
+
id: "smoke",
|
|
7654
|
+
label: "Smoke",
|
|
7655
|
+
severity: "fail",
|
|
7656
|
+
summary: "smoke prompt returned empty text",
|
|
7657
|
+
details: buildDetails3({
|
|
7658
|
+
runtimePaths,
|
|
7659
|
+
metadata,
|
|
7660
|
+
session,
|
|
7661
|
+
agentReason: agentSelection.reason,
|
|
7662
|
+
workspaceReason: workspaceSelection.reason,
|
|
7663
|
+
replyText,
|
|
7664
|
+
verbose: resolvedOptions.verbose
|
|
7665
|
+
})
|
|
7666
|
+
};
|
|
7667
|
+
}
|
|
7668
|
+
return {
|
|
7669
|
+
id: "smoke",
|
|
7670
|
+
label: "Smoke",
|
|
7671
|
+
severity: replyText === "ok" ? "pass" : "warn",
|
|
7672
|
+
summary: replyText === "ok" ? "smoke prompt succeeded and reply received" : "smoke prompt succeeded with non-ideal reply",
|
|
7673
|
+
details: buildDetails3({
|
|
7674
|
+
runtimePaths,
|
|
7675
|
+
metadata,
|
|
7676
|
+
session,
|
|
7677
|
+
agentReason: agentSelection.reason,
|
|
7678
|
+
workspaceReason: workspaceSelection.reason,
|
|
7679
|
+
replyText,
|
|
7680
|
+
verbose: resolvedOptions.verbose
|
|
7681
|
+
}),
|
|
7682
|
+
metadata: {
|
|
7683
|
+
agent: session.agent,
|
|
7684
|
+
workspace: session.workspace,
|
|
7685
|
+
transportSession: session.transportSession,
|
|
7686
|
+
replyText
|
|
7687
|
+
}
|
|
7688
|
+
};
|
|
7689
|
+
} finally {
|
|
7690
|
+
await transport.dispose?.();
|
|
7691
|
+
}
|
|
7692
|
+
} catch (error) {
|
|
7693
|
+
return {
|
|
7694
|
+
id: "smoke",
|
|
7695
|
+
label: "Smoke",
|
|
7696
|
+
severity: "fail",
|
|
7697
|
+
summary: "smoke transport probe failed",
|
|
7698
|
+
details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError6(error)}`]
|
|
7699
|
+
};
|
|
6064
7700
|
}
|
|
6065
7701
|
}
|
|
6066
|
-
function
|
|
6067
|
-
|
|
6068
|
-
|
|
7702
|
+
function selectAgent(config, explicitAgent) {
|
|
7703
|
+
if (explicitAgent) {
|
|
7704
|
+
if (!(explicitAgent in config.agents)) {
|
|
7705
|
+
return {
|
|
7706
|
+
reason: "explicit --agent",
|
|
7707
|
+
error: createSelectionFailure(`smoke agent not found: ${explicitAgent}`)
|
|
7708
|
+
};
|
|
7709
|
+
}
|
|
6069
7710
|
return {
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
cwd: options.cwd,
|
|
6080
|
-
env: {
|
|
6081
|
-
...options.env,
|
|
6082
|
-
WEACPX_DAEMON_COMMAND: options.processExecPath,
|
|
6083
|
-
WEACPX_DAEMON_ARG0: options.cliEntryPath,
|
|
6084
|
-
WEACPX_DAEMON_ARG1: "run",
|
|
6085
|
-
WEACPX_DAEMON_CWD: options.cwd,
|
|
6086
|
-
WEACPX_DAEMON_STDOUT: paths.stdoutLog,
|
|
6087
|
-
WEACPX_DAEMON_STDERR: paths.stderrLog
|
|
6088
|
-
},
|
|
6089
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
6090
|
-
windowsHide: true
|
|
6091
|
-
}
|
|
7711
|
+
value: explicitAgent,
|
|
7712
|
+
reason: "explicit --agent"
|
|
7713
|
+
};
|
|
7714
|
+
}
|
|
7715
|
+
const firstAgent = Object.keys(config.agents)[0];
|
|
7716
|
+
if (!firstAgent) {
|
|
7717
|
+
return {
|
|
7718
|
+
missingDefault: "agent",
|
|
7719
|
+
reason: "no configured agent available"
|
|
6092
7720
|
};
|
|
6093
7721
|
}
|
|
6094
7722
|
return {
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
7723
|
+
value: firstAgent,
|
|
7724
|
+
reason: "default first configured agent"
|
|
7725
|
+
};
|
|
7726
|
+
}
|
|
7727
|
+
function selectWorkspace(config, explicitWorkspace) {
|
|
7728
|
+
if (explicitWorkspace) {
|
|
7729
|
+
if (!(explicitWorkspace in config.workspaces)) {
|
|
7730
|
+
return {
|
|
7731
|
+
reason: "explicit --workspace",
|
|
7732
|
+
error: createSelectionFailure(`smoke workspace not found: ${explicitWorkspace}`)
|
|
7733
|
+
};
|
|
6103
7734
|
}
|
|
7735
|
+
return {
|
|
7736
|
+
value: explicitWorkspace,
|
|
7737
|
+
reason: "explicit --workspace"
|
|
7738
|
+
};
|
|
7739
|
+
}
|
|
7740
|
+
const firstWorkspace = Object.keys(config.workspaces)[0];
|
|
7741
|
+
if (!firstWorkspace) {
|
|
7742
|
+
return {
|
|
7743
|
+
missingDefault: "workspace",
|
|
7744
|
+
reason: "no configured workspace available"
|
|
7745
|
+
};
|
|
7746
|
+
}
|
|
7747
|
+
return {
|
|
7748
|
+
value: firstWorkspace,
|
|
7749
|
+
reason: "default first configured workspace"
|
|
6104
7750
|
};
|
|
6105
7751
|
}
|
|
6106
|
-
function
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
" -WindowStyle Hidden `",
|
|
6114
|
-
" -PassThru",
|
|
6115
|
-
"[Console]::Out.WriteLine($process.Id)"
|
|
6116
|
-
].join(`
|
|
6117
|
-
`);
|
|
6118
|
-
return Buffer.from(script, "utf16le").toString("base64");
|
|
7752
|
+
function createSelectionFailure(summary) {
|
|
7753
|
+
return {
|
|
7754
|
+
id: "smoke",
|
|
7755
|
+
label: "Smoke",
|
|
7756
|
+
severity: "fail",
|
|
7757
|
+
summary
|
|
7758
|
+
};
|
|
6119
7759
|
}
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
7760
|
+
function selectionDetails(agentSelection, workspaceSelection) {
|
|
7761
|
+
const details = [];
|
|
7762
|
+
if (agentSelection.value) {
|
|
7763
|
+
details.push(`agent: ${agentSelection.value} (${agentSelection.reason})`);
|
|
7764
|
+
} else {
|
|
7765
|
+
details.push(`agent: unavailable (${agentSelection.reason})`);
|
|
6123
7766
|
}
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
7767
|
+
if (workspaceSelection.value) {
|
|
7768
|
+
details.push(`workspace: ${workspaceSelection.value} (${workspaceSelection.reason})`);
|
|
7769
|
+
} else {
|
|
7770
|
+
details.push(`workspace: unavailable (${workspaceSelection.reason})`);
|
|
7771
|
+
}
|
|
7772
|
+
return details;
|
|
6127
7773
|
}
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
child.on("close", (code) => {
|
|
6156
|
-
if (settled) {
|
|
6157
|
-
return;
|
|
6158
|
-
}
|
|
6159
|
-
if (code !== 0) {
|
|
6160
|
-
settled = true;
|
|
6161
|
-
reject(new Error(`Failed to launch hidden Windows daemon process (exit ${code ?? 1})`));
|
|
6162
|
-
return;
|
|
6163
|
-
}
|
|
6164
|
-
const pid = Number.parseInt(stdout.trim(), 10);
|
|
6165
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
6166
|
-
settled = true;
|
|
6167
|
-
reject(new Error("Failed to read daemon pid from hidden Windows launcher"));
|
|
6168
|
-
return;
|
|
6169
|
-
}
|
|
6170
|
-
settled = true;
|
|
6171
|
-
resolve(pid);
|
|
7774
|
+
function buildSession(options) {
|
|
7775
|
+
const timestamp = (options.now ?? (() => new Date))().getTime();
|
|
7776
|
+
const agentConfig = options.config.agents[options.agent];
|
|
7777
|
+
const workspaceConfig = options.config.workspaces[options.workspace];
|
|
7778
|
+
if (!agentConfig) {
|
|
7779
|
+
throw new Error(`smoke agent not found: ${options.agent}`);
|
|
7780
|
+
}
|
|
7781
|
+
if (!workspaceConfig) {
|
|
7782
|
+
throw new Error(`smoke workspace not found: ${options.workspace}`);
|
|
7783
|
+
}
|
|
7784
|
+
return {
|
|
7785
|
+
alias: "weacpx-doctor",
|
|
7786
|
+
agent: options.agent,
|
|
7787
|
+
...agentConfig.command ? { agentCommand: agentConfig.command } : {},
|
|
7788
|
+
workspace: options.workspace,
|
|
7789
|
+
transportSession: `weacpx-doctor-${timestamp}`,
|
|
7790
|
+
replyMode: options.config.wechat.replyMode,
|
|
7791
|
+
cwd: workspaceConfig.cwd
|
|
7792
|
+
};
|
|
7793
|
+
}
|
|
7794
|
+
async function defaultCreateTransport(options) {
|
|
7795
|
+
if (options.config.transport.type === "acpx-bridge") {
|
|
7796
|
+
const client = await (options.spawnAcpxBridgeClient ?? spawnAcpxBridgeClient)({
|
|
7797
|
+
acpxCommand: options.metadata.command,
|
|
7798
|
+
bridgeEntryPath: (options.resolveBridgeEntryPath ?? resolveBridgeEntryPath)(),
|
|
7799
|
+
permissionMode: options.config.transport.permissionMode,
|
|
7800
|
+
nonInteractivePermissions: options.config.transport.nonInteractivePermissions
|
|
6172
7801
|
});
|
|
7802
|
+
return new AcpxBridgeTransport(client);
|
|
7803
|
+
}
|
|
7804
|
+
return new AcpxCliTransport({
|
|
7805
|
+
...options.config.transport,
|
|
7806
|
+
command: options.metadata.command
|
|
6173
7807
|
});
|
|
6174
7808
|
}
|
|
6175
|
-
|
|
6176
|
-
|
|
7809
|
+
function buildDetails3(options) {
|
|
7810
|
+
const details = [
|
|
7811
|
+
`config path: ${options.runtimePaths.configPath}`,
|
|
7812
|
+
`agent: ${options.session.agent} (${options.agentReason})`,
|
|
7813
|
+
`workspace: ${options.session.workspace} (${options.workspaceReason})`,
|
|
7814
|
+
`transport session: ${options.session.transportSession}`,
|
|
7815
|
+
`reply: ${JSON.stringify(options.replyText)}`
|
|
7816
|
+
];
|
|
7817
|
+
if (options.verbose) {
|
|
7818
|
+
details.push(`transport type: ${options.session.agentCommand ? "agent-command" : "driver-default"}`);
|
|
7819
|
+
details.push(`acpx command: ${options.metadata.command}`);
|
|
7820
|
+
details.push(`acpx source: ${options.metadata.source}`);
|
|
7821
|
+
details.push(`smoke prompt: ${JSON.stringify(SMOKE_PROMPT)}`);
|
|
7822
|
+
}
|
|
7823
|
+
return details;
|
|
6177
7824
|
}
|
|
6178
|
-
|
|
6179
|
-
|
|
7825
|
+
function formatError6(error) {
|
|
7826
|
+
return error instanceof Error ? error.message : String(error);
|
|
7827
|
+
}
|
|
7828
|
+
var SMOKE_PROMPT = "Reply with exactly: ok";
|
|
7829
|
+
var init_smoke_check = __esm(async () => {
|
|
7830
|
+
init_load_config();
|
|
7831
|
+
init_resolve_acpx_command();
|
|
7832
|
+
init_acpx_bridge_client();
|
|
7833
|
+
init_acpx_cli_transport();
|
|
7834
|
+
await init_main();
|
|
7835
|
+
});
|
|
7836
|
+
|
|
7837
|
+
// src/doctor/checks/wechat-check.ts
|
|
7838
|
+
async function checkWechat(options = {}) {
|
|
7839
|
+
const ids = listWeixinAccountIds();
|
|
7840
|
+
const accounts = ids.map((accountId) => {
|
|
6180
7841
|
try {
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
7842
|
+
return {
|
|
7843
|
+
accountId,
|
|
7844
|
+
account: resolveWeixinAccount(accountId)
|
|
7845
|
+
};
|
|
7846
|
+
} catch (error) {
|
|
7847
|
+
return {
|
|
7848
|
+
accountId,
|
|
7849
|
+
error: formatError7(error)
|
|
7850
|
+
};
|
|
7851
|
+
}
|
|
7852
|
+
});
|
|
7853
|
+
const configuredAccount = accounts.find((entry) => ("account" in entry) && entry.account.configured);
|
|
7854
|
+
const loggedIn = Boolean(configuredAccount);
|
|
7855
|
+
if (!loggedIn) {
|
|
7856
|
+
return {
|
|
7857
|
+
id: "wechat",
|
|
7858
|
+
label: "WeChat",
|
|
7859
|
+
severity: "warn",
|
|
7860
|
+
summary: "wechat is not logged in",
|
|
7861
|
+
details: buildVerboseDetails(false, options.verbose, accounts),
|
|
7862
|
+
suggestions: ["weacpx login"]
|
|
7863
|
+
};
|
|
6184
7864
|
}
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
7865
|
+
return {
|
|
7866
|
+
id: "wechat",
|
|
7867
|
+
label: "WeChat",
|
|
7868
|
+
severity: "pass",
|
|
7869
|
+
summary: "wechat is logged in",
|
|
7870
|
+
details: buildVerboseDetails(true, options.verbose, accounts)
|
|
7871
|
+
};
|
|
7872
|
+
}
|
|
7873
|
+
function buildVerboseDetails(loggedIn, verbose, accounts) {
|
|
7874
|
+
if (!verbose) {
|
|
6188
7875
|
return;
|
|
6189
7876
|
}
|
|
6190
|
-
const
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
7877
|
+
const details = [];
|
|
7878
|
+
details.push(`loggedIn: ${loggedIn}`);
|
|
7879
|
+
details.push(`accountIds: ${accounts.length > 0 ? accounts.map((entry) => entry.accountId).join(", ") : "(none)"}`);
|
|
7880
|
+
for (const entry of accounts) {
|
|
7881
|
+
if ("account" in entry) {
|
|
7882
|
+
details.push(`account[${entry.account.accountId}].configured: ${entry.account.configured}`);
|
|
7883
|
+
details.push(`account[${entry.account.accountId}].baseUrl: ${entry.account.baseUrl}`);
|
|
7884
|
+
continue;
|
|
6194
7885
|
}
|
|
6195
|
-
|
|
7886
|
+
details.push(`account[${entry.accountId}].resolveError: ${entry.error ?? "unknown"}`);
|
|
6196
7887
|
}
|
|
6197
|
-
|
|
6198
|
-
process.kill(pid, "SIGKILL");
|
|
6199
|
-
} catch {}
|
|
7888
|
+
return details;
|
|
6200
7889
|
}
|
|
6201
|
-
|
|
6202
|
-
return
|
|
6203
|
-
const child = spawn(command, args, { stdio: "ignore" });
|
|
6204
|
-
child.on("error", reject);
|
|
6205
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
6206
|
-
});
|
|
7890
|
+
function formatError7(error) {
|
|
7891
|
+
return error instanceof Error ? error.message : String(error);
|
|
6207
7892
|
}
|
|
7893
|
+
var init_wechat_check = __esm(() => {
|
|
7894
|
+
init_weixin();
|
|
7895
|
+
});
|
|
6208
7896
|
|
|
6209
|
-
// src/
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
7897
|
+
// src/doctor/render-doctor.ts
|
|
7898
|
+
function renderDoctor(report, options = {}) {
|
|
7899
|
+
return options.verbose ? renderVerboseDoctor(report) : renderDefaultDoctor(report);
|
|
7900
|
+
}
|
|
7901
|
+
function renderDefaultDoctor(report) {
|
|
7902
|
+
const lines = [];
|
|
7903
|
+
for (const check of report.checks) {
|
|
7904
|
+
lines.push(renderCheckLine(check));
|
|
7905
|
+
}
|
|
7906
|
+
lines.push(renderSummaryLine(report.checks));
|
|
7907
|
+
const suggestions = collectSuggestions(report.checks);
|
|
7908
|
+
if (suggestions.length > 0) {
|
|
7909
|
+
lines.push("Next steps:");
|
|
7910
|
+
for (const suggestion of suggestions) {
|
|
7911
|
+
lines.push(`- ${suggestion}`);
|
|
7912
|
+
}
|
|
7913
|
+
}
|
|
7914
|
+
return lines;
|
|
7915
|
+
}
|
|
7916
|
+
function renderVerboseDoctor(report) {
|
|
7917
|
+
const lines = [];
|
|
7918
|
+
for (const check of report.checks) {
|
|
7919
|
+
lines.push(renderCheckLine(check));
|
|
7920
|
+
for (const detail of check.details ?? []) {
|
|
7921
|
+
lines.push(` detail: ${detail}`);
|
|
7922
|
+
}
|
|
7923
|
+
}
|
|
7924
|
+
lines.push(renderSummaryLine(report.checks));
|
|
7925
|
+
const suggestions = collectSuggestions(report.checks);
|
|
7926
|
+
if (suggestions.length > 0) {
|
|
7927
|
+
lines.push("Next steps:");
|
|
7928
|
+
for (const suggestion of suggestions) {
|
|
7929
|
+
lines.push(`- ${suggestion}`);
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
return lines;
|
|
7933
|
+
}
|
|
7934
|
+
function renderCheckLine(check) {
|
|
7935
|
+
return `${SEVERITY_LABELS[check.severity]} ${check.label}: ${check.summary}`;
|
|
7936
|
+
}
|
|
7937
|
+
function renderSummaryLine(checks) {
|
|
7938
|
+
const counts = summarizeChecks(checks);
|
|
7939
|
+
return `Summary: PASS ${counts.pass}, WARN ${counts.warn}, FAIL ${counts.fail}, SKIP ${counts.skip}`;
|
|
7940
|
+
}
|
|
7941
|
+
function summarizeChecks(checks) {
|
|
7942
|
+
return checks.reduce((counts, check) => {
|
|
7943
|
+
counts[check.severity] += 1;
|
|
7944
|
+
return counts;
|
|
7945
|
+
}, { pass: 0, warn: 0, fail: 0, skip: 0 });
|
|
7946
|
+
}
|
|
7947
|
+
function collectSuggestions(checks) {
|
|
7948
|
+
const seen = new Set;
|
|
7949
|
+
const suggestions = [];
|
|
7950
|
+
for (const check of checks) {
|
|
7951
|
+
for (const suggestion of check.suggestions ?? []) {
|
|
7952
|
+
if (seen.has(suggestion)) {
|
|
7953
|
+
continue;
|
|
7954
|
+
}
|
|
7955
|
+
seen.add(suggestion);
|
|
7956
|
+
suggestions.push(suggestion);
|
|
7957
|
+
}
|
|
7958
|
+
}
|
|
7959
|
+
return suggestions;
|
|
7960
|
+
}
|
|
7961
|
+
var SEVERITY_LABELS;
|
|
7962
|
+
var init_render_doctor = __esm(() => {
|
|
7963
|
+
SEVERITY_LABELS = {
|
|
7964
|
+
pass: "PASS",
|
|
7965
|
+
warn: "WARN",
|
|
7966
|
+
fail: "FAIL",
|
|
7967
|
+
skip: "SKIP"
|
|
7968
|
+
};
|
|
7969
|
+
});
|
|
7970
|
+
|
|
7971
|
+
// src/doctor/doctor.ts
|
|
7972
|
+
import { homedir as homedir8 } from "node:os";
|
|
7973
|
+
import { join as join6 } from "node:path";
|
|
7974
|
+
async function runDoctor(options = {}, deps = {}) {
|
|
7975
|
+
const home = deps.home ?? process.env.HOME ?? homedir8();
|
|
7976
|
+
const runtimePaths = resolveDoctorRuntimePaths(home, deps.resolveRuntimePaths);
|
|
7977
|
+
const sharedLoadConfig = createSharedLoadConfig(runtimePaths, deps.loadConfig ?? loadConfig);
|
|
7978
|
+
const checks = [];
|
|
7979
|
+
checks.push(await (deps.checkConfig ?? checkConfig)({
|
|
7980
|
+
loadConfig: sharedLoadConfig,
|
|
7981
|
+
resolveRuntimePaths: () => runtimePaths
|
|
7982
|
+
}));
|
|
7983
|
+
checks.push(await (deps.checkRuntime ?? checkRuntime)({
|
|
7984
|
+
home
|
|
7985
|
+
}));
|
|
7986
|
+
checks.push(await (deps.checkDaemon ?? checkDaemon)({
|
|
7987
|
+
home
|
|
7988
|
+
}));
|
|
7989
|
+
checks.push(await (deps.checkWechat ?? checkWechat)({
|
|
7990
|
+
verbose: options.verbose
|
|
7991
|
+
}));
|
|
7992
|
+
checks.push(await (deps.checkAcpx ?? checkAcpx)({
|
|
7993
|
+
verbose: options.verbose,
|
|
7994
|
+
loadConfig: sharedLoadConfig,
|
|
7995
|
+
resolveRuntimePaths: () => runtimePaths
|
|
7996
|
+
}));
|
|
7997
|
+
checks.push(await (deps.checkBridge ?? checkBridge)({
|
|
7998
|
+
verbose: options.verbose,
|
|
7999
|
+
loadConfig: sharedLoadConfig,
|
|
8000
|
+
resolveRuntimePaths: () => runtimePaths
|
|
8001
|
+
}));
|
|
8002
|
+
checks.push(options.smoke === true ? await (deps.checkSmoke ?? ((runOptions) => defaultCheckSmoke(runOptions, {
|
|
8003
|
+
resolveRuntimePaths: () => runtimePaths,
|
|
8004
|
+
loadConfig: sharedLoadConfig
|
|
8005
|
+
})))(options) : createSmokeSkipResult("smoke probe not requested"));
|
|
8006
|
+
const report = { checks };
|
|
8007
|
+
const output = (deps.renderDoctor ?? renderDoctor)(report, options);
|
|
6213
8008
|
return {
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
8009
|
+
report,
|
|
8010
|
+
output,
|
|
8011
|
+
exitCode: checks.some((check) => check.severity === "fail") ? 1 : 0
|
|
8012
|
+
};
|
|
8013
|
+
}
|
|
8014
|
+
function resolveDoctorRuntimePaths(home, resolver) {
|
|
8015
|
+
if (resolver) {
|
|
8016
|
+
return resolver();
|
|
8017
|
+
}
|
|
8018
|
+
if (depsUseExplicitRuntimeOverrides()) {
|
|
8019
|
+
return resolveRuntimePaths();
|
|
8020
|
+
}
|
|
8021
|
+
return {
|
|
8022
|
+
configPath: join6(home, ".weacpx", "config.json"),
|
|
8023
|
+
statePath: join6(home, ".weacpx", "state.json")
|
|
8024
|
+
};
|
|
8025
|
+
}
|
|
8026
|
+
function depsUseExplicitRuntimeOverrides() {
|
|
8027
|
+
return Boolean(process.env.WEACPX_CONFIG || process.env.WEACPX_STATE);
|
|
8028
|
+
}
|
|
8029
|
+
function createSharedLoadConfig(runtimePaths, loader) {
|
|
8030
|
+
let pending;
|
|
8031
|
+
return async (configPath) => {
|
|
8032
|
+
if (configPath !== runtimePaths.configPath) {
|
|
8033
|
+
return await loader(configPath);
|
|
8034
|
+
}
|
|
8035
|
+
pending ??= loader(configPath);
|
|
8036
|
+
return await pending;
|
|
8037
|
+
};
|
|
8038
|
+
}
|
|
8039
|
+
async function defaultCheckSmoke(options, deps) {
|
|
8040
|
+
return await checkSmoke(options, {
|
|
8041
|
+
resolveRuntimePaths: deps.resolveRuntimePaths,
|
|
8042
|
+
loadConfig: deps.loadConfig
|
|
8043
|
+
});
|
|
8044
|
+
}
|
|
8045
|
+
function createSmokeSkipResult(summary) {
|
|
8046
|
+
return {
|
|
8047
|
+
id: "smoke",
|
|
8048
|
+
label: "Smoke",
|
|
8049
|
+
severity: "skip",
|
|
8050
|
+
summary
|
|
6220
8051
|
};
|
|
6221
8052
|
}
|
|
8053
|
+
var init_doctor = __esm(async () => {
|
|
8054
|
+
init_load_config();
|
|
8055
|
+
init_daemon_check();
|
|
8056
|
+
init_runtime_check();
|
|
8057
|
+
init_wechat_check();
|
|
8058
|
+
init_render_doctor();
|
|
8059
|
+
await __promiseAll([
|
|
8060
|
+
init_main(),
|
|
8061
|
+
init_acpx_check(),
|
|
8062
|
+
init_bridge_check(),
|
|
8063
|
+
init_config_check(),
|
|
8064
|
+
init_smoke_check()
|
|
8065
|
+
]);
|
|
8066
|
+
});
|
|
8067
|
+
|
|
8068
|
+
// src/doctor/index.ts
|
|
8069
|
+
var exports_doctor = {};
|
|
8070
|
+
__export(exports_doctor, {
|
|
8071
|
+
main: () => main3
|
|
8072
|
+
});
|
|
8073
|
+
async function main3(options, deps = {}) {
|
|
8074
|
+
const result = await (deps.runDoctor ?? runDoctor)(options);
|
|
8075
|
+
const print = deps.print ?? ((line) => console.log(line));
|
|
8076
|
+
for (const line of result.output) {
|
|
8077
|
+
print(line);
|
|
8078
|
+
}
|
|
8079
|
+
return result.exitCode;
|
|
8080
|
+
}
|
|
8081
|
+
var init_doctor2 = __esm(async () => {
|
|
8082
|
+
await init_doctor();
|
|
8083
|
+
});
|
|
8084
|
+
|
|
8085
|
+
// src/cli.ts
|
|
8086
|
+
init_create_daemon_controller();
|
|
8087
|
+
init_daemon_files();
|
|
8088
|
+
import { homedir as homedir9 } from "node:os";
|
|
8089
|
+
import { sep } from "node:path";
|
|
8090
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
6222
8091
|
|
|
6223
8092
|
// src/daemon/daemon-runtime.ts
|
|
8093
|
+
init_daemon_status();
|
|
6224
8094
|
import { mkdir as mkdir4, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
|
|
6225
8095
|
import { dirname as dirname3 } from "node:path";
|
|
8096
|
+
|
|
6226
8097
|
class DaemonRuntime {
|
|
6227
8098
|
paths;
|
|
6228
8099
|
options;
|
|
@@ -6270,6 +8141,8 @@ class DaemonRuntime {
|
|
|
6270
8141
|
}
|
|
6271
8142
|
|
|
6272
8143
|
// src/cli.ts
|
|
8144
|
+
init_version();
|
|
8145
|
+
init_consumer_lock();
|
|
6273
8146
|
var HELP_LINES = [
|
|
6274
8147
|
"用法:",
|
|
6275
8148
|
"weacpx login - 微信登录",
|
|
@@ -6277,13 +8150,26 @@ var HELP_LINES = [
|
|
|
6277
8150
|
"weacpx run - 前台运行",
|
|
6278
8151
|
"weacpx start - 后台启动",
|
|
6279
8152
|
"weacpx status - 查看状态",
|
|
6280
|
-
"weacpx stop - 停止服务"
|
|
8153
|
+
"weacpx stop - 停止服务",
|
|
8154
|
+
"weacpx doctor - 运行诊断",
|
|
8155
|
+
"weacpx version - 查看版本"
|
|
6281
8156
|
];
|
|
6282
8157
|
async function runCli(args, deps = {}) {
|
|
6283
8158
|
const command = args[0];
|
|
6284
8159
|
const print = deps.print ?? ((line) => console.log(line));
|
|
6285
|
-
const controller = deps.controller ?? createDefaultController();
|
|
6286
8160
|
switch (command) {
|
|
8161
|
+
case "version":
|
|
8162
|
+
case "--version":
|
|
8163
|
+
case "-v":
|
|
8164
|
+
print(readVersion());
|
|
8165
|
+
return 0;
|
|
8166
|
+
case "--help":
|
|
8167
|
+
case "-h": {
|
|
8168
|
+
for (const line of HELP_LINES) {
|
|
8169
|
+
print(line);
|
|
8170
|
+
}
|
|
8171
|
+
return 0;
|
|
8172
|
+
}
|
|
6287
8173
|
case "login":
|
|
6288
8174
|
await (deps.login ?? defaultLogin)();
|
|
6289
8175
|
return 0;
|
|
@@ -6293,7 +8179,18 @@ async function runCli(args, deps = {}) {
|
|
|
6293
8179
|
case "run":
|
|
6294
8180
|
await (deps.run ?? defaultRun)();
|
|
6295
8181
|
return 0;
|
|
8182
|
+
case "doctor": {
|
|
8183
|
+
const parsed = parseDoctorArgs(args.slice(1));
|
|
8184
|
+
if (!parsed.ok) {
|
|
8185
|
+
for (const line of HELP_LINES) {
|
|
8186
|
+
print(line);
|
|
8187
|
+
}
|
|
8188
|
+
return 1;
|
|
8189
|
+
}
|
|
8190
|
+
return await (deps.doctor ?? defaultDoctor)(parsed.options);
|
|
8191
|
+
}
|
|
6296
8192
|
case "start": {
|
|
8193
|
+
const controller = deps.controller ?? createDefaultController();
|
|
6297
8194
|
const result = await controller.start();
|
|
6298
8195
|
if (result.state === "already-running") {
|
|
6299
8196
|
print("weacpx 已在后台运行");
|
|
@@ -6305,7 +8202,13 @@ async function runCli(args, deps = {}) {
|
|
|
6305
8202
|
return 0;
|
|
6306
8203
|
}
|
|
6307
8204
|
case "status": {
|
|
8205
|
+
const controller = deps.controller ?? createDefaultController();
|
|
6308
8206
|
const status = await controller.getStatus();
|
|
8207
|
+
if (status.state === "indeterminate") {
|
|
8208
|
+
print("weacpx 进程仍在运行,但状态元数据缺失");
|
|
8209
|
+
print(`PID: ${status.pid}`);
|
|
8210
|
+
return 1;
|
|
8211
|
+
}
|
|
6309
8212
|
if (status.state !== "running") {
|
|
6310
8213
|
print("weacpx 未运行");
|
|
6311
8214
|
return 0;
|
|
@@ -6322,6 +8225,7 @@ async function runCli(args, deps = {}) {
|
|
|
6322
8225
|
return 0;
|
|
6323
8226
|
}
|
|
6324
8227
|
case "stop": {
|
|
8228
|
+
const controller = deps.controller ?? createDefaultController();
|
|
6325
8229
|
const result = await controller.stop();
|
|
6326
8230
|
if (result.detail === "not-running") {
|
|
6327
8231
|
print("weacpx 未运行");
|
|
@@ -6338,8 +8242,8 @@ async function runCli(args, deps = {}) {
|
|
|
6338
8242
|
}
|
|
6339
8243
|
}
|
|
6340
8244
|
async function defaultLogin() {
|
|
6341
|
-
const { main:
|
|
6342
|
-
await
|
|
8245
|
+
const { main: main4 } = await init_login().then(() => exports_login);
|
|
8246
|
+
await main4();
|
|
6343
8247
|
}
|
|
6344
8248
|
async function defaultLogout() {
|
|
6345
8249
|
const { logout: logout2 } = await Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk));
|
|
@@ -6349,40 +8253,85 @@ async function defaultRun() {
|
|
|
6349
8253
|
const [{ buildApp: buildApp2, resolveRuntimePaths: resolveRuntimePaths2 }, { loadWeixinSdk: loadWeixinSdk2 }, { runConsole: runConsole2 }] = await Promise.all([
|
|
6350
8254
|
init_main().then(() => exports_main),
|
|
6351
8255
|
Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk)),
|
|
6352
|
-
Promise.resolve().then(() => exports_run_console)
|
|
8256
|
+
Promise.resolve().then(() => (init_run_console(), exports_run_console))
|
|
6353
8257
|
]);
|
|
6354
8258
|
const runtimePaths = resolveRuntimePaths2();
|
|
6355
8259
|
const daemonPaths = resolveDaemonPaths({ home: requireHome() });
|
|
6356
8260
|
const daemonRuntime = new DaemonRuntime(daemonPaths, { pid: process.pid });
|
|
6357
8261
|
await runConsole2(runtimePaths, {
|
|
6358
8262
|
buildApp: (paths) => buildApp2(paths, {
|
|
6359
|
-
defaultLoggingLevel:
|
|
8263
|
+
defaultLoggingLevel: resolveCliEntryPath2().includes(`${sep}src${sep}`) ? "debug" : "info"
|
|
6360
8264
|
}),
|
|
6361
8265
|
loadWeixinSdk: loadWeixinSdk2,
|
|
6362
|
-
daemonRuntime
|
|
8266
|
+
daemonRuntime,
|
|
8267
|
+
consumerLockFactory: (runtime) => createWeixinConsumerLock({
|
|
8268
|
+
lockFilePath: `${daemonPaths.runtimeDir}${sep}weixin-consumer.lock.json`,
|
|
8269
|
+
onDiagnostic: async (event, context) => {
|
|
8270
|
+
await runtime.logger.info(`weixin.consumer_lock.${event}`, "weixin consumer lock diagnostic", context);
|
|
8271
|
+
}
|
|
8272
|
+
})
|
|
6363
8273
|
});
|
|
6364
8274
|
}
|
|
8275
|
+
async function defaultDoctor(options) {
|
|
8276
|
+
const { main: main4 } = await init_doctor2().then(() => exports_doctor);
|
|
8277
|
+
return await main4(options);
|
|
8278
|
+
}
|
|
6365
8279
|
function createDefaultController() {
|
|
6366
8280
|
const daemonPaths = resolveDaemonPaths({ home: requireHome() });
|
|
6367
8281
|
return createDaemonController(daemonPaths, {
|
|
6368
8282
|
processExecPath: process.execPath,
|
|
6369
|
-
cliEntryPath:
|
|
8283
|
+
cliEntryPath: resolveCliEntryPath2(),
|
|
6370
8284
|
cwd: process.cwd(),
|
|
6371
8285
|
env: process.env
|
|
6372
8286
|
});
|
|
6373
8287
|
}
|
|
6374
8288
|
function requireHome() {
|
|
6375
|
-
const home = process.env.HOME ??
|
|
8289
|
+
const home = process.env.HOME ?? homedir9();
|
|
6376
8290
|
if (!home) {
|
|
6377
8291
|
throw new Error("Unable to resolve the current user home directory");
|
|
6378
8292
|
}
|
|
6379
8293
|
return home;
|
|
6380
8294
|
}
|
|
6381
|
-
function
|
|
8295
|
+
function resolveCliEntryPath2() {
|
|
6382
8296
|
if (process.argv[1]) {
|
|
6383
8297
|
return process.argv[1];
|
|
6384
8298
|
}
|
|
6385
|
-
return
|
|
8299
|
+
return fileURLToPath5(import.meta.url);
|
|
8300
|
+
}
|
|
8301
|
+
function parseDoctorArgs(args) {
|
|
8302
|
+
const options = {};
|
|
8303
|
+
for (let index = 0;index < args.length; index++) {
|
|
8304
|
+
const arg = args[index];
|
|
8305
|
+
switch (arg) {
|
|
8306
|
+
case "--verbose":
|
|
8307
|
+
options.verbose = true;
|
|
8308
|
+
break;
|
|
8309
|
+
case "--smoke":
|
|
8310
|
+
options.smoke = true;
|
|
8311
|
+
break;
|
|
8312
|
+
case "--agent": {
|
|
8313
|
+
const value = args[index + 1];
|
|
8314
|
+
if (!value || value.startsWith("--")) {
|
|
8315
|
+
return { ok: false };
|
|
8316
|
+
}
|
|
8317
|
+
options.agent = value;
|
|
8318
|
+
index++;
|
|
8319
|
+
break;
|
|
8320
|
+
}
|
|
8321
|
+
case "--workspace": {
|
|
8322
|
+
const value = args[index + 1];
|
|
8323
|
+
if (!value || value.startsWith("--")) {
|
|
8324
|
+
return { ok: false };
|
|
8325
|
+
}
|
|
8326
|
+
options.workspace = value;
|
|
8327
|
+
index++;
|
|
8328
|
+
break;
|
|
8329
|
+
}
|
|
8330
|
+
default:
|
|
8331
|
+
return { ok: false };
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
return { ok: true, options };
|
|
6386
8335
|
}
|
|
6387
8336
|
if (__require.main == __require.module) {
|
|
6388
8337
|
process.exitCode = await runCli(process.argv.slice(2));
|