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/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 path from "node:path";
1562
+ import path2 from "node:path";
1070
1563
  function resolveStateDir() {
1071
- return process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
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 fs from "node:fs";
1077
- import path2 from "node:path";
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 path2.join(resolveStateDir(), "openclaw-weixin");
1584
+ return path3.join(resolveStateDir(), "openclaw-weixin");
1092
1585
  }
1093
1586
  function resolveAccountIndexPath() {
1094
- return path2.join(resolveWeixinStateDir(), "accounts.json");
1587
+ return path3.join(resolveWeixinStateDir(), "accounts.json");
1095
1588
  }
1096
1589
  function listIndexedWeixinAccountIds() {
1097
1590
  const filePath = resolveAccountIndexPath();
1098
1591
  try {
1099
- if (!fs.existsSync(filePath))
1592
+ if (!fs2.existsSync(filePath))
1100
1593
  return [];
1101
- const raw = fs.readFileSync(filePath, "utf-8");
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
- fs.mkdirSync(dir, { recursive: true });
1113
- fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify([accountId], null, 2), "utf-8");
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 path2.join(resolveWeixinStateDir(), "accounts");
1609
+ return path3.join(resolveWeixinStateDir(), "accounts");
1117
1610
  }
1118
1611
  function resolveAccountPath(accountId) {
1119
- return path2.join(resolveAccountsDir(), `${accountId}.json`);
1612
+ return path3.join(resolveAccountsDir(), `${accountId}.json`);
1120
1613
  }
1121
1614
  function loadLegacyToken() {
1122
- const legacyPath = path2.join(resolveStateDir(), "credentials", "openclaw-weixin", "credentials.json");
1615
+ const legacyPath = path3.join(resolveStateDir(), "credentials", "openclaw-weixin", "credentials.json");
1123
1616
  try {
1124
- if (!fs.existsSync(legacyPath))
1617
+ if (!fs2.existsSync(legacyPath))
1125
1618
  return;
1126
- const raw = fs.readFileSync(legacyPath, "utf-8");
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 (fs.existsSync(filePath)) {
1136
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
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
- fs.mkdirSync(dir, { recursive: true });
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
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
1662
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
1170
1663
  try {
1171
- fs.chmodSync(filePath, 384);
1664
+ fs2.chmodSync(filePath, 384);
1172
1665
  } catch {}
1173
1666
  }
1174
1667
  function clearWeixinAccount(accountId) {
1175
1668
  try {
1176
- fs.unlinkSync(resolveAccountPath(accountId));
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
- fs.writeFileSync(resolveAccountIndexPath(), "[]", "utf-8");
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 path2.join(resolveStateDir(), "openclaw.json");
1685
+ return path3.join(resolveStateDir(), "openclaw.json");
1193
1686
  }
1194
1687
  function loadConfigRouteTag(accountId) {
1195
1688
  try {
1196
1689
  const configPath = resolveConfigPath();
1197
- if (!fs.existsSync(configPath))
1690
+ if (!fs2.existsSync(configPath))
1198
1691
  return;
1199
- const raw = fs.readFileSync(configPath, "utf-8");
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 fs2 from "node:fs";
1740
+ import fs3 from "node:fs";
1248
1741
  import os2 from "node:os";
1249
- import path3 from "node:path";
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 path3.join(MAIN_LOG_DIR, `openclaw-${dateKey}.log`);
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
- fs2.mkdirSync(MAIN_LOG_DIR, { recursive: true });
1790
+ fs3.mkdirSync(MAIN_LOG_DIR, { recursive: true });
1298
1791
  logDirEnsured = true;
1299
1792
  }
1300
- fs2.appendFileSync(resolveMainLogPath(), `${entry}
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 = path3.join("/tmp", "openclaw");
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 = readChannelVersion();
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(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
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/process-message.ts
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 processOneMessage(full, deps) {
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 reply = async (text) => {
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: markdownToPlainText(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 response = await deps.agent.chat(request);
2964
- if (response.media) {
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 = response.media.url;
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: response.text ? markdownToPlainText(response.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 (response.text) {
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: markdownToPlainText(response.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(`processOneMessage: agent or send failed: ${errorText}`);
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 init_process_message = __esm(() => {
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 processOneMessage(full, {
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
- init_process_message();
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 mkdir5, readdir, rename, rm as rm4, stat } from "node:fs/promises";
3338
- import { basename, dirname as dirname4, join as join2 } from "node:path";
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 mkdir5(dirname4(options.filePath), { recursive: true });
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 rm4(filePath, { force: true });
3917
+ await rm5(filePath, { force: true });
3390
3918
  return;
3391
3919
  }
3392
- await rm4(`${filePath}.${maxFiles}`, { force: true });
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 = dirname4(filePath);
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 = join2(parentDir, file);
3950
+ const candidate = join3(parentDir, file);
3423
3951
  const details = await stat(candidate);
3424
3952
  if (details.mtime.getTime() < cutoff) {
3425
- await rm4(candidate, { force: true });
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 readFile3 } from "node:fs/promises";
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 ?? homedir();
3989
+ const home = process.env.HOME ?? homedir2();
3462
3990
  if (!home) {
3463
3991
  return;
3464
3992
  }
3465
3993
  try {
3466
- const raw = await readFile3(resolve(home, ".acpx", "sessions", "index.json"), "utf8");
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 cwd = readFlagValue(parts, ["--cwd", "-d"]);
3699
- if (cwd) {
3700
- return { kind: "session.shortcut.new", agent: parts[2], cwd };
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 cwd = readFlagValue(parts, ["--cwd", "-d"]);
3705
- if (cwd) {
3706
- return { kind: "session.shortcut", agent: parts[1], cwd };
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 readFlagValue(parts, flags) {
3737
- for (let index = 0;index < parts.length; index += 1) {
3738
- if (flags.includes(parts[index] ?? "")) {
3739
- return parts[index + 1] ?? "";
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
- return "";
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 === "allow" || value === "deny" || value === "fail") {
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 ?? "fail";
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, cwdInput, createNew) {
3893
- return await context.lifecycle.handleSessionShortcut(chatKey, agent, cwdInput, createNew);
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
- `- mode:${session.modeId ?? "未设置"}`
4807
+ `- 全局默认:${globalDefault}`,
4808
+ `- 当前会话覆盖:${sessionOverride ?? "未设置"}`,
4809
+ `- 当前生效:${effective}`
3936
4810
  ].join(`
3937
4811
  `)
3938
4812
  };
3939
4813
  }
3940
- async function handleModeSet(context, chatKey, modeId) {
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.interaction.setModeTransportSession(session, modeId);
3946
- await context.sessions.setCurrentSessionMode(chatKey, modeId);
3947
- return { text: `已设置当前会话 mode:${modeId}` };
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
- const result = await context.interaction.promptTransportSession(session, text, reply);
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
- const result = await context.interaction.promptTransportSession(recovered, text, reply);
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 homedir2 } from "node:os";
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("~") ? homedir2() + path11.slice(1) : path11;
5124
+ const expanded = path11.startsWith("~") ? homedir3() + path11.slice(1) : path11;
4217
5125
  return normalize(expanded);
4218
5126
  }
4219
- var init_workspace_handler = () => {};
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 homedir3 } from "node:os";
4225
- async function handleSessionShortcutCommand(context, ops, chatKey, agent, cwdInput, createNew) {
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
- const cwd = normalizePathForWorkspace2(cwdInput);
4230
- if (!await pathExists2(cwd)) {
4231
- return { text: `工作区路径不存在:${cwdInput}` };
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, `${workspace.name}:${alias}`);
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, cwd) {
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("~") ? homedir3() + path11.slice(1) : path11;
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 renderSessionCreationVerificationError(session);
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
- `请先准备好一个已有会话,然后在微信里执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
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.cwd, false);
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.cwd, true);
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, cwdInput, createNew) => handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(), chatKey, agent, cwdInput, createNew),
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 readFile4 } from "node:fs/promises";
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 readFile4(path11, "utf8"));
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 !== "allow" && transport.nonInteractivePermissions !== "deny" && transport.nonInteractivePermissions !== "fail") {
4758
- throw new Error("transport.nonInteractivePermissions must be allow, deny, or fail");
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 === "allow" || transport.nonInteractivePermissions === "deny" || transport.nonInteractivePermissions === "fail" ? transport.nonInteractivePermissions : DEFAULT_NON_INTERACTIVE_PERMISSIONS;
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 = "fail";
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 mkdir6, writeFile as writeFile4 } from "node:fs/promises";
4847
- import { dirname as dirname5 } from "node:path";
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 mkdir6(dirname5(this.path), { recursive: true });
4859
- await writeFile4(this.path, `${JSON.stringify(config, null, 2)}
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 readFile5 } from "node:fs/promises";
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
- const template = JSON.parse(await readFile5(templatePath, "utf8"));
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 options.configuredCommand;
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 pathApi.resolve(packageDir, binPath);
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 "acpx";
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
- cwd: this.config.workspaces[session.workspace].cwd
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 mkdir7, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
5155
- import { dirname as dirname6 } from "node:path";
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 readFile6(this.path, "utf8");
6303
+ const content = await readFile7(this.path, "utf8");
5165
6304
  if (content.trim() === "") {
5166
6305
  return createEmptyState();
5167
6306
  }
5168
- return JSON.parse(content);
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 mkdir7(dirname6(this.path), { recursive: true });
5178
- await writeFile5(this.path, JSON.stringify(state, null, 2));
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
- this.writeLine(encodeBridgeRequest({
5265
- id,
5266
- method,
5267
- params
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
- const response = JSON.parse(line);
5273
- const pending = this.pending.get(response.id);
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 ?? "fail"
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, _reply) {
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 dirname7, join as join3 } from "node:path";
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 join3(dirname7(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
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 ?? "fail";
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 homedir4 } from "node:os";
5778
- import { dirname as dirname8, join as join4 } from "node:path";
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 ?? homedir4();
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 = dirname8(configPath);
5857
- const runtimeDir = join4(rootDir, "runtime");
5858
- return join4(runtimeDir, "app.log");
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/cli.ts
5877
- import { homedir as homedir5 } from "node:os";
5878
- import { sep } from "node:path";
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
- // src/daemon/create-daemon-controller.ts
5882
- import { mkdir as mkdir3, open } from "node:fs/promises";
5883
- import { spawn } from "node:child_process";
5884
-
5885
- // src/daemon/daemon-controller.ts
5886
- import { mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
5887
- import { dirname as dirname2 } from "node:path";
5888
-
5889
- // src/daemon/daemon-status.ts
5890
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
5891
- import { dirname } from "node:path";
5892
-
5893
- class DaemonStatusStore {
5894
- path;
5895
- constructor(path) {
5896
- this.path = path;
5897
- }
5898
- async load() {
5899
- try {
5900
- const content = await readFile(this.path, "utf8");
5901
- if (content.trim() === "") {
5902
- return null;
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
- return JSON.parse(content);
5905
- } catch (error) {
5906
- if (error.code === "ENOENT") {
5907
- return null;
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
- throw error;
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
- async clear() {
5917
- await rm(this.path, { force: true });
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/daemon/daemon-controller.ts
5922
- class DaemonController {
5923
- paths;
5924
- deps;
5925
- statusStore;
5926
- startupPollIntervalMs;
5927
- startupTimeoutMs;
5928
- onStartupPoll;
5929
- shutdownPollIntervalMs;
5930
- shutdownTimeoutMs;
5931
- onShutdownPoll;
5932
- constructor(paths, deps) {
5933
- this.paths = paths;
5934
- this.deps = deps;
5935
- this.statusStore = new DaemonStatusStore(paths.statusFile);
5936
- this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
5937
- this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
5938
- this.shutdownPollIntervalMs = deps.shutdownPollIntervalMs ?? 50;
5939
- this.shutdownTimeoutMs = deps.shutdownTimeoutMs ?? 5000;
5940
- this.onStartupPoll = deps.onStartupPoll ?? (async () => {
5941
- await new Promise((resolve) => setTimeout(resolve, this.startupPollIntervalMs));
5942
- });
5943
- this.onShutdownPoll = deps.onShutdownPoll ?? (async () => {
5944
- await new Promise((resolve) => setTimeout(resolve, this.shutdownPollIntervalMs));
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
- async getStatus() {
5948
- const pid = await this.loadPid();
5949
- const status = await this.statusStore.load();
5950
- if (!pid) {
5951
- return { state: "stopped" };
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
- if (!this.deps.isProcessRunning(pid)) {
5954
- await this.clearRuntimeFiles();
5955
- return { state: "stopped", stale: true };
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
- if (!status) {
5958
- return { state: "stopped" };
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
- state: "running",
5962
- pid,
5963
- status
7489
+ ok: true,
7490
+ detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
5964
7491
  };
5965
7492
  }
5966
- async start() {
5967
- const current = await this.getStatus();
5968
- if (current.state === "running") {
5969
- return { state: "already-running", pid: current.pid };
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 this.statusStore.clear();
5972
- const pid = await this.deps.spawnDetached();
5973
- await this.writePid(pid);
5974
- await this.waitForStartupMetadata(pid);
5975
- return { state: "started", pid };
5976
- }
5977
- async stop() {
5978
- const pid = await this.loadPid();
5979
- if (!pid) {
5980
- return { state: "stopped", detail: "not-running" };
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
- if (this.deps.isProcessRunning(pid)) {
5983
- await this.deps.terminateProcess(pid);
5984
- await this.waitForShutdown(pid);
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
- await this.clearRuntimeFiles();
5987
- return { state: "stopped", detail: "stopped" };
7522
+ return {
7523
+ ok: true,
7524
+ detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
7525
+ };
5988
7526
  }
5989
- async loadPid() {
5990
- try {
5991
- const content = await readFile2(this.paths.pidFile, "utf8");
5992
- const pid = Number(content.trim());
5993
- return Number.isFinite(pid) && pid > 0 ? pid : null;
5994
- } catch (error) {
5995
- if (error.code === "ENOENT") {
5996
- return null;
5997
- }
5998
- throw error;
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
- async writePid(pid) {
6002
- await mkdir2(dirname2(this.paths.pidFile), { recursive: true });
6003
- await writeFile2(this.paths.pidFile, `${pid}
6004
- `);
6005
- }
6006
- async clearRuntimeFiles() {
6007
- await rm2(this.paths.pidFile, { force: true });
6008
- await this.statusStore.clear();
6009
- }
6010
- async waitForStartupMetadata(pid) {
6011
- const deadline = Date.now() + this.startupTimeoutMs;
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
- throw new Error(`weacpx daemon did not report ready state within ${this.startupTimeoutMs}ms (pid ${pid})`);
6024
- }
6025
- async waitForShutdown(pid) {
6026
- const deadline = Date.now() + this.shutdownTimeoutMs;
6027
- while (Date.now() < deadline) {
6028
- if (!this.deps.isProcessRunning(pid)) {
6029
- return;
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
- if (!this.deps.isProcessRunning(pid)) {
6034
- return;
7559
+ const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
7560
+ if (!parentCheck.ok) {
7561
+ return parentCheck;
6035
7562
  }
6036
- throw new Error(`weacpx daemon did not exit within ${this.shutdownTimeoutMs}ms (pid ${pid})`);
7563
+ return {
7564
+ ok: true,
7565
+ creatableFrom: parentCheck.creatableFrom
7566
+ };
6037
7567
  }
6038
7568
  }
6039
-
6040
- // src/daemon/create-daemon-controller.ts
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 defaultIsProcessRunning(pid) {
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
- process.kill(pid, 0);
6061
- return true;
6062
- } catch {
6063
- return false;
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 buildSpawnRequest(paths, options, stdoutFd, stderrFd) {
6067
- const platform = options.platform ?? process.platform;
6068
- if (platform === "win32") {
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
- mode: "windows-hidden",
6071
- command: "powershell.exe",
6072
- args: [
6073
- "-NoProfile",
6074
- "-NonInteractive",
6075
- "-EncodedCommand",
6076
- buildWindowsLauncherScript()
6077
- ],
6078
- options: {
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
- mode: "direct",
6096
- command: options.processExecPath,
6097
- args: [options.cliEntryPath, "run"],
6098
- options: {
6099
- cwd: options.cwd,
6100
- detached: true,
6101
- env: options.env,
6102
- stdio: ["ignore", stdoutFd, stderrFd]
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 buildWindowsLauncherScript() {
6107
- const script = [
6108
- "$process = Start-Process -FilePath $env:WEACPX_DAEMON_COMMAND `",
6109
- " -ArgumentList @($env:WEACPX_DAEMON_ARG0, $env:WEACPX_DAEMON_ARG1) `",
6110
- " -WorkingDirectory $env:WEACPX_DAEMON_CWD `",
6111
- " -RedirectStandardOutput $env:WEACPX_DAEMON_STDOUT `",
6112
- " -RedirectStandardError $env:WEACPX_DAEMON_STDERR `",
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
- async function defaultSpawnProcess(request) {
6121
- if (request.mode === "windows-hidden") {
6122
- return await spawnWindowsHiddenProcess(request);
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
- const child = spawn(request.command, request.args, request.options);
6125
- child.unref();
6126
- return child.pid ?? 0;
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
- async function spawnWindowsHiddenProcess(request) {
6129
- return await new Promise((resolve, reject) => {
6130
- const child = spawn(request.command, request.args, request.options);
6131
- let stdout = "";
6132
- let settled = false;
6133
- child.stdout?.setEncoding("utf8");
6134
- child.stdout?.on("data", (chunk) => {
6135
- stdout += chunk;
6136
- if (settled) {
6137
- return;
6138
- }
6139
- const pid = Number.parseInt(stdout.trim(), 10);
6140
- if (!Number.isFinite(pid) || pid <= 0) {
6141
- return;
6142
- }
6143
- settled = true;
6144
- child.stdout?.destroy();
6145
- child.unref();
6146
- resolve(pid);
6147
- });
6148
- child.on("error", (error) => {
6149
- if (settled) {
6150
- return;
6151
- }
6152
- settled = true;
6153
- reject(error);
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
- async function defaultTerminateProcess(pid) {
6176
- await terminateProcessTree(pid);
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
- async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand) {
6179
- if (platform === "win32") {
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
- await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
6182
- } catch {}
6183
- return;
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
- try {
6186
- process.kill(pid, "SIGTERM");
6187
- } catch {
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 deadline = Date.now() + 5000;
6191
- while (Date.now() < deadline) {
6192
- if (!defaultIsProcessRunning(pid)) {
6193
- return;
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
- await new Promise((resolve) => setTimeout(resolve, 100));
7886
+ details.push(`account[${entry.accountId}].resolveError: ${entry.error ?? "unknown"}`);
6196
7887
  }
6197
- try {
6198
- process.kill(pid, "SIGKILL");
6199
- } catch {}
7888
+ return details;
6200
7889
  }
6201
- async function defaultRunProcessCommand(command, args) {
6202
- return await new Promise((resolve, reject) => {
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/daemon/daemon-files.ts
6210
- import { join } from "node:path";
6211
- function resolveDaemonPaths(options) {
6212
- const runtimeDir = options.runtimeDir ?? join(options.home, ".weacpx", "runtime");
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
- runtimeDir,
6215
- pidFile: join(runtimeDir, "daemon.pid"),
6216
- statusFile: join(runtimeDir, "status.json"),
6217
- stdoutLog: join(runtimeDir, "stdout.log"),
6218
- stderrLog: join(runtimeDir, "stderr.log"),
6219
- appLog: join(runtimeDir, "app.log")
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: main3 } = await init_login().then(() => exports_login);
6342
- await main3();
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: resolveCliEntryPath().includes(`${sep}src${sep}`) ? "debug" : "info"
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: resolveCliEntryPath(),
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 ?? homedir5();
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 resolveCliEntryPath() {
8295
+ function resolveCliEntryPath2() {
6382
8296
  if (process.argv[1]) {
6383
8297
  return process.argv[1];
6384
8298
  }
6385
- return fileURLToPath4(import.meta.url);
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));