weacpx 0.1.6 → 0.2.0

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
@@ -47,6 +47,129 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
49
49
 
50
+ // src/weixin/monitor/consumer-lock.ts
51
+ import { mkdir as mkdir5, open as open2, readFile as readFile3, rm as rm4 } from "node:fs/promises";
52
+ import { dirname as dirname4, join as join2 } from "node:path";
53
+ import { homedir } from "node:os";
54
+ function createWeixinConsumerLock(options = {}) {
55
+ const lockFilePath = options.lockFilePath ?? join2(homedir(), ".weacpx", "runtime", "weixin-consumer.lock.json");
56
+ const isProcessRunning = options.isProcessRunning ?? defaultIsProcessRunning2;
57
+ const onDiagnostic = options.onDiagnostic;
58
+ return {
59
+ async acquire(meta) {
60
+ await mkdir5(dirname4(lockFilePath), { recursive: true });
61
+ while (true) {
62
+ try {
63
+ const handle = await open2(lockFilePath, "wx");
64
+ try {
65
+ await handle.writeFile(`${JSON.stringify(meta, null, 2)}
66
+ `, "utf8");
67
+ } finally {
68
+ await handle.close();
69
+ }
70
+ await onDiagnostic?.("lock_acquired", {
71
+ lockFilePath,
72
+ pid: meta.pid,
73
+ mode: meta.mode,
74
+ configPath: meta.configPath,
75
+ statePath: meta.statePath,
76
+ hostname: meta.hostname
77
+ });
78
+ return;
79
+ } catch (error) {
80
+ const code = error.code;
81
+ if (code !== "EEXIST") {
82
+ throw error;
83
+ }
84
+ await onDiagnostic?.("lock_exists", {
85
+ lockFilePath,
86
+ pid: meta.pid,
87
+ mode: meta.mode
88
+ });
89
+ const existing = await loadLockMetadata(lockFilePath);
90
+ if (!existing) {
91
+ await rm4(lockFilePath, { force: true });
92
+ await onDiagnostic?.("lock_invalid_removed", {
93
+ lockFilePath,
94
+ reason: "invalid_or_unreadable_metadata"
95
+ });
96
+ continue;
97
+ }
98
+ if (!isProcessRunning(existing.pid)) {
99
+ await rm4(lockFilePath, { force: true });
100
+ await onDiagnostic?.("lock_stale_removed", {
101
+ lockFilePath,
102
+ stalePid: existing.pid,
103
+ staleMode: existing.mode,
104
+ staleConfigPath: existing.configPath,
105
+ staleStatePath: existing.statePath,
106
+ reason: "owner_process_not_running"
107
+ });
108
+ continue;
109
+ }
110
+ await onDiagnostic?.("lock_active_conflict", {
111
+ lockFilePath,
112
+ activePid: existing.pid,
113
+ activeMode: existing.mode,
114
+ activeConfigPath: existing.configPath,
115
+ activeStatePath: existing.statePath,
116
+ requestedPid: meta.pid,
117
+ requestedMode: meta.mode
118
+ });
119
+ throw new ActiveWeixinConsumerLockError(lockFilePath, existing);
120
+ }
121
+ }
122
+ },
123
+ async release() {
124
+ await rm4(lockFilePath, { force: true });
125
+ await onDiagnostic?.("lock_released", {
126
+ lockFilePath
127
+ });
128
+ }
129
+ };
130
+ }
131
+ async function loadLockMetadata(path) {
132
+ try {
133
+ const raw = await readFile3(path, "utf8");
134
+ const parsed = JSON.parse(raw);
135
+ if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
136
+ return null;
137
+ }
138
+ return parsed;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ function defaultIsProcessRunning2(pid) {
144
+ try {
145
+ process.kill(pid, 0);
146
+ return true;
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+ var ActiveWeixinConsumerLockError;
152
+ var init_consumer_lock = __esm(() => {
153
+ ActiveWeixinConsumerLockError = class ActiveWeixinConsumerLockError extends Error {
154
+ existing;
155
+ lockFilePath;
156
+ constructor(lockFilePath, existing) {
157
+ super([
158
+ "weacpx Weixin consumer is already running.",
159
+ `pid: ${existing.pid}`,
160
+ `mode: ${existing.mode}`,
161
+ `config: ${existing.configPath}`,
162
+ `state: ${existing.statePath}`,
163
+ "Try stopping the existing instance or close the foreground `weacpx run` process before starting a new one."
164
+ ].join(`
165
+ `));
166
+ this.name = "ActiveWeixinConsumerLockError";
167
+ this.lockFilePath = lockFilePath;
168
+ this.existing = existing;
169
+ }
170
+ };
171
+ });
172
+
50
173
  // node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
51
174
  var require_QRMode = __commonJS((exports, module) => {
52
175
  module.exports = {
@@ -2428,6 +2551,25 @@ var init_media_download = __esm(() => {
2428
2551
  WEIXIN_MEDIA_MAX_BYTES = 100 * 1024 * 1024;
2429
2552
  });
2430
2553
 
2554
+ // src/weixin/messaging/execute-chat-turn.ts
2555
+ async function executeChatTurn(params) {
2556
+ let usedReply = false;
2557
+ const response = await params.agent.chat({
2558
+ ...params.request,
2559
+ reply: async (text) => {
2560
+ const delivered = await params.onReplySegment?.(text);
2561
+ if (delivered !== false) {
2562
+ usedReply = true;
2563
+ }
2564
+ }
2565
+ });
2566
+ return {
2567
+ text: usedReply ? undefined : response.text,
2568
+ media: response.media,
2569
+ usedReply
2570
+ };
2571
+ }
2572
+
2431
2573
  // src/weixin/messaging/inbound.ts
2432
2574
  function contextTokenKey(accountId, userId) {
2433
2575
  return `${accountId}:${userId}`;
@@ -2826,23 +2968,29 @@ var init_slash_commands = __esm(() => {
2826
2968
  init_send();
2827
2969
  });
2828
2970
 
2829
- // src/weixin/messaging/process-message.ts
2971
+ // src/weixin/messaging/handle-weixin-message-turn.ts
2830
2972
  import crypto4 from "node:crypto";
2831
2973
  import fs6 from "node:fs/promises";
2974
+ import { tmpdir } from "node:os";
2832
2975
  import path9 from "node:path";
2833
- async function saveMediaBuffer(buffer, contentType, subdir, _maxBytes, originalFilename) {
2834
- const dir = path9.join(MEDIA_TEMP_DIR, subdir ?? "");
2835
- await fs6.mkdir(dir, { recursive: true });
2836
- let ext = ".bin";
2837
- if (originalFilename) {
2838
- ext = path9.extname(originalFilename) || ".bin";
2839
- } else if (contentType) {
2840
- ext = getExtensionFromMime(contentType);
2841
- }
2842
- const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
2843
- const filePath = path9.join(dir, name);
2844
- await fs6.writeFile(filePath, buffer);
2845
- return { path: filePath };
2976
+ function resolveMediaTempDir(customRoot) {
2977
+ return customRoot ?? path9.join(tmpdir(), "weacpx", "media");
2978
+ }
2979
+ function createSaveMediaBuffer(mediaTempDir) {
2980
+ return async function saveMediaBuffer(buffer, contentType, subdir, _maxBytes, originalFilename) {
2981
+ const dir = path9.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
2982
+ await fs6.mkdir(dir, { recursive: true });
2983
+ let ext = ".bin";
2984
+ if (originalFilename) {
2985
+ ext = path9.extname(originalFilename) || ".bin";
2986
+ } else if (contentType) {
2987
+ ext = getExtensionFromMime(contentType);
2988
+ }
2989
+ const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
2990
+ const filePath = path9.join(dir, name);
2991
+ await fs6.writeFile(filePath, buffer);
2992
+ return { path: filePath };
2993
+ };
2846
2994
  }
2847
2995
  function extractTextBody(itemList) {
2848
2996
  if (!itemList?.length)
@@ -2857,13 +3005,13 @@ function extractTextBody(itemList) {
2857
3005
  function findMediaItem(itemList) {
2858
3006
  if (!itemList?.length)
2859
3007
  return;
2860
- const direct = itemList.find((i) => i.type === MessageItemType.IMAGE && hasDownloadableMedia(i.image_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.VIDEO && hasDownloadableMedia(i.video_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.FILE && hasDownloadableMedia(i.file_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.VOICE && hasDownloadableMedia(i.voice_item?.media) && !i.voice_item?.text);
3008
+ const direct = itemList.find((item) => item.type === MessageItemType.IMAGE && hasDownloadableMedia(item.image_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.VIDEO && hasDownloadableMedia(item.video_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.FILE && hasDownloadableMedia(item.file_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.VOICE && hasDownloadableMedia(item.voice_item?.media) && !item.voice_item?.text);
2861
3009
  if (direct)
2862
3010
  return direct;
2863
- const refItem = itemList.find((i) => i.type === MessageItemType.TEXT && i.ref_msg?.message_item && isMediaItem(i.ref_msg.message_item));
3011
+ const refItem = itemList.find((item) => item.type === MessageItemType.TEXT && item.ref_msg?.message_item && isMediaItem(item.ref_msg.message_item));
2864
3012
  return refItem?.ref_msg?.message_item ?? undefined;
2865
3013
  }
2866
- async function processOneMessage(full, deps) {
3014
+ async function handleWeixinMessageTurn(full, deps) {
2867
3015
  const receivedAt = Date.now();
2868
3016
  const textBody = extractTextBody(full.item_list);
2869
3017
  if (textBody.startsWith("/")) {
@@ -2891,7 +3039,7 @@ async function processOneMessage(full, deps) {
2891
3039
  try {
2892
3040
  const downloaded = await downloadMediaFromItem(mediaItem, {
2893
3041
  cdnBaseUrl: deps.cdnBaseUrl,
2894
- saveMedia: saveMediaBuffer,
3042
+ saveMedia: createSaveMediaBuffer(deps.mediaTempDir),
2895
3043
  log: deps.log,
2896
3044
  errLog: deps.errLog,
2897
3045
  label: "inbound"
@@ -2914,26 +3062,31 @@ async function processOneMessage(full, deps) {
2914
3062
  };
2915
3063
  }
2916
3064
  } catch (err) {
2917
- logger.error(`media download failed: ${String(err)}`);
3065
+ deps.errLog(`media download failed: ${String(err)}`);
2918
3066
  }
2919
3067
  }
2920
3068
  const to = full.from_user_id ?? "";
2921
- const reply = async (text) => {
3069
+ const sendReplySegment = async (text) => {
3070
+ const plainText = markdownToPlainText(text).trim();
3071
+ if (plainText.length === 0) {
3072
+ return false;
3073
+ }
2922
3074
  try {
2923
3075
  await sendMessageWeixin({
2924
3076
  to,
2925
- text: markdownToPlainText(text),
3077
+ text: plainText,
2926
3078
  opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
2927
3079
  });
3080
+ return true;
2928
3081
  } catch (err) {
2929
- logger.error(`intermediate reply failed: ${String(err)}`);
3082
+ deps.errLog(`intermediate reply failed: ${String(err)}`);
3083
+ return false;
2930
3084
  }
2931
3085
  };
2932
3086
  const request = {
2933
3087
  conversationId: full.from_user_id ?? "",
2934
3088
  text: bodyFromItemList(full.item_list),
2935
- media,
2936
- reply
3089
+ media
2937
3090
  };
2938
3091
  let typingTimer;
2939
3092
  const startTyping = () => {
@@ -2954,31 +3107,40 @@ async function processOneMessage(full, deps) {
2954
3107
  typingTimer = setInterval(startTyping, 1e4);
2955
3108
  }
2956
3109
  try {
2957
- const response = await deps.agent.chat(request);
2958
- if (response.media) {
3110
+ const turn = await executeChatTurn({
3111
+ agent: deps.agent,
3112
+ request,
3113
+ onReplySegment: sendReplySegment
3114
+ });
3115
+ if (turn.media) {
2959
3116
  let filePath;
2960
- const mediaUrl = response.media.url;
3117
+ const mediaUrl = turn.media.url;
2961
3118
  if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
2962
- filePath = await downloadRemoteImageToTemp(mediaUrl, path9.join(MEDIA_TEMP_DIR, "outbound"));
3119
+ filePath = await downloadRemoteImageToTemp(mediaUrl, path9.join(resolveMediaTempDir(deps.mediaTempDir), "outbound"));
2963
3120
  } else {
2964
3121
  filePath = path9.isAbsolute(mediaUrl) ? mediaUrl : path9.resolve(mediaUrl);
2965
3122
  }
2966
3123
  await sendWeixinMediaFile({
2967
3124
  filePath,
2968
3125
  to,
2969
- text: response.text ? markdownToPlainText(response.text) : "",
3126
+ text: turn.text ? markdownToPlainText(turn.text) : "",
2970
3127
  opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
2971
3128
  cdnBaseUrl: deps.cdnBaseUrl
2972
3129
  });
2973
- } else if (response.text) {
3130
+ } else if (turn.text) {
3131
+ const finalText = markdownToPlainText(turn.text).trim();
3132
+ if (finalText.length === 0) {
3133
+ return;
3134
+ }
2974
3135
  await sendMessageWeixin({
2975
3136
  to,
2976
- text: markdownToPlainText(response.text),
3137
+ text: finalText,
2977
3138
  opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
2978
3139
  });
2979
3140
  }
2980
3141
  } catch (err) {
2981
- logger.error(`processOneMessage: agent or send failed: ${err instanceof Error ? err.stack ?? err.message : JSON.stringify(err)}`);
3142
+ const errorText = err instanceof Error ? err.stack ?? err.message : JSON.stringify(err);
3143
+ deps.errLog(`handleWeixinMessageTurn: agent or send failed: ${errorText}`);
2982
3144
  sendWeixinErrorNotice({
2983
3145
  to,
2984
3146
  contextToken,
@@ -3003,14 +3165,13 @@ async function processOneMessage(full, deps) {
3003
3165
  }
3004
3166
  }
3005
3167
  }
3006
- var MEDIA_TEMP_DIR = "/tmp/weixin-agent/media", hasDownloadableMedia = (m) => m?.encrypt_query_param || m?.full_url;
3007
- var init_process_message = __esm(() => {
3168
+ var hasDownloadableMedia = (media) => media?.encrypt_query_param || media?.full_url;
3169
+ var init_handle_weixin_message_turn = __esm(() => {
3008
3170
  init_api();
3009
3171
  init_types();
3010
3172
  init_upload();
3011
3173
  init_media_download();
3012
3174
  init_mime();
3013
- init_logger();
3014
3175
  init_inbound();
3015
3176
  init_error_notice();
3016
3177
  init_send_media();
@@ -3138,7 +3299,7 @@ async function monitorWeixinProvider(opts) {
3138
3299
  aLog.info(`inbound: from=${full.from_user_id} types=${full.item_list?.map((i) => i.type).join(",") ?? "none"}`);
3139
3300
  const fromUserId = full.from_user_id ?? "";
3140
3301
  const cachedConfig = await configManager.getForUser(fromUserId, full.context_token);
3141
- await processOneMessage(full, {
3302
+ await handleWeixinMessageTurn(full, {
3142
3303
  accountId,
3143
3304
  agent,
3144
3305
  baseUrl,
@@ -3180,7 +3341,7 @@ var init_monitor = __esm(() => {
3180
3341
  init_api();
3181
3342
  init_config_cache();
3182
3343
  init_session_guard();
3183
- init_process_message();
3344
+ init_handle_weixin_message_turn();
3184
3345
  init_sync_buf();
3185
3346
  init_logger();
3186
3347
  });
@@ -3327,93 +3488,9 @@ var init_weixin_sdk = __esm(() => {
3327
3488
  init_weixin();
3328
3489
  });
3329
3490
 
3330
- // src/config/agent-templates.ts
3331
- function getAgentTemplate(name) {
3332
- const template = TEMPLATES[name];
3333
- if (!template) {
3334
- return null;
3335
- }
3336
- return {
3337
- ...template
3338
- };
3339
- }
3340
- function listAgentTemplates() {
3341
- return Object.keys(TEMPLATES);
3342
- }
3343
- var TEMPLATES;
3344
- var init_agent_templates = __esm(() => {
3345
- TEMPLATES = {
3346
- codex: {
3347
- driver: "codex"
3348
- },
3349
- claude: {
3350
- driver: "claude"
3351
- }
3352
- };
3353
- });
3354
-
3355
- // src/formatting/render-text.ts
3356
- function renderHelpText() {
3357
- return [
3358
- "可用命令:",
3359
- "",
3360
- "先看这 3 个:",
3361
- "/ss new <agent> -d <path> - 新建会话",
3362
- "/use <alias> - 切会话",
3363
- "/status - 看状态",
3364
- "",
3365
- "Agent:",
3366
- "/agents - 看 Agent",
3367
- "/agent add <codex|claude> - 加 Agent",
3368
- "/agent rm <name> - 删 Agent",
3369
- "",
3370
- "工作区:",
3371
- "/workspaces - 看工作区",
3372
- "/workspace 或 /ws - 工作区命令",
3373
- "/ws new <name> -d <path> - 加工作区",
3374
- "/workspace rm <name> - 删工作区",
3375
- "",
3376
- "会话:",
3377
- "/sessions - 看会话",
3378
- "/session 或 /ss - 会话命令",
3379
- "/ss <agent> -d <path> - 快速新建",
3380
- "/ss new <agent> -d <path> - 新建会话",
3381
- "/ss new <alias> -a <name> --ws <name> - 指定配置新建",
3382
- "/ss attach <alias> -a <name> --ws <name> --name <transport-session> - 挂已有会话",
3383
- "/use <alias> - 切会话",
3384
- "/session reset 或 /clear - 清上下文",
3385
- "",
3386
- "权限:",
3387
- "/pm 或 /permission - 权限设置",
3388
- "/pm set <allow|read|deny> - 设审批级别",
3389
- "/pm auto [allow|deny|fail] - 设自动处理",
3390
- "",
3391
- "常用:",
3392
- "/status - 看状态",
3393
- "/cancel 或 /stop - 停当前任务"
3394
- ].join(`
3395
- `);
3396
- }
3397
- function renderAgents(config) {
3398
- const names = Object.keys(config.agents);
3399
- if (names.length === 0) {
3400
- return "还没有注册任何 Agent。";
3401
- }
3402
- return ["已注册的 Agent:", ...names.map((name) => `- ${name}`)].join(`
3403
- `);
3404
- }
3405
- function renderWorkspaces(config) {
3406
- const names = Object.entries(config.workspaces);
3407
- if (names.length === 0) {
3408
- return "还没有注册任何工作区。";
3409
- }
3410
- return ["已注册的工作区:", ...names.map(([name, workspace]) => `- ${name}: ${workspace.cwd}`)].join(`
3411
- `);
3412
- }
3413
-
3414
3491
  // src/logging/app-logger.ts
3415
- import { appendFile, mkdir as mkdir5, readdir, rename, rm as rm4, stat } from "node:fs/promises";
3416
- import { basename, dirname as dirname4, join as join2 } from "node:path";
3492
+ import { appendFile, mkdir as mkdir6, readdir, rename, rm as rm5, stat } from "node:fs/promises";
3493
+ import { basename, dirname as dirname5, join as join3 } from "node:path";
3417
3494
  function createNoopAppLogger() {
3418
3495
  return {
3419
3496
  debug: async () => {},
@@ -3443,7 +3520,7 @@ function createAppLogger(options) {
3443
3520
  return;
3444
3521
  }
3445
3522
  const line = formatLogLine(now(), level, event, message, context);
3446
- await mkdir5(dirname4(options.filePath), { recursive: true });
3523
+ await mkdir6(dirname5(options.filePath), { recursive: true });
3447
3524
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
3448
3525
  await appendFile(options.filePath, line, "utf8");
3449
3526
  }
@@ -3464,10 +3541,10 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
3464
3541
  return;
3465
3542
  }
3466
3543
  if (maxFiles <= 0) {
3467
- await rm4(filePath, { force: true });
3544
+ await rm5(filePath, { force: true });
3468
3545
  return;
3469
3546
  }
3470
- await rm4(`${filePath}.${maxFiles}`, { force: true });
3547
+ await rm5(`${filePath}.${maxFiles}`, { force: true });
3471
3548
  for (let index = maxFiles - 1;index >= 1; index -= 1) {
3472
3549
  const source = `${filePath}.${index}`;
3473
3550
  try {
@@ -3481,7 +3558,7 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
3481
3558
  await rename(filePath, `${filePath}.1`);
3482
3559
  }
3483
3560
  async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
3484
- const parentDir = dirname4(filePath);
3561
+ const parentDir = dirname5(filePath);
3485
3562
  const prefix = `${basename(filePath)}.`;
3486
3563
  const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
3487
3564
  let files = [];
@@ -3497,10 +3574,10 @@ async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
3497
3574
  if (!file.startsWith(prefix) || !/^\d+$/.test(file.slice(prefix.length))) {
3498
3575
  continue;
3499
3576
  }
3500
- const candidate = join2(parentDir, file);
3577
+ const candidate = join3(parentDir, file);
3501
3578
  const details = await stat(candidate);
3502
3579
  if (details.mtime.getTime() < cutoff) {
3503
- await rm4(candidate, { force: true });
3580
+ await rm5(candidate, { force: true });
3504
3581
  }
3505
3582
  }
3506
3583
  }
@@ -3532,16 +3609,16 @@ var init_app_logger = __esm(() => {
3532
3609
  });
3533
3610
 
3534
3611
  // src/transport/acpx-session-index.ts
3535
- import { readFile as readFile3 } from "node:fs/promises";
3536
- import { homedir } from "node:os";
3612
+ import { readFile as readFile4 } from "node:fs/promises";
3613
+ import { homedir as homedir2 } from "node:os";
3537
3614
  import { resolve } from "node:path";
3538
3615
  async function resolveSessionAgentCommandFromIndex(session) {
3539
- const home = process.env.HOME ?? homedir();
3616
+ const home = process.env.HOME ?? homedir2();
3540
3617
  if (!home) {
3541
3618
  return;
3542
3619
  }
3543
3620
  try {
3544
- const raw = await readFile3(resolve(home, ".acpx", "sessions", "index.json"), "utf8");
3621
+ const raw = await readFile4(resolve(home, ".acpx", "sessions", "index.json"), "utf8");
3545
3622
  const parsed = JSON.parse(raw);
3546
3623
  const targetCwd = resolve(session.cwd);
3547
3624
  const match = parsed.entries?.find((entry) => entry.name === session.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
@@ -3692,8 +3769,10 @@ function parseCommand(input) {
3692
3769
  }
3693
3770
  const parts = tokenizeCommand(trimmed);
3694
3771
  const command = normalizeCommand(parts[0] ?? "");
3695
- if (command === "/help")
3772
+ if (command === "/help" && parts.length === 1)
3696
3773
  return { kind: "help" };
3774
+ if (command === "/help" && parts.length === 2)
3775
+ return { kind: "help", topic: parts[1] };
3697
3776
  if (command === "/agents")
3698
3777
  return { kind: "agents" };
3699
3778
  if (command === "/workspaces")
@@ -3708,6 +3787,10 @@ function parseCommand(input) {
3708
3787
  return { kind: "session.reset" };
3709
3788
  if (command === "/mode" && parts.length === 1)
3710
3789
  return { kind: "mode.show" };
3790
+ if (command === "/replymode" && parts.length === 1)
3791
+ return { kind: "replymode.show" };
3792
+ if (command === "/config" && parts.length === 1)
3793
+ return { kind: "config.show" };
3711
3794
  if (command === "/permission" && parts.length === 1)
3712
3795
  return { kind: "permission.status" };
3713
3796
  if (command === "/session" && parts.length === 1)
@@ -3731,12 +3814,21 @@ function parseCommand(input) {
3731
3814
  return { kind: "permission.auto.set", policy };
3732
3815
  }
3733
3816
  }
3817
+ if (command === "/config" && parts[1] === "set" && parts.length === 4) {
3818
+ return { kind: "config.set", path: parts[2] ?? "", value: parts[3] ?? "" };
3819
+ }
3734
3820
  if (command === "/use" && parts[1]) {
3735
3821
  return { kind: "session.use", alias: parts[1] };
3736
3822
  }
3737
3823
  if (command === "/mode" && parts[1]) {
3738
3824
  return { kind: "mode.set", modeId: parts[1] };
3739
3825
  }
3826
+ if (command === "/replymode" && parts[1] === "reset" && parts.length === 2) {
3827
+ return { kind: "replymode.reset" };
3828
+ }
3829
+ if (command === "/replymode" && (parts[1] === "stream" || parts[1] === "final") && parts.length === 2) {
3830
+ return { kind: "replymode.set", replyMode: parts[1] };
3831
+ }
3740
3832
  if (command === "/agent" && parts[1] === "add" && parts[2]) {
3741
3833
  return { kind: "agent.add", template: parts[2] };
3742
3834
  }
@@ -3746,13 +3838,23 @@ function parseCommand(input) {
3746
3838
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
3747
3839
  const name = parts[2];
3748
3840
  let cwd = "";
3841
+ let invalid = false;
3749
3842
  for (let index = 3;index < parts.length; index += 1) {
3750
3843
  if (parts[index] === "--cwd" || parts[index] === "-d") {
3844
+ if (index + 1 >= parts.length) {
3845
+ invalid = true;
3846
+ break;
3847
+ }
3751
3848
  cwd = parts[index + 1] ?? "";
3752
3849
  index += 1;
3850
+ continue;
3753
3851
  }
3852
+ invalid = true;
3853
+ break;
3854
+ }
3855
+ if (!invalid && name.trim().length > 0 && cwd.trim().length > 0) {
3856
+ return { kind: "workspace.new", name, cwd };
3754
3857
  }
3755
- return { kind: "workspace.new", name, cwd };
3756
3858
  }
3757
3859
  if (command === "/workspace" && parts[1] === "rm" && parts[2]) {
3758
3860
  return { kind: "workspace.rm", name: parts[2] };
@@ -3762,26 +3864,41 @@ function parseCommand(input) {
3762
3864
  const alias = parts[2];
3763
3865
  let agent = "";
3764
3866
  let workspace = "";
3867
+ let invalid = false;
3765
3868
  for (let index = 3;index < parts.length; index += 1) {
3766
3869
  if (parts[index] === "--agent" || parts[index] === "-a") {
3870
+ if (index + 1 >= parts.length) {
3871
+ invalid = true;
3872
+ break;
3873
+ }
3767
3874
  agent = parts[index + 1] ?? "";
3768
3875
  index += 1;
3876
+ continue;
3769
3877
  } else if (parts[index] === "--ws" || parts[index] === "-ws") {
3878
+ if (index + 1 >= parts.length) {
3879
+ invalid = true;
3880
+ break;
3881
+ }
3770
3882
  workspace = parts[index + 1] ?? "";
3771
3883
  index += 1;
3884
+ continue;
3772
3885
  }
3886
+ invalid = true;
3887
+ break;
3888
+ }
3889
+ if (!invalid && alias.trim().length > 0 && agent.trim().length > 0 && workspace.trim().length > 0) {
3890
+ return { kind: "session.new", alias, agent, workspace };
3773
3891
  }
3774
- return { kind: "session.new", alias, agent, workspace };
3775
3892
  }
3776
- const cwd = readFlagValue(parts, ["--cwd", "-d"]);
3777
- if (cwd) {
3778
- return { kind: "session.shortcut.new", agent: parts[2], cwd };
3893
+ const shortcutTarget = readSessionShortcutTarget(parts, 3);
3894
+ if (shortcutTarget) {
3895
+ return { kind: "session.shortcut.new", agent: parts[2], ...shortcutTarget };
3779
3896
  }
3780
3897
  }
3781
3898
  if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach" && parts[1] !== "reset") {
3782
- const cwd = readFlagValue(parts, ["--cwd", "-d"]);
3783
- if (cwd) {
3784
- return { kind: "session.shortcut", agent: parts[1], cwd };
3899
+ const shortcutTarget = readSessionShortcutTarget(parts, 2);
3900
+ if (shortcutTarget) {
3901
+ return { kind: "session.shortcut", agent: parts[1], ...shortcutTarget };
3785
3902
  }
3786
3903
  }
3787
3904
  if (command === "/session" && parts[1] === "attach" && parts[2]) {
@@ -3789,19 +3906,39 @@ function parseCommand(input) {
3789
3906
  let agent = "";
3790
3907
  let workspace = "";
3791
3908
  let transportSession = "";
3909
+ let invalid = false;
3792
3910
  for (let index = 3;index < parts.length; index += 1) {
3793
3911
  if (parts[index] === "--agent" || parts[index] === "-a") {
3912
+ if (index + 1 >= parts.length) {
3913
+ invalid = true;
3914
+ break;
3915
+ }
3794
3916
  agent = parts[index + 1] ?? "";
3795
3917
  index += 1;
3918
+ continue;
3796
3919
  } else if (parts[index] === "--ws" || parts[index] === "-ws") {
3920
+ if (index + 1 >= parts.length) {
3921
+ invalid = true;
3922
+ break;
3923
+ }
3797
3924
  workspace = parts[index + 1] ?? "";
3798
3925
  index += 1;
3926
+ continue;
3799
3927
  } else if (parts[index] === "--name") {
3928
+ if (index + 1 >= parts.length) {
3929
+ invalid = true;
3930
+ break;
3931
+ }
3800
3932
  transportSession = parts[index + 1] ?? "";
3801
3933
  index += 1;
3934
+ continue;
3802
3935
  }
3936
+ invalid = true;
3937
+ break;
3938
+ }
3939
+ if (!invalid && alias.trim().length > 0 && agent.trim().length > 0 && workspace.trim().length > 0 && transportSession.trim().length > 0) {
3940
+ return { kind: "session.attach", alias, agent, workspace, transportSession };
3803
3941
  }
3804
- return { kind: "session.attach", alias, agent, workspace, transportSession };
3805
3942
  }
3806
3943
  if (command.startsWith("/") && isRecognizedCommand(command)) {
3807
3944
  return { kind: "invalid", text: trimmed, recognizedCommand: command };
@@ -3811,13 +3948,42 @@ function parseCommand(input) {
3811
3948
  function hasAnyFlag(parts, flags) {
3812
3949
  return parts.some((part) => flags.includes(part));
3813
3950
  }
3814
- function readFlagValue(parts, flags) {
3815
- for (let index = 0;index < parts.length; index += 1) {
3816
- if (flags.includes(parts[index] ?? "")) {
3817
- return parts[index + 1] ?? "";
3951
+ function readSessionShortcutTarget(parts, startIndex) {
3952
+ let cwd = "";
3953
+ let workspace = "";
3954
+ let invalid = false;
3955
+ for (let index = startIndex;index < parts.length; index += 1) {
3956
+ if (parts[index] === "--cwd" || parts[index] === "-d") {
3957
+ if (index + 1 >= parts.length || workspace) {
3958
+ invalid = true;
3959
+ break;
3960
+ }
3961
+ cwd = parts[index + 1] ?? "";
3962
+ index += 1;
3963
+ continue;
3964
+ }
3965
+ if (parts[index] === "--ws" || parts[index] === "-ws") {
3966
+ if (index + 1 >= parts.length || cwd) {
3967
+ invalid = true;
3968
+ break;
3969
+ }
3970
+ workspace = parts[index + 1] ?? "";
3971
+ index += 1;
3972
+ continue;
3818
3973
  }
3974
+ invalid = true;
3975
+ break;
3819
3976
  }
3820
- return "";
3977
+ if (invalid) {
3978
+ return null;
3979
+ }
3980
+ if (cwd.trim().length > 0) {
3981
+ return { cwd };
3982
+ }
3983
+ if (workspace.trim().length > 0) {
3984
+ return { workspace };
3985
+ }
3986
+ return null;
3821
3987
  }
3822
3988
  function normalizeCommand(command) {
3823
3989
  if (command === "/ss")
@@ -3843,7 +4009,7 @@ function toPermissionMode(value) {
3843
4009
  return null;
3844
4010
  }
3845
4011
  function toNonInteractivePermission(value) {
3846
- if (value === "allow" || value === "deny" || value === "fail") {
4012
+ if (value === "deny" || value === "fail") {
3847
4013
  return value;
3848
4014
  }
3849
4015
  return null;
@@ -3890,6 +4056,8 @@ var init_parse_command = __esm(() => {
3890
4056
  "/cancel",
3891
4057
  "/clear",
3892
4058
  "/mode",
4059
+ "/replymode",
4060
+ "/config",
3893
4061
  "/permission",
3894
4062
  "/session",
3895
4063
  "/workspace",
@@ -3898,431 +4066,1228 @@ var init_parse_command = __esm(() => {
3898
4066
  ]);
3899
4067
  });
3900
4068
 
3901
- // src/commands/command-router.ts
3902
- import { access } from "node:fs/promises";
3903
- import { basename as basename2, normalize } from "node:path";
3904
- import { homedir as homedir2 } from "node:os";
4069
+ // src/commands/config-clone.ts
4070
+ function cloneAppConfig(config) {
4071
+ return {
4072
+ transport: { ...config.transport },
4073
+ logging: { ...config.logging },
4074
+ wechat: { ...config.wechat },
4075
+ agents: Object.fromEntries(Object.entries(config.agents).map(([name, agent]) => [name, { ...agent }])),
4076
+ workspaces: Object.fromEntries(Object.entries(config.workspaces).map(([name, workspace]) => [name, { ...workspace }]))
4077
+ };
4078
+ }
3905
4079
 
3906
- class CommandRouter {
3907
- sessions;
3908
- transport;
3909
- config;
3910
- configStore;
3911
- resolveSessionAgentCommand;
3912
- logger;
3913
- constructor(sessions, transport, config, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex) {
3914
- this.sessions = sessions;
3915
- this.transport = transport;
3916
- this.config = config;
3917
- this.configStore = configStore;
3918
- this.resolveSessionAgentCommand = resolveSessionAgentCommand;
3919
- this.logger = logger2 ?? createNoopAppLogger();
4080
+ // src/commands/handlers/permission-handler.ts
4081
+ function handlePermissionStatus(context, title) {
4082
+ return { text: renderPermissionStatus(context.config, title) };
4083
+ }
4084
+ async function handlePermissionModeSet(context, mode) {
4085
+ if (!context.config || !context.configStore) {
4086
+ return { text: "当前没有加载可写入的配置。" };
3920
4087
  }
3921
- async handle(chatKey, input, reply) {
3922
- const startedAt = Date.now();
3923
- const command = parseCommand(input);
3924
- await this.logger.debug("command.parsed", "parsed inbound command", {
3925
- chatKey,
3926
- kind: command.kind
3927
- });
3928
- return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
3929
- switch (command.kind) {
3930
- case "invalid":
3931
- return {
3932
- text: [
3933
- "无法识别的命令格式。",
3934
- "",
3935
- "正确的会话创建格式:",
3936
- "/session new <别名> --agent <Agent名> --ws <工作区名>",
3937
- "",
3938
- "例如:",
3939
- "/session new demo --agent claude --ws weacpx"
3940
- ].join(`
4088
+ const previous = cloneAppConfig(context.config);
4089
+ const updated = await context.configStore.updateTransport({
4090
+ permissionMode: mode
4091
+ });
4092
+ try {
4093
+ await context.transport.updatePermissionPolicy?.(updated.transport);
4094
+ } catch (error) {
4095
+ await context.configStore.save(previous);
4096
+ context.replaceConfig(previous);
4097
+ throw error;
4098
+ }
4099
+ context.replaceConfig(updated);
4100
+ return { text: renderPermissionStatus(context.config, "权限模式已更新:") };
4101
+ }
4102
+ function handlePermissionAutoStatus(context, title) {
4103
+ return { text: renderPermissionStatus(context.config, title) };
4104
+ }
4105
+ async function handlePermissionAutoSet(context, policy) {
4106
+ if (!context.config || !context.configStore) {
4107
+ return { text: "当前没有加载可写入的配置。" };
4108
+ }
4109
+ const previous = cloneAppConfig(context.config);
4110
+ const updated = await context.configStore.updateTransport({
4111
+ nonInteractivePermissions: policy
4112
+ });
4113
+ try {
4114
+ await context.transport.updatePermissionPolicy?.(updated.transport);
4115
+ } catch (error) {
4116
+ await context.configStore.save(previous);
4117
+ context.replaceConfig(previous);
4118
+ throw error;
4119
+ }
4120
+ context.replaceConfig(updated);
4121
+ return { text: renderPermissionStatus(context.config, "非交互策略已更新:") };
4122
+ }
4123
+ function renderPermissionStatus(config, title) {
4124
+ const permissionMode = config?.transport.permissionMode ?? "approve-all";
4125
+ const nonInteractivePermissions = config?.transport.nonInteractivePermissions ?? "deny";
4126
+ return [title, `- mode: ${permissionMode}`, `- auto: ${nonInteractivePermissions}`].join(`
4127
+ `);
4128
+ }
4129
+ var permissionHelp;
4130
+ var init_permission_handler = __esm(() => {
4131
+ permissionHelp = {
4132
+ topic: "permission",
4133
+ aliases: ["pm"],
4134
+ summary: "查看和修改 transport 权限策略。",
4135
+ commands: [
4136
+ { usage: "/pm 或 /permission", description: "查看当前权限模式" },
4137
+ { usage: "/pm set <allow|read|deny>", description: "设置审批级别" },
4138
+ { usage: "/pm auto", description: "查看当前非交互策略" },
4139
+ { usage: "/pm auto <deny|fail>", description: "设置非交互策略" }
4140
+ ],
4141
+ examples: ["/pm set read", "/pm auto deny"]
4142
+ };
4143
+ });
4144
+
4145
+ // src/commands/handlers/config-handler.ts
4146
+ function handleConfigShow(context) {
4147
+ const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path11) => `- ${path11}`)];
4148
+ if (context.config) {
4149
+ lines.push("", "示例:", "- /config set wechat.replyMode final", "- /config set logging.level debug");
4150
+ }
4151
+ return { text: lines.join(`
4152
+ `) };
4153
+ }
4154
+ async function handleConfigSet(context, path11, rawValue) {
4155
+ if (!context.config || !context.configStore) {
4156
+ return { text: "当前没有加载可写入的配置。" };
4157
+ }
4158
+ const previous = cloneAppConfig(context.config);
4159
+ const updated = cloneAppConfig(context.config);
4160
+ const result = applySupportedConfigUpdate(updated, path11, rawValue);
4161
+ if ("error" in result) {
4162
+ return { text: result.error };
4163
+ }
4164
+ await context.configStore.save(updated);
4165
+ if (path11 === "transport.permissionMode" || path11 === "transport.nonInteractivePermissions") {
4166
+ try {
4167
+ await context.transport.updatePermissionPolicy?.(updated.transport);
4168
+ } catch (error) {
4169
+ await context.configStore.save(previous);
4170
+ context.replaceConfig(previous);
4171
+ throw error;
4172
+ }
4173
+ }
4174
+ context.replaceConfig(updated);
4175
+ return { text: `配置已更新:${path11} = ${result.renderedValue}` };
4176
+ }
4177
+ function applySupportedConfigUpdate(config, path11, rawValue) {
4178
+ switch (path11) {
4179
+ case "transport.type": {
4180
+ const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
4181
+ if (!parsed)
4182
+ return { error: "transport.type 只支持:acpx-cli、acpx-bridge" };
4183
+ config.transport.type = parsed;
4184
+ return { renderedValue: parsed };
4185
+ }
4186
+ case "transport.command":
4187
+ if (!rawValue.trim())
4188
+ return { error: "transport.command 不能为空。" };
4189
+ config.transport.command = rawValue;
4190
+ return { renderedValue: rawValue };
4191
+ case "transport.sessionInitTimeoutMs": {
4192
+ const parsed = parsePositiveNumber(rawValue, "transport.sessionInitTimeoutMs");
4193
+ if ("error" in parsed)
4194
+ return parsed;
4195
+ config.transport.sessionInitTimeoutMs = parsed.value;
4196
+ return { renderedValue: String(parsed.value) };
4197
+ }
4198
+ case "transport.permissionMode": {
4199
+ const parsed = parseEnum(rawValue, ["approve-all", "approve-reads", "deny-all"]);
4200
+ if (!parsed)
4201
+ return { error: "transport.permissionMode 只支持:approve-all、approve-reads、deny-all" };
4202
+ config.transport.permissionMode = parsed;
4203
+ return { renderedValue: parsed };
4204
+ }
4205
+ case "transport.nonInteractivePermissions": {
4206
+ const parsed = parseEnum(rawValue, ["deny", "fail"]);
4207
+ if (!parsed)
4208
+ return { error: "transport.nonInteractivePermissions 只支持:deny、fail" };
4209
+ config.transport.nonInteractivePermissions = parsed;
4210
+ return { renderedValue: parsed };
4211
+ }
4212
+ case "logging.level": {
4213
+ const parsed = parseEnum(rawValue, ["error", "info", "debug"]);
4214
+ if (!parsed)
4215
+ return { error: "logging.level 只支持:error、info、debug" };
4216
+ config.logging.level = parsed;
4217
+ return { renderedValue: parsed };
4218
+ }
4219
+ case "logging.maxSizeBytes": {
4220
+ const parsed = parsePositiveNumber(rawValue, "logging.maxSizeBytes");
4221
+ if ("error" in parsed)
4222
+ return parsed;
4223
+ config.logging.maxSizeBytes = parsed.value;
4224
+ return { renderedValue: String(parsed.value) };
4225
+ }
4226
+ case "logging.maxFiles": {
4227
+ const parsed = parsePositiveNumber(rawValue, "logging.maxFiles");
4228
+ if ("error" in parsed)
4229
+ return parsed;
4230
+ config.logging.maxFiles = parsed.value;
4231
+ return { renderedValue: String(parsed.value) };
4232
+ }
4233
+ case "logging.retentionDays": {
4234
+ const parsed = parsePositiveNumber(rawValue, "logging.retentionDays");
4235
+ if ("error" in parsed)
4236
+ return parsed;
4237
+ config.logging.retentionDays = parsed.value;
4238
+ return { renderedValue: String(parsed.value) };
4239
+ }
4240
+ case "wechat.replyMode": {
4241
+ const parsed = parseEnum(rawValue, ["stream", "final"]);
4242
+ if (!parsed)
4243
+ return { error: "wechat.replyMode 只支持:stream、final" };
4244
+ config.wechat.replyMode = parsed;
4245
+ return { renderedValue: parsed };
4246
+ }
4247
+ }
4248
+ const agentMatch = path11.match(/^agents\.([^.]+)\.(driver|command)$/);
4249
+ if (agentMatch) {
4250
+ const [, name, field] = agentMatch;
4251
+ if (!name || !field) {
4252
+ return { error: `不支持修改这个配置路径:${path11}` };
4253
+ }
4254
+ const agent = config.agents[name];
4255
+ if (!agent) {
4256
+ return { error: `Agent「${name}」不存在,请先创建。` };
4257
+ }
4258
+ if (!rawValue.trim()) {
4259
+ return { error: `${path11} 不能为空。` };
4260
+ }
4261
+ if (field === "driver") {
4262
+ agent.driver = rawValue;
4263
+ } else {
4264
+ agent.command = rawValue;
4265
+ }
4266
+ return { renderedValue: rawValue };
4267
+ }
4268
+ const workspaceMatch = path11.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
4269
+ if (workspaceMatch) {
4270
+ const [, name, field] = workspaceMatch;
4271
+ if (!name || !field) {
4272
+ return { error: `不支持修改这个配置路径:${path11}` };
4273
+ }
4274
+ const workspace = config.workspaces[name];
4275
+ if (!workspace) {
4276
+ return { error: `工作区「${name}」不存在,请先创建。` };
4277
+ }
4278
+ if (!rawValue.trim()) {
4279
+ return { error: `${path11} 不能为空。` };
4280
+ }
4281
+ if (field === "cwd") {
4282
+ workspace.cwd = rawValue;
4283
+ } else {
4284
+ workspace.description = rawValue;
4285
+ }
4286
+ return { renderedValue: rawValue };
4287
+ }
4288
+ return { error: `不支持修改这个配置路径:${path11}` };
4289
+ }
4290
+ function parseEnum(value, allowed) {
4291
+ return allowed.includes(value) ? value : null;
4292
+ }
4293
+ function parsePositiveNumber(rawValue, path11) {
4294
+ const value = Number(rawValue);
4295
+ if (!Number.isFinite(value) || value <= 0) {
4296
+ return { error: `${path11} 必须是正数。` };
4297
+ }
4298
+ return { value };
4299
+ }
4300
+ var SUPPORTED_CONFIG_PATHS, configHelp;
4301
+ var init_config_handler = __esm(() => {
4302
+ SUPPORTED_CONFIG_PATHS = [
4303
+ "transport.type",
4304
+ "transport.command",
4305
+ "transport.sessionInitTimeoutMs",
4306
+ "transport.permissionMode",
4307
+ "transport.nonInteractivePermissions",
4308
+ "logging.level",
4309
+ "logging.maxSizeBytes",
4310
+ "logging.maxFiles",
4311
+ "logging.retentionDays",
4312
+ "wechat.replyMode",
4313
+ "agents.<name>.driver",
4314
+ "agents.<name>.command",
4315
+ "workspaces.<name>.cwd",
4316
+ "workspaces.<name>.description"
4317
+ ];
4318
+ configHelp = {
4319
+ topic: "config",
4320
+ aliases: [],
4321
+ summary: "查看和修改受支持的配置字段。",
4322
+ commands: [
4323
+ { usage: "/config", description: "查看当前支持修改的配置路径" },
4324
+ { usage: "/config set <path> <value>", description: "修改一个受支持的配置值" }
4325
+ ],
4326
+ examples: ["/config set wechat.replyMode final", "/config set logging.level debug"]
4327
+ };
4328
+ });
4329
+
4330
+ // src/commands/handlers/session-handler.ts
4331
+ async function handleSessions(context, chatKey) {
4332
+ const sessions = await context.sessions.listSessions(chatKey);
4333
+ if (sessions.length === 0) {
4334
+ return { text: "还没有会话。请先执行 /session new <alias> --agent <name> --ws <name>。" };
4335
+ }
4336
+ return {
4337
+ text: [
4338
+ "会话列表:",
4339
+ ...sessions.map((session) => `- ${session.alias} (${session.agent} @ ${session.workspace})${session.isCurrent ? " [当前]" : ""}`)
4340
+ ].join(`
3941
4341
  `)
3942
- };
3943
- case "help":
3944
- return { text: renderHelpText() };
3945
- case "agents":
3946
- return { text: this.config ? renderAgents(this.config) : "No config loaded." };
3947
- case "agent.add": {
3948
- if (!this.config || !this.configStore) {
3949
- return { text: "当前没有加载可写入的配置。" };
3950
- }
3951
- const template = getAgentTemplate(command.template);
3952
- if (!template) {
3953
- return { text: `暂不支持这个 Agent 模板。当前可用:${listAgentTemplates().join("、")}` };
3954
- }
3955
- const updated = await this.configStore.upsertAgent(command.template, template);
3956
- this.replaceConfig(updated);
3957
- return { text: `Agent「${command.template}」已保存` };
3958
- }
3959
- case "agent.rm": {
3960
- if (!this.config || !this.configStore) {
3961
- return { text: "当前没有加载可写入的配置。" };
3962
- }
3963
- if (!this.config.agents[command.name]) {
3964
- return { text: "没有找到这个 Agent。" };
3965
- }
3966
- const updated = await this.configStore.removeAgent(command.name);
3967
- this.replaceConfig(updated);
3968
- return { text: `Agent「${command.name}」已删除` };
3969
- }
3970
- case "permission.status":
3971
- return { text: this.renderPermissionStatus("当前权限模式:") };
3972
- case "permission.mode.set": {
3973
- if (!this.config || !this.configStore) {
3974
- return { text: "当前没有加载可写入的配置。" };
3975
- }
3976
- const updated = await this.configStore.updateTransport({
3977
- permissionMode: command.mode
3978
- });
3979
- this.replaceConfig(updated);
3980
- return { text: this.renderPermissionStatus("权限模式已更新:") };
3981
- }
3982
- case "permission.auto.status":
3983
- return { text: this.renderPermissionStatus("当前非交互策略:") };
3984
- case "permission.auto.set": {
3985
- if (!this.config || !this.configStore) {
3986
- return { text: "当前没有加载可写入的配置。" };
3987
- }
3988
- const updated = await this.configStore.updateTransport({
3989
- nonInteractivePermissions: command.policy
3990
- });
3991
- this.replaceConfig(updated);
3992
- return { text: this.renderPermissionStatus("非交互策略已更新:") };
3993
- }
3994
- case "workspaces":
3995
- return { text: this.config ? renderWorkspaces(this.config) : "No config loaded." };
3996
- case "workspace.new": {
3997
- if (!this.config || !this.configStore) {
3998
- return { text: "当前没有加载可写入的配置。" };
3999
- }
4000
- const wsCwd = normalizePathForWorkspace(command.cwd);
4001
- if (!await pathExists(wsCwd)) {
4002
- return { text: `工作区路径不存在:${command.cwd}` };
4003
- }
4004
- const updated = await this.configStore.upsertWorkspace(command.name, wsCwd);
4005
- this.replaceConfig(updated);
4006
- return { text: `工作区「${command.name}」已保存` };
4007
- }
4008
- case "workspace.rm": {
4009
- if (!this.config || !this.configStore) {
4010
- return { text: "当前没有加载可写入的配置。" };
4011
- }
4012
- const updated = await this.configStore.removeWorkspace(command.name);
4013
- this.replaceConfig(updated);
4014
- return { text: `工作区「${command.name}」已删除` };
4015
- }
4016
- case "sessions": {
4017
- const sessions = await this.sessions.listSessions(chatKey);
4018
- if (sessions.length === 0) {
4019
- return { text: "还没有会话。请先执行 /session new <alias> --agent <name> --ws <name>。" };
4020
- }
4021
- return {
4022
- text: [
4023
- "会话列表:",
4024
- ...sessions.map((session) => `- ${session.alias} (${session.agent} @ ${session.workspace})${session.isCurrent ? " [当前]" : ""}`)
4025
- ].join(`
4342
+ };
4343
+ }
4344
+ async function handleSessionNew(context, chatKey, alias, agent, workspace) {
4345
+ const session = context.lifecycle.resolveSession(alias, agent, workspace, `${workspace}:${alias}`);
4346
+ try {
4347
+ await context.lifecycle.ensureTransportSession(session);
4348
+ const exists = await context.lifecycle.checkTransportSession(session);
4349
+ if (!exists) {
4350
+ return context.recovery.renderSessionCreationVerificationError(session);
4351
+ }
4352
+ } catch (error) {
4353
+ return context.recovery.renderSessionCreationError(session, error);
4354
+ }
4355
+ await context.sessions.attachSession(alias, agent, workspace, session.transportSession);
4356
+ await context.lifecycle.refreshSessionTransportAgentCommand(alias);
4357
+ await context.sessions.useSession(chatKey, alias);
4358
+ await context.logger.info("session.created", "created and selected logical session", {
4359
+ alias,
4360
+ agent,
4361
+ workspace
4362
+ });
4363
+ return { text: `会话「${alias}」已创建并切换` };
4364
+ }
4365
+ async function handleSessionShortcut(context, chatKey, agent, target, createNew) {
4366
+ return await context.lifecycle.handleSessionShortcut(chatKey, agent, target, createNew);
4367
+ }
4368
+ async function handleSessionAttach(context, chatKey, alias, agent, workspace, transportSession) {
4369
+ const attached = context.lifecycle.resolveSession(alias, agent, workspace, transportSession);
4370
+ const exists = await context.lifecycle.checkTransportSession(attached);
4371
+ if (!exists) {
4372
+ return {
4373
+ text: [
4374
+ "没有找到可绑定的已有会话。",
4375
+ `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
4376
+ ].join(`
4026
4377
  `)
4027
- };
4028
- }
4029
- case "session.new": {
4030
- const session = this.sessions.resolveSession(command.alias, command.agent, command.workspace, `${command.workspace}:${command.alias}`);
4031
- try {
4032
- await this.ensureTransportSession(session);
4033
- const exists = await this.checkTransportSession(session);
4034
- if (!exists) {
4035
- return this.renderSessionCreationVerificationError(session);
4036
- }
4037
- } catch (error) {
4038
- return this.renderSessionCreationError(session, error);
4039
- }
4040
- await this.sessions.attachSession(command.alias, command.agent, command.workspace, session.transportSession);
4041
- await this.refreshSessionTransportAgentCommand(command.alias);
4042
- await this.sessions.useSession(chatKey, command.alias);
4043
- await this.logger.info("session.created", "created and selected logical session", {
4044
- alias: command.alias,
4045
- agent: command.agent,
4046
- workspace: command.workspace
4047
- });
4048
- return { text: `会话「${command.alias}」已创建并切换` };
4049
- }
4050
- case "session.shortcut":
4051
- return await this.handleSessionShortcut(chatKey, command.agent, command.cwd, false);
4052
- case "session.shortcut.new":
4053
- return await this.handleSessionShortcut(chatKey, command.agent, command.cwd, true);
4054
- case "session.attach": {
4055
- const attached = this.sessions.resolveSession(command.alias, command.agent, command.workspace, command.transportSession);
4056
- const exists = await this.checkTransportSession(attached);
4057
- if (!exists) {
4058
- return {
4059
- text: [
4060
- "没有找到可绑定的已有会话。",
4061
- `请确认会话名是否正确,然后重新执行:/session attach ${command.alias} --agent ${command.agent} --ws ${command.workspace} --name <会话名>`
4062
- ].join(`
4378
+ };
4379
+ }
4380
+ await context.sessions.attachSession(alias, agent, workspace, transportSession);
4381
+ await context.lifecycle.refreshSessionTransportAgentCommand(alias);
4382
+ await context.sessions.useSession(chatKey, alias);
4383
+ await context.logger.info("session.attached", "attached existing transport session", {
4384
+ alias,
4385
+ agent,
4386
+ workspace,
4387
+ transportSession
4388
+ });
4389
+ return { text: `会话「${alias}」已绑定并切换` };
4390
+ }
4391
+ async function handleSessionUse(context, chatKey, alias) {
4392
+ await context.sessions.useSession(chatKey, alias);
4393
+ await context.logger.info("session.selected", "selected logical session", {
4394
+ alias,
4395
+ chatKey
4396
+ });
4397
+ return { text: `已切换到会话「${alias}」` };
4398
+ }
4399
+ async function handleModeShow(context, chatKey) {
4400
+ const session = await context.sessions.getCurrentSession(chatKey);
4401
+ if (!session) {
4402
+ return { text: NO_CURRENT_SESSION_TEXT };
4403
+ }
4404
+ return {
4405
+ text: [
4406
+ "当前 mode:",
4407
+ `- 会话:${session.alias}`,
4408
+ `- mode:${session.modeId ?? "未设置"}`
4409
+ ].join(`
4063
4410
  `)
4064
- };
4065
- }
4066
- await this.sessions.attachSession(command.alias, command.agent, command.workspace, command.transportSession);
4067
- await this.refreshSessionTransportAgentCommand(command.alias);
4068
- await this.sessions.useSession(chatKey, command.alias);
4069
- await this.logger.info("session.attached", "attached existing transport session", {
4070
- alias: command.alias,
4071
- agent: command.agent,
4072
- workspace: command.workspace,
4073
- transportSession: command.transportSession
4074
- });
4075
- return { text: `会话「${command.alias}」已绑定并切换` };
4076
- }
4077
- case "session.use":
4078
- await this.sessions.useSession(chatKey, command.alias);
4079
- await this.logger.info("session.selected", "selected logical session", {
4080
- alias: command.alias,
4081
- chatKey
4082
- });
4083
- return { text: `已切换到会话「${command.alias}」` };
4084
- case "mode.show": {
4085
- const session = await this.sessions.getCurrentSession(chatKey);
4086
- if (!session) {
4087
- return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
4088
- }
4089
- return {
4090
- text: [
4091
- "当前 mode:",
4092
- `- 会话:${session.alias}`,
4093
- `- mode:${session.modeId ?? "未设置"}`
4094
- ].join(`
4411
+ };
4412
+ }
4413
+ async function handleModeSet(context, chatKey, modeId) {
4414
+ const session = await context.sessions.getCurrentSession(chatKey);
4415
+ if (!session) {
4416
+ return { text: NO_CURRENT_SESSION_TEXT };
4417
+ }
4418
+ await context.interaction.setModeTransportSession(session, modeId);
4419
+ await context.sessions.setCurrentSessionMode(chatKey, modeId);
4420
+ return { text: `已设置当前会话 mode:${modeId}` };
4421
+ }
4422
+ async function handleReplyModeShow(context, chatKey) {
4423
+ const session = await context.sessions.getCurrentSession(chatKey);
4424
+ if (!session) {
4425
+ return { text: NO_CURRENT_SESSION_TEXT };
4426
+ }
4427
+ const globalDefault = context.config?.wechat.replyMode ?? "stream";
4428
+ const sessionOverride = session.replyMode;
4429
+ const effective = sessionOverride ?? globalDefault;
4430
+ return {
4431
+ text: [
4432
+ "当前 reply mode:",
4433
+ `- 会话:${session.alias}`,
4434
+ `- 全局默认:${globalDefault}`,
4435
+ `- 当前会话覆盖:${sessionOverride ?? "未设置"}`,
4436
+ `- 当前生效:${effective}`
4437
+ ].join(`
4095
4438
  `)
4096
- };
4097
- }
4098
- case "mode.set": {
4099
- const session = await this.sessions.getCurrentSession(chatKey);
4100
- if (!session) {
4101
- return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
4102
- }
4103
- await this.setModeTransportSession(session, command.modeId);
4104
- await this.sessions.setCurrentSessionMode(chatKey, command.modeId);
4105
- return { text: `已设置当前会话 mode:${command.modeId}` };
4106
- }
4107
- case "status": {
4108
- const session = await this.sessions.getCurrentSession(chatKey);
4109
- if (!session) {
4110
- return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
4111
- }
4112
- return {
4113
- text: [
4114
- "当前会话:",
4115
- `- 名称:${session.alias}`,
4116
- `- Agent:${session.agent}`,
4117
- `- 工作区:${session.workspace}`
4118
- ].join(`
4439
+ };
4440
+ }
4441
+ async function handleReplyModeSet(context, chatKey, replyMode) {
4442
+ const session = await context.sessions.getCurrentSession(chatKey);
4443
+ if (!session) {
4444
+ return { text: NO_CURRENT_SESSION_TEXT };
4445
+ }
4446
+ await context.sessions.setCurrentSessionReplyMode(chatKey, replyMode);
4447
+ return { text: `已设置当前会话 reply mode:${replyMode}` };
4448
+ }
4449
+ async function handleReplyModeReset(context, chatKey) {
4450
+ const session = await context.sessions.getCurrentSession(chatKey);
4451
+ if (!session) {
4452
+ return { text: NO_CURRENT_SESSION_TEXT };
4453
+ }
4454
+ await context.sessions.setCurrentSessionReplyMode(chatKey, undefined);
4455
+ const globalDefault = context.config?.wechat.replyMode ?? "stream";
4456
+ return { text: `已重置当前会话 reply mode,当前回退到全局默认:${globalDefault}` };
4457
+ }
4458
+ async function handleStatus(context, chatKey) {
4459
+ const session = await context.sessions.getCurrentSession(chatKey);
4460
+ if (!session) {
4461
+ return { text: NO_CURRENT_SESSION_TEXT };
4462
+ }
4463
+ return {
4464
+ text: [
4465
+ "当前会话:",
4466
+ `- 名称:${session.alias}`,
4467
+ `- Agent:${session.agent}`,
4468
+ `- 工作区:${session.workspace}`
4469
+ ].join(`
4119
4470
  `)
4120
- };
4121
- }
4122
- case "cancel": {
4123
- const session = await this.sessions.getCurrentSession(chatKey);
4124
- if (!session) {
4125
- return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
4126
- }
4127
- try {
4128
- const result = await this.cancelTransportSession(session);
4129
- return { text: result.message || "cancelled" };
4130
- } catch (error) {
4131
- return this.renderTransportError(session, error);
4132
- }
4133
- }
4134
- case "session.reset":
4135
- return await this.resetCurrentSession(chatKey);
4136
- case "prompt": {
4137
- const session = await this.sessions.getCurrentSession(chatKey);
4138
- if (!session) {
4139
- return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
4140
- }
4141
- try {
4142
- const result = await this.promptTransportSession(session, command.text, reply);
4143
- return { text: result.text };
4144
- } catch (error) {
4145
- const recovered = await this.tryRecoverMissingSession(session, error);
4146
- if (recovered) {
4147
- const result = await this.promptTransportSession(recovered, command.text, reply);
4148
- return { text: result.text };
4149
- }
4150
- return this.renderTransportError(session, error);
4151
- }
4152
- }
4471
+ };
4472
+ }
4473
+ async function handleCancel(context, chatKey) {
4474
+ const session = await context.sessions.getCurrentSession(chatKey);
4475
+ if (!session) {
4476
+ return { text: NO_CURRENT_SESSION_TEXT };
4477
+ }
4478
+ try {
4479
+ const result = await context.interaction.cancelTransportSession(session);
4480
+ return { text: result.message || "cancelled" };
4481
+ } catch (error) {
4482
+ return context.recovery.renderTransportError(session, error);
4483
+ }
4484
+ }
4485
+ async function handleSessionReset(context, chatKey) {
4486
+ return await context.lifecycle.resetCurrentSession(chatKey);
4487
+ }
4488
+ async function handlePrompt(context, chatKey, text, reply) {
4489
+ const session = await context.sessions.getCurrentSession(chatKey);
4490
+ if (!session) {
4491
+ return { text: NO_CURRENT_SESSION_TEXT };
4492
+ }
4493
+ try {
4494
+ const effectiveReplyMode = session.replyMode ?? context.config?.wechat.replyMode ?? "stream";
4495
+ const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
4496
+ const result = await context.interaction.promptTransportSession(session, text, transportReply);
4497
+ return { text: result.text };
4498
+ } catch (error) {
4499
+ const recovered = await context.recovery.tryRecoverMissingSession(session, error);
4500
+ if (recovered) {
4501
+ const effectiveReplyMode = recovered.replyMode ?? context.config?.wechat.replyMode ?? "stream";
4502
+ const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
4503
+ const result = await context.interaction.promptTransportSession(recovered, text, transportReply);
4504
+ return { text: result.text };
4505
+ }
4506
+ return context.recovery.renderTransportError(session, error);
4507
+ }
4508
+ }
4509
+ var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。", sessionHelp, modeHelp, replyModeHelp, statusHelp, cancelHelp;
4510
+ var init_session_handler = __esm(() => {
4511
+ sessionHelp = {
4512
+ topic: "session",
4513
+ aliases: ["ss", "sessions"],
4514
+ summary: "创建、恢复、切换和重置逻辑会话。",
4515
+ commands: [
4516
+ { usage: "/sessions", description: "查看当前会话列表" },
4517
+ { usage: "/session 或 /ss", description: "查看会话列表" },
4518
+ { usage: "/ss <agent> (-d <path> | --ws <name>)", description: "快速新建或复用一个会话" },
4519
+ { usage: "/ss new <agent> (-d <path> | --ws <name>)", description: "强制新建会话" },
4520
+ { usage: "/ss new <alias> -a <name> --ws <name>", description: "按指定配置新建会话" },
4521
+ { usage: "/ss attach <alias> -a <name> --ws <name> --name <transport-session>", description: "绑定已有会话" },
4522
+ { usage: "/use <alias>", description: "切换当前会话" },
4523
+ { usage: "/session reset 或 /clear", description: "重置当前会话上下文" }
4524
+ ],
4525
+ examples: ["/ss codex -d /absolute/path/to/repo", "/use backend-fix", "/session reset"]
4526
+ };
4527
+ modeHelp = {
4528
+ topic: "mode",
4529
+ aliases: [],
4530
+ summary: "查看或设置当前会话 mode。",
4531
+ commands: [
4532
+ { usage: "/mode", description: "查看当前会话已保存的 mode" },
4533
+ { usage: "/mode <id>", description: "设置当前会话 mode" }
4534
+ ],
4535
+ examples: ["/mode", "/mode plan"]
4536
+ };
4537
+ replyModeHelp = {
4538
+ topic: "replymode",
4539
+ aliases: [],
4540
+ summary: "查看或设置当前逻辑会话的回复输出模式。",
4541
+ commands: [
4542
+ { usage: "/replymode", description: "查看全局默认、当前覆盖和实际生效值" },
4543
+ { usage: "/replymode stream", description: "当前会话使用流式回复" },
4544
+ { usage: "/replymode final", description: "当前会话只发送最终文本" },
4545
+ { usage: "/replymode reset", description: "清除当前会话覆盖并回退到全局默认" }
4546
+ ],
4547
+ examples: ["/replymode", "/replymode final"]
4548
+ };
4549
+ statusHelp = {
4550
+ topic: "status",
4551
+ aliases: [],
4552
+ summary: "查看当前选中会话的状态。",
4553
+ commands: [{ usage: "/status", description: "查看当前会话状态" }],
4554
+ examples: ["/status"]
4555
+ };
4556
+ cancelHelp = {
4557
+ topic: "cancel",
4558
+ aliases: ["stop"],
4559
+ summary: "取消当前会话里正在执行的任务。",
4560
+ commands: [
4561
+ { usage: "/cancel", description: "取消当前任务" },
4562
+ { usage: "/stop", description: "取消当前任务(/cancel 别名)" }
4563
+ ],
4564
+ examples: ["/cancel"]
4565
+ };
4566
+ });
4567
+
4568
+ // src/commands/transport-diagnostics.ts
4569
+ function summarizeTransportError(message) {
4570
+ return message.replace(/\s+/g, " ").trim().slice(0, 200);
4571
+ }
4572
+ function summarizeTransportDiagnostic(output) {
4573
+ const trimmed = output.replace(/\s+/g, " ").trim();
4574
+ if (trimmed.length === 0) {
4575
+ return;
4576
+ }
4577
+ return trimmed.slice(0, 200);
4578
+ }
4579
+ function summarizeTransportDiagnosticTail(output) {
4580
+ const trimmed = output.replace(/\s+/g, " ").trim();
4581
+ if (trimmed.length === 0) {
4582
+ return;
4583
+ }
4584
+ return trimmed.slice(-200);
4585
+ }
4586
+ function summarizeTransportNdjson(output, prefix) {
4587
+ const lines = output.split(`
4588
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
4589
+ if (lines.length === 0) {
4590
+ return {};
4591
+ }
4592
+ const methods = new Set;
4593
+ let agentMessageChunkCount = 0;
4594
+ let stopReason;
4595
+ for (const line of lines) {
4596
+ try {
4597
+ const payload = JSON.parse(line);
4598
+ if (typeof payload.method === "string" && payload.method.length > 0) {
4599
+ methods.add(payload.method);
4153
4600
  }
4154
- });
4601
+ if (payload.params?.update?.sessionUpdate === "agent_message_chunk") {
4602
+ agentMessageChunkCount += 1;
4603
+ }
4604
+ if (typeof payload.result?.stopReason === "string" && payload.result.stopReason.length > 0) {
4605
+ stopReason = payload.result.stopReason;
4606
+ }
4607
+ } catch {
4608
+ continue;
4609
+ }
4155
4610
  }
4156
- async clearSession(chatKey) {
4157
- await this.resetCurrentSession(chatKey);
4611
+ const summary = {
4612
+ [`${prefix}LineCount`]: lines.length
4613
+ };
4614
+ if (methods.size > 0) {
4615
+ summary[`${prefix}Methods`] = [...methods].join(",");
4158
4616
  }
4159
- async handleSessionShortcut(chatKey, agent, cwdInput, createNew) {
4160
- if (!this.config || !this.configStore) {
4161
- return { text: "当前没有加载可写入的配置。" };
4617
+ if (agentMessageChunkCount > 0) {
4618
+ summary[`${prefix}AgentMessageChunkCount`] = agentMessageChunkCount;
4619
+ }
4620
+ if (stopReason) {
4621
+ summary[`${prefix}StopReason`] = stopReason;
4622
+ }
4623
+ return summary;
4624
+ }
4625
+ function isPartialPromptOutputError(message) {
4626
+ return message.includes("未收到最终回复");
4627
+ }
4628
+
4629
+ // src/formatting/render-text.ts
4630
+ function renderAgents(config) {
4631
+ const names = Object.keys(config.agents);
4632
+ if (names.length === 0) {
4633
+ return "还没有注册任何 Agent。";
4634
+ }
4635
+ return ["已注册的 Agent:", ...names.map((name) => `- ${name}`)].join(`
4636
+ `);
4637
+ }
4638
+ function renderWorkspaces(config) {
4639
+ const names = Object.entries(config.workspaces);
4640
+ if (names.length === 0) {
4641
+ return "还没有注册任何工作区。";
4642
+ }
4643
+ return ["已注册的工作区:", ...names.map(([name, workspace]) => `- ${name}: ${workspace.cwd}`)].join(`
4644
+ `);
4645
+ }
4646
+
4647
+ // src/config/agent-templates.ts
4648
+ function getAgentTemplate(name) {
4649
+ const template = TEMPLATES[name];
4650
+ if (!template) {
4651
+ return null;
4652
+ }
4653
+ return {
4654
+ ...template
4655
+ };
4656
+ }
4657
+ function listAgentTemplates() {
4658
+ return Object.keys(TEMPLATES);
4659
+ }
4660
+ var TEMPLATES;
4661
+ var init_agent_templates = __esm(() => {
4662
+ TEMPLATES = {
4663
+ codex: {
4664
+ driver: "codex"
4665
+ },
4666
+ claude: {
4667
+ driver: "claude"
4162
4668
  }
4163
- const cwd = normalizePathForWorkspace(cwdInput);
4164
- if (!await pathExists(cwd)) {
4165
- return { text: `工作区路径不存在:${cwdInput}` };
4669
+ };
4670
+ });
4671
+
4672
+ // src/commands/handlers/agent-handler.ts
4673
+ function handleAgents(context) {
4674
+ return { text: context.config ? renderAgents(context.config) : "No config loaded." };
4675
+ }
4676
+ async function handleAgentAdd(context, templateName) {
4677
+ if (!context.config || !context.configStore) {
4678
+ return { text: "当前没有加载可写入的配置。" };
4679
+ }
4680
+ const template = getAgentTemplate(templateName);
4681
+ if (!template) {
4682
+ return { text: `暂不支持这个 Agent 模板。当前可用:${listAgentTemplates().join("、")}` };
4683
+ }
4684
+ const updated = await context.configStore.upsertAgent(templateName, template);
4685
+ context.replaceConfig(updated);
4686
+ return { text: `Agent「${templateName}」已保存` };
4687
+ }
4688
+ async function handleAgentRemove(context, agentName) {
4689
+ if (!context.config || !context.configStore) {
4690
+ return { text: "当前没有加载可写入的配置。" };
4691
+ }
4692
+ if (!context.config.agents[agentName]) {
4693
+ return { text: "没有找到这个 Agent。" };
4694
+ }
4695
+ const updated = await context.configStore.removeAgent(agentName);
4696
+ context.replaceConfig(updated);
4697
+ return { text: `Agent「${agentName}」已删除` };
4698
+ }
4699
+ var agentHelp;
4700
+ var init_agent_handler = __esm(() => {
4701
+ init_agent_templates();
4702
+ agentHelp = {
4703
+ topic: "agent",
4704
+ aliases: ["agents"],
4705
+ summary: "管理已注册的 Agent。",
4706
+ commands: [
4707
+ { usage: "/agents", description: "查看当前已注册的 Agent" },
4708
+ { usage: "/agent add <codex|claude>", description: "添加内置 Agent 模板" },
4709
+ { usage: "/agent rm <name>", description: "删除一个 Agent" }
4710
+ ],
4711
+ examples: ["/agent add claude", "/agent rm codex"]
4712
+ };
4713
+ });
4714
+
4715
+ // src/commands/handlers/workspace-handler.ts
4716
+ import { access } from "node:fs/promises";
4717
+ import { homedir as homedir3 } from "node:os";
4718
+ import { normalize } from "node:path";
4719
+ function handleWorkspaces(context) {
4720
+ return { text: context.config ? renderWorkspaces(context.config) : "No config loaded." };
4721
+ }
4722
+ async function handleWorkspaceCreate(context, workspaceName, cwd) {
4723
+ if (!context.config || !context.configStore) {
4724
+ return { text: "当前没有加载可写入的配置。" };
4725
+ }
4726
+ const normalizedCwd = normalizePathForWorkspace(cwd);
4727
+ if (!await pathExists(normalizedCwd)) {
4728
+ return { text: `工作区路径不存在:${cwd}` };
4729
+ }
4730
+ const updated = await context.configStore.upsertWorkspace(workspaceName, normalizedCwd);
4731
+ context.replaceConfig(updated);
4732
+ return { text: `工作区「${workspaceName}」已保存` };
4733
+ }
4734
+ async function handleWorkspaceRemove(context, workspaceName) {
4735
+ if (!context.config || !context.configStore) {
4736
+ return { text: "当前没有加载可写入的配置。" };
4737
+ }
4738
+ const updated = await context.configStore.removeWorkspace(workspaceName);
4739
+ context.replaceConfig(updated);
4740
+ return { text: `工作区「${workspaceName}」已删除` };
4741
+ }
4742
+ async function pathExists(path11) {
4743
+ try {
4744
+ await access(path11);
4745
+ return true;
4746
+ } catch {
4747
+ return false;
4748
+ }
4749
+ }
4750
+ function normalizePathForWorkspace(path11) {
4751
+ const expanded = path11.startsWith("~") ? homedir3() + path11.slice(1) : path11;
4752
+ return normalize(expanded);
4753
+ }
4754
+ var workspaceHelp;
4755
+ var init_workspace_handler = __esm(() => {
4756
+ workspaceHelp = {
4757
+ topic: "workspace",
4758
+ aliases: ["ws", "workspaces"],
4759
+ summary: "管理已注册的工作区。",
4760
+ commands: [
4761
+ { usage: "/workspaces", description: "查看当前已注册的工作区" },
4762
+ { usage: "/workspace 或 /ws", description: "查看工作区列表" },
4763
+ { usage: "/ws new <name> -d <path>", description: "添加工作区" },
4764
+ { usage: "/workspace rm <name>", description: "删除工作区" }
4765
+ ],
4766
+ examples: ['/ws new backend -d "/tmp/backend"', "/workspace rm backend"]
4767
+ };
4768
+ });
4769
+
4770
+ // src/commands/help/help-registry.ts
4771
+ function getHelpTopic(topic) {
4772
+ return HELP_TOPIC_MAP.get(topic) ?? null;
4773
+ }
4774
+ function listHelpTopics() {
4775
+ return HELP_TOPICS;
4776
+ }
4777
+ var HELP_TOPICS, HELP_TOPIC_MAP;
4778
+ var init_help_registry = __esm(() => {
4779
+ init_agent_handler();
4780
+ init_config_handler();
4781
+ init_permission_handler();
4782
+ init_session_handler();
4783
+ init_workspace_handler();
4784
+ HELP_TOPICS = [
4785
+ sessionHelp,
4786
+ workspaceHelp,
4787
+ agentHelp,
4788
+ permissionHelp,
4789
+ configHelp,
4790
+ modeHelp,
4791
+ replyModeHelp,
4792
+ statusHelp,
4793
+ cancelHelp
4794
+ ];
4795
+ HELP_TOPIC_MAP = new Map;
4796
+ for (const topic of HELP_TOPICS) {
4797
+ HELP_TOPIC_MAP.set(topic.topic, topic);
4798
+ for (const alias of topic.aliases) {
4799
+ HELP_TOPIC_MAP.set(alias, topic);
4166
4800
  }
4167
- const workspace = await this.resolveShortcutWorkspace(cwd);
4168
- await this.logger.info("session.shortcut.workspace", "resolved shortcut workspace", {
4801
+ }
4802
+ });
4803
+
4804
+ // src/commands/handlers/help-handler.ts
4805
+ function handleHelp(topic) {
4806
+ if (!topic) {
4807
+ return { text: renderHelpIndex() };
4808
+ }
4809
+ const entry = getHelpTopic(topic);
4810
+ if (!entry) {
4811
+ return { text: renderUnknownHelpTopic(topic) };
4812
+ }
4813
+ return { text: renderHelpTopic(entry) };
4814
+ }
4815
+ function renderHelpIndex() {
4816
+ const topics = listHelpTopics();
4817
+ return [
4818
+ "常用入口:",
4819
+ "- /ss <agent> (-d <path> | --ws <name>) - 快速新建或切到会话",
4820
+ "- /use <alias> - 切换当前会话",
4821
+ "- /status - 查看当前会话状态",
4822
+ "",
4823
+ "顶级命令:",
4824
+ ...topics.map((topic) => `- ${topic.topic} - ${topic.summary}`),
4825
+ "",
4826
+ "查看专题说明:",
4827
+ "- /help <topic>",
4828
+ "- 例如:/help ss、/help ws、/help pm"
4829
+ ].join(`
4830
+ `);
4831
+ }
4832
+ function renderHelpTopic(topic) {
4833
+ return [
4834
+ `帮助主题:${topic.topic}`,
4835
+ `说明:${topic.summary}`,
4836
+ ...topic.aliases.length > 0 ? [`别名:${topic.aliases.join("、")}`] : [],
4837
+ "",
4838
+ "命令:",
4839
+ ...topic.commands.map((command) => `- ${command.usage} - ${command.description}`),
4840
+ ...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : []
4841
+ ].join(`
4842
+ `);
4843
+ }
4844
+ function renderUnknownHelpTopic(topic) {
4845
+ return [
4846
+ `未知帮助主题:${topic}`,
4847
+ "",
4848
+ "可用主题:",
4849
+ ...listHelpTopics().map((entry) => `- ${entry.topic}`)
4850
+ ].join(`
4851
+ `);
4852
+ }
4853
+ var init_help_handler = __esm(() => {
4854
+ init_help_registry();
4855
+ });
4856
+
4857
+ // src/commands/handlers/session-shortcut-handler.ts
4858
+ import { access as access2 } from "node:fs/promises";
4859
+ import { basename as basename2, normalize as normalize2 } from "node:path";
4860
+ import { homedir as homedir4 } from "node:os";
4861
+ async function handleSessionShortcutCommand(context, ops, chatKey, agent, target, createNew) {
4862
+ if (!context.config || !context.configStore) {
4863
+ return { text: "当前没有加载可写入的配置。" };
4864
+ }
4865
+ if (!context.config.agents[agent]) {
4866
+ return { text: `agent "${agent}" is not registered` };
4867
+ }
4868
+ const workspace = await resolveShortcutWorkspace(context, target);
4869
+ if ("error" in workspace) {
4870
+ return { text: workspace.error };
4871
+ }
4872
+ await context.logger.info("session.shortcut.workspace", "resolved shortcut workspace", {
4873
+ workspace: workspace.name,
4874
+ cwd: workspace.cwd,
4875
+ reused: workspace.reused
4876
+ });
4877
+ const baseAlias = `${workspace.name}:${agent}`;
4878
+ const alias = createNew ? await allocateUniqueSessionAlias(context, baseAlias, chatKey) : baseAlias;
4879
+ if (!createNew && await hasLogicalSession(context, alias, chatKey)) {
4880
+ await context.sessions.useSession(chatKey, alias);
4881
+ await context.logger.info("session.shortcut.reused", "reused existing logical session", {
4882
+ alias,
4169
4883
  workspace: workspace.name,
4170
- cwd: workspace.cwd,
4171
- reused: workspace.reused
4884
+ agent
4172
4885
  });
4173
- const baseAlias = `${workspace.name}:${agent}`;
4174
- const alias = createNew ? await this.allocateUniqueSessionAlias(baseAlias, chatKey) : baseAlias;
4175
- if (!createNew && await this.hasLogicalSession(alias, chatKey)) {
4176
- await this.sessions.useSession(chatKey, alias);
4177
- await this.logger.info("session.shortcut.reused", "reused existing logical session", {
4178
- alias,
4179
- workspace: workspace.name,
4180
- agent
4181
- });
4886
+ return {
4887
+ text: [
4888
+ `已切换到会话「${alias}」`,
4889
+ `- 复用工作区:${workspace.name}`,
4890
+ `- 复用会话:${alias}`
4891
+ ].join(`
4892
+ `)
4893
+ };
4894
+ }
4895
+ const session = ops.resolveSession(alias, agent, workspace.name, `${workspace.name}:${alias}`);
4896
+ try {
4897
+ await ops.ensureTransportSession(session);
4898
+ const exists = await ops.checkTransportSession(session);
4899
+ if (!exists) {
4900
+ return renderShortcutSessionCreationError(workspace, alias);
4901
+ }
4902
+ } catch {
4903
+ return renderShortcutSessionCreationError(workspace, alias);
4904
+ }
4905
+ await context.sessions.attachSession(alias, agent, workspace.name, session.transportSession);
4906
+ await ops.refreshSessionTransportAgentCommand(alias);
4907
+ await context.sessions.useSession(chatKey, alias);
4908
+ await context.logger.info("session.shortcut.created", "created new logical session from shortcut", {
4909
+ alias,
4910
+ workspace: workspace.name,
4911
+ agent,
4912
+ workspaceReused: workspace.reused
4913
+ });
4914
+ return {
4915
+ text: [
4916
+ `已创建并切换到会话「${alias}」`,
4917
+ workspace.reused ? `- 复用工作区:${workspace.name}` : `- 新增工作区:${workspace.name} -> ${workspace.cwd}`,
4918
+ `- 新增会话:${alias}`
4919
+ ].join(`
4920
+ `)
4921
+ };
4922
+ }
4923
+ async function resolveShortcutWorkspace(context, target) {
4924
+ if (target.workspace) {
4925
+ const workspace = context.config?.workspaces[target.workspace];
4926
+ if (!workspace) {
4927
+ return { error: `workspace "${target.workspace}" is not registered` };
4928
+ }
4929
+ return {
4930
+ name: target.workspace,
4931
+ cwd: workspace.cwd,
4932
+ reused: true
4933
+ };
4934
+ }
4935
+ const cwdInput = target.cwd ?? "";
4936
+ const cwd = normalizePathForWorkspace2(cwdInput);
4937
+ if (!await pathExists2(cwd)) {
4938
+ return { error: `工作区路径不存在:${cwdInput}` };
4939
+ }
4940
+ const existingByPath = Object.entries(context.config?.workspaces ?? {}).find(([, workspace]) => sameWorkspacePath(workspace.cwd, cwd));
4941
+ if (existingByPath) {
4942
+ return {
4943
+ name: existingByPath[0],
4944
+ cwd: existingByPath[1].cwd,
4945
+ reused: true
4946
+ };
4947
+ }
4948
+ const workspaceName = allocateWorkspaceName(context, basename2(cwd));
4949
+ const updated = await context.configStore.upsertWorkspace(workspaceName, cwd);
4950
+ context.replaceConfig(updated);
4951
+ return {
4952
+ name: workspaceName,
4953
+ cwd,
4954
+ reused: false
4955
+ };
4956
+ }
4957
+ function allocateWorkspaceName(context, baseName) {
4958
+ if (!context.config?.workspaces[baseName]) {
4959
+ return baseName;
4960
+ }
4961
+ let suffix = 2;
4962
+ while (context.config.workspaces[`${baseName}-${suffix}`]) {
4963
+ suffix += 1;
4964
+ }
4965
+ return `${baseName}-${suffix}`;
4966
+ }
4967
+ async function allocateUniqueSessionAlias(context, baseAlias, chatKey) {
4968
+ if (!await hasLogicalSession(context, baseAlias, chatKey)) {
4969
+ return baseAlias;
4970
+ }
4971
+ let suffix = 2;
4972
+ while (await hasLogicalSession(context, `${baseAlias}-${suffix}`, chatKey)) {
4973
+ suffix += 1;
4974
+ }
4975
+ return `${baseAlias}-${suffix}`;
4976
+ }
4977
+ async function hasLogicalSession(context, alias, chatKey) {
4978
+ const sessions = await context.sessions.listSessions(chatKey);
4979
+ return sessions.some((session) => session.alias === alias);
4980
+ }
4981
+ function renderShortcutSessionCreationError(workspace, alias) {
4982
+ return {
4983
+ text: [
4984
+ `会话「${alias}」创建失败。`,
4985
+ workspace.reused ? `- 复用工作区:${workspace.name}` : `- 已新增工作区:${workspace.name} -> ${workspace.cwd}`,
4986
+ "- 会话未创建,请重试。"
4987
+ ].join(`
4988
+ `)
4989
+ };
4990
+ }
4991
+ async function pathExists2(path11) {
4992
+ try {
4993
+ await access2(path11);
4994
+ return true;
4995
+ } catch {
4996
+ return false;
4997
+ }
4998
+ }
4999
+ function normalizePathForWorkspace2(path11) {
5000
+ const expanded = path11.startsWith("~") ? homedir4() + path11.slice(1) : path11;
5001
+ return normalize2(expanded);
5002
+ }
5003
+ function sameWorkspacePath(left, right) {
5004
+ const normalizedLeft = normalizePathForWorkspace2(left);
5005
+ const normalizedRight = normalizePathForWorkspace2(right);
5006
+ if (process.platform === "win32") {
5007
+ return normalizedLeft.toLowerCase() === normalizedRight.toLowerCase();
5008
+ }
5009
+ return normalizedLeft === normalizedRight;
5010
+ }
5011
+ var init_session_shortcut_handler = () => {};
5012
+
5013
+ // src/commands/handlers/session-recovery-handler.ts
5014
+ function renderTransportError(session, error) {
5015
+ const message = error instanceof Error ? error.message : String(error);
5016
+ if (message.includes("No acpx session found")) {
5017
+ return {
5018
+ text: [
5019
+ `当前会话「${session.alias}」暂时不可用。`,
5020
+ `请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${session.workspace}`,
5021
+ `如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
5022
+ ].join(`
5023
+ `)
5024
+ };
5025
+ }
5026
+ if (!isPartialPromptOutputError(message)) {
5027
+ throw error;
5028
+ }
5029
+ return {
5030
+ text: [
5031
+ `当前会话「${session.alias}」执行中断,未收到最终回复。`,
5032
+ "请直接重试;如果长时间无响应,可先发送 /cancel 后再重试。",
5033
+ `错误信息:${summarizeTransportError(message)}`
5034
+ ].join(`
5035
+ `)
5036
+ };
5037
+ }
5038
+ function renderSessionCreationError(session, error) {
5039
+ const message = error instanceof Error ? error.message : String(error);
5040
+ if (message.includes("timed out") && message.includes("sessions new")) {
5041
+ return renderSessionCreationFailure(session, message);
5042
+ }
5043
+ throw error;
5044
+ }
5045
+ function renderSessionCreationVerificationError(session) {
5046
+ return renderSessionCreationFailure(session, "未检测到可用的后端会话。");
5047
+ }
5048
+ function renderSessionCreationFailure(session, detail) {
5049
+ return {
5050
+ text: [
5051
+ "会话创建失败。",
5052
+ `错误信息:${summarizeTransportError(detail)}`,
5053
+ `如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
5054
+ ].join(`
5055
+ `)
5056
+ };
5057
+ }
5058
+ async function tryRecoverMissingSession(ops, session, error) {
5059
+ const message = error instanceof Error ? error.message : String(error);
5060
+ if (!message.includes("No acpx session found")) {
5061
+ return null;
5062
+ }
5063
+ const transportAgentCommand = await ops.resolveSessionAgentCommand(session);
5064
+ if (!transportAgentCommand || transportAgentCommand === session.agentCommand) {
5065
+ return null;
5066
+ }
5067
+ await ops.setSessionTransportAgentCommand(session.alias, transportAgentCommand);
5068
+ return await ops.getSession(session.alias);
5069
+ }
5070
+ var init_session_recovery_handler = () => {};
5071
+
5072
+ // src/commands/handlers/session-reset-handler.ts
5073
+ async function handleSessionResetCommand(context, ops, chatKey) {
5074
+ const session = await context.sessions.getCurrentSession(chatKey);
5075
+ if (!session) {
5076
+ return { text: NO_CURRENT_SESSION_TEXT2 };
5077
+ }
5078
+ const resetSession = ops.resolveSession(session.alias, session.agent, session.workspace, buildResetTransportSessionName(session, ops.now()));
5079
+ try {
5080
+ await ops.ensureTransportSession(resetSession);
5081
+ const exists = await ops.checkTransportSession(resetSession);
5082
+ if (!exists) {
4182
5083
  return {
4183
5084
  text: [
4184
- `已切换到会话「${alias}」`,
4185
- `- 复用工作区:${workspace.name}`,
4186
- `- 复用会话:${alias}`
5085
+ `会话「${session.alias}」重置失败。`,
5086
+ "新的后端会话未创建成功,请稍后重试。"
4187
5087
  ].join(`
4188
5088
  `)
4189
5089
  };
4190
5090
  }
4191
- const session = this.sessions.resolveSession(alias, agent, workspace.name, `${workspace.name}:${alias}`);
4192
- try {
4193
- await this.ensureTransportSession(session);
4194
- const exists = await this.checkTransportSession(session);
4195
- if (!exists) {
4196
- return this.renderShortcutSessionCreationError(workspace, alias);
5091
+ } catch (error) {
5092
+ return renderTransportError(resetSession, error);
5093
+ }
5094
+ await context.sessions.attachSession(resetSession.alias, resetSession.agent, resetSession.workspace, resetSession.transportSession);
5095
+ await ops.refreshSessionTransportAgentCommand(resetSession.alias);
5096
+ await context.sessions.useSession(chatKey, resetSession.alias);
5097
+ await context.logger.info("session.reset", "reset current logical session", {
5098
+ alias: resetSession.alias,
5099
+ agent: resetSession.agent,
5100
+ workspace: resetSession.workspace,
5101
+ transportSession: resetSession.transportSession,
5102
+ chatKey
5103
+ });
5104
+ return { text: `会话「${resetSession.alias}」已重置` };
5105
+ }
5106
+ function buildResetTransportSessionName(session, now) {
5107
+ return `${session.workspace}:${session.alias}:reset-${now}`;
5108
+ }
5109
+ var NO_CURRENT_SESSION_TEXT2 = "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。";
5110
+ var init_session_reset_handler = __esm(() => {
5111
+ init_session_recovery_handler();
5112
+ });
5113
+
5114
+ // src/commands/command-router.ts
5115
+ class CommandRouter {
5116
+ sessions;
5117
+ transport;
5118
+ config;
5119
+ configStore;
5120
+ resolveSessionAgentCommand;
5121
+ logger;
5122
+ constructor(sessions, transport, config, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex) {
5123
+ this.sessions = sessions;
5124
+ this.transport = transport;
5125
+ this.config = config;
5126
+ this.configStore = configStore;
5127
+ this.resolveSessionAgentCommand = resolveSessionAgentCommand;
5128
+ this.logger = logger2 ?? createNoopAppLogger();
5129
+ }
5130
+ async handle(chatKey, input, reply) {
5131
+ const startedAt = Date.now();
5132
+ const command = parseCommand(input);
5133
+ await this.logger.debug("command.parsed", "parsed inbound command", {
5134
+ chatKey,
5135
+ kind: command.kind
5136
+ });
5137
+ return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
5138
+ switch (command.kind) {
5139
+ case "invalid":
5140
+ return {
5141
+ text: [
5142
+ "无法识别的命令格式。",
5143
+ "",
5144
+ "正确的会话创建格式:",
5145
+ "/session new <别名> --agent <Agent名> --ws <工作区名>",
5146
+ "",
5147
+ "例如:",
5148
+ "/session new demo --agent claude --ws weacpx"
5149
+ ].join(`
5150
+ `)
5151
+ };
5152
+ case "help":
5153
+ return handleHelp(command.topic);
5154
+ case "agents":
5155
+ return handleAgents(this.createHandlerContext());
5156
+ case "agent.add":
5157
+ return await handleAgentAdd(this.createHandlerContext(), command.template);
5158
+ case "agent.rm":
5159
+ return await handleAgentRemove(this.createHandlerContext(), command.name);
5160
+ case "permission.status":
5161
+ return handlePermissionStatus(this.createHandlerContext(), "当前权限模式:");
5162
+ case "permission.mode.set":
5163
+ return await handlePermissionModeSet(this.createHandlerContext(), command.mode);
5164
+ case "permission.auto.status":
5165
+ return handlePermissionAutoStatus(this.createHandlerContext(), "当前非交互策略:");
5166
+ case "permission.auto.set":
5167
+ return await handlePermissionAutoSet(this.createHandlerContext(), command.policy);
5168
+ case "config.show":
5169
+ return handleConfigShow(this.createHandlerContext());
5170
+ case "config.set":
5171
+ return await handleConfigSet(this.createHandlerContext(), command.path, command.value);
5172
+ case "workspaces":
5173
+ return handleWorkspaces(this.createHandlerContext());
5174
+ case "workspace.new":
5175
+ return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd);
5176
+ case "workspace.rm":
5177
+ return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
5178
+ case "sessions":
5179
+ return await handleSessions(this.createSessionHandlerContext(), chatKey);
5180
+ case "session.new":
5181
+ return await handleSessionNew(this.createSessionHandlerContext(), chatKey, command.alias, command.agent, command.workspace);
5182
+ case "session.shortcut":
5183
+ return await handleSessionShortcut(this.createSessionHandlerContext(), chatKey, command.agent, command, false);
5184
+ case "session.shortcut.new":
5185
+ return await handleSessionShortcut(this.createSessionHandlerContext(), chatKey, command.agent, command, true);
5186
+ case "session.attach":
5187
+ return await handleSessionAttach(this.createSessionHandlerContext(), chatKey, command.alias, command.agent, command.workspace, command.transportSession);
5188
+ case "session.use":
5189
+ return await handleSessionUse(this.createSessionHandlerContext(), chatKey, command.alias);
5190
+ case "mode.show":
5191
+ return await handleModeShow(this.createSessionHandlerContext(), chatKey);
5192
+ case "mode.set":
5193
+ return await handleModeSet(this.createSessionHandlerContext(), chatKey, command.modeId);
5194
+ case "replymode.show":
5195
+ return await handleReplyModeShow(this.createSessionHandlerContext(), chatKey);
5196
+ case "replymode.set":
5197
+ return await handleReplyModeSet(this.createSessionHandlerContext(), chatKey, command.replyMode);
5198
+ case "replymode.reset":
5199
+ return await handleReplyModeReset(this.createSessionHandlerContext(), chatKey);
5200
+ case "status":
5201
+ return await handleStatus(this.createSessionHandlerContext(), chatKey);
5202
+ case "cancel":
5203
+ return await handleCancel(this.createSessionHandlerContext(), chatKey);
5204
+ case "session.reset":
5205
+ return await handleSessionReset(this.createSessionHandlerContext(), chatKey);
5206
+ case "prompt":
5207
+ return await handlePrompt(this.createSessionHandlerContext(), chatKey, command.text, reply);
4197
5208
  }
4198
- } catch {
4199
- return this.renderShortcutSessionCreationError(workspace, alias);
4200
- }
4201
- await this.sessions.attachSession(alias, agent, workspace.name, session.transportSession);
4202
- await this.refreshSessionTransportAgentCommand(alias);
4203
- await this.sessions.useSession(chatKey, alias);
4204
- await this.logger.info("session.shortcut.created", "created new logical session from shortcut", {
4205
- alias,
4206
- workspace: workspace.name,
4207
- agent,
4208
- workspaceReused: workspace.reused
4209
5209
  });
4210
- return {
4211
- text: [
4212
- `已创建并切换到会话「${alias}」`,
4213
- workspace.reused ? `- 复用工作区:${workspace.name}` : `- 新增工作区:${workspace.name} -> ${workspace.cwd}`,
4214
- `- 新增会话:${alias}`
4215
- ].join(`
4216
- `)
4217
- };
4218
5210
  }
4219
- replaceConfig(updated) {
4220
- if (!this.config) {
4221
- return;
4222
- }
4223
- this.config.transport = { ...updated.transport };
4224
- this.config.agents = { ...updated.agents };
4225
- this.config.workspaces = { ...updated.workspaces };
4226
- }
4227
- renderPermissionStatus(title) {
4228
- const permissionMode = this.config?.transport.permissionMode ?? "approve-all";
4229
- const nonInteractivePermissions = this.config?.transport.nonInteractivePermissions ?? "fail";
4230
- return [title, `- mode: ${permissionMode}`, `- auto: ${nonInteractivePermissions}`].join(`
4231
- `);
5211
+ async clearSession(chatKey) {
5212
+ await handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(), chatKey);
4232
5213
  }
4233
- renderTransportError(session, error) {
4234
- const message = error instanceof Error ? error.message : String(error);
4235
- if (message.includes("No acpx session found")) {
4236
- return {
4237
- text: [
4238
- `当前会话「${session.alias}」暂时不可用。`,
4239
- `请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${session.workspace}`,
4240
- `如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
4241
- ].join(`
4242
- `)
4243
- };
4244
- }
4245
- if (!isPartialPromptOutputError(message)) {
4246
- throw error;
4247
- }
5214
+ createHandlerContext() {
4248
5215
  return {
4249
- text: [
4250
- `当前会话「${session.alias}」执行中断,未收到最终回复。`,
4251
- "请直接重试;如果长时间无响应,可先发送 /cancel 后再重试。",
4252
- `错误信息:${summarizeTransportError(message)}`
4253
- ].join(`
4254
- `)
5216
+ sessions: this.sessions,
5217
+ transport: this.transport,
5218
+ config: this.config,
5219
+ configStore: this.configStore,
5220
+ logger: this.logger,
5221
+ replaceConfig: (updated) => this.replaceConfig(updated)
4255
5222
  };
4256
5223
  }
4257
- renderSessionCreationError(session, error) {
4258
- const message = error instanceof Error ? error.message : String(error);
4259
- if (message.includes("timed out") && message.includes("sessions new")) {
4260
- return this.renderSessionCreationVerificationError(session);
4261
- }
4262
- throw error;
5224
+ createSessionHandlerContext() {
5225
+ return {
5226
+ ...this.createHandlerContext(),
5227
+ lifecycle: this.createSessionLifecycleOps(),
5228
+ interaction: this.createSessionInteractionOps(),
5229
+ recovery: this.createSessionRenderRecoveryOps()
5230
+ };
4263
5231
  }
4264
- renderSessionCreationVerificationError(session) {
5232
+ createSessionLifecycleOps() {
4265
5233
  return {
4266
- text: [
4267
- "当前还不能直接在微信里创建新会话。",
4268
- `请先准备好一个已有会话,然后在微信里执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
4269
- ].join(`
4270
- `)
5234
+ resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
5235
+ ensureTransportSession: (session) => this.ensureTransportSession(session),
5236
+ checkTransportSession: (session) => this.checkTransportSession(session),
5237
+ handleSessionShortcut: (chatKey, agent, target, createNew) => handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(), chatKey, agent, target, createNew),
5238
+ resetCurrentSession: (chatKey) => handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(), chatKey),
5239
+ refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
4271
5240
  };
4272
5241
  }
4273
- async resolveShortcutWorkspace(cwd) {
4274
- const existingByPath = Object.entries(this.config?.workspaces ?? {}).find(([, workspace]) => sameWorkspacePath(workspace.cwd, cwd));
4275
- if (existingByPath) {
4276
- return {
4277
- name: existingByPath[0],
4278
- cwd: existingByPath[1].cwd,
4279
- reused: true
4280
- };
4281
- }
4282
- const baseName = basename2(cwd);
4283
- const workspaceName = this.allocateWorkspaceName(baseName, cwd);
4284
- const updated = await this.configStore.upsertWorkspace(workspaceName, cwd);
4285
- this.replaceConfig(updated);
5242
+ createSessionInteractionOps() {
4286
5243
  return {
4287
- name: workspaceName,
4288
- cwd,
4289
- reused: false
5244
+ setModeTransportSession: (session, modeId) => this.setModeTransportSession(session, modeId),
5245
+ cancelTransportSession: (session) => this.cancelTransportSession(session),
5246
+ promptTransportSession: (session, text, reply) => this.promptTransportSession(session, text, reply)
4290
5247
  };
4291
5248
  }
4292
- allocateWorkspaceName(baseName, cwd) {
4293
- if (!this.config?.workspaces[baseName]) {
4294
- return baseName;
4295
- }
4296
- let suffix = 2;
4297
- while (this.config.workspaces[`${baseName}-${suffix}`]) {
4298
- suffix += 1;
4299
- }
4300
- return `${baseName}-${suffix}`;
5249
+ createSessionRenderRecoveryOps() {
5250
+ return {
5251
+ renderSessionCreationError: (session, error) => renderSessionCreationError(session, error),
5252
+ renderSessionCreationVerificationError: (session) => renderSessionCreationVerificationError(session),
5253
+ tryRecoverMissingSession: (session, error) => tryRecoverMissingSession(this.createSessionRecoveryOps(), session, error),
5254
+ renderTransportError: (session, error) => renderTransportError(session, error)
5255
+ };
4301
5256
  }
4302
- async allocateUniqueSessionAlias(baseAlias, chatKey) {
4303
- if (!await this.hasLogicalSession(baseAlias, chatKey)) {
4304
- return baseAlias;
4305
- }
4306
- let suffix = 2;
4307
- while (await this.hasLogicalSession(`${baseAlias}-${suffix}`, chatKey)) {
4308
- suffix += 1;
4309
- }
4310
- return `${baseAlias}-${suffix}`;
5257
+ createSessionResetOps() {
5258
+ return {
5259
+ ensureTransportSession: (session) => this.ensureTransportSession(session),
5260
+ checkTransportSession: (session) => this.checkTransportSession(session),
5261
+ resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
5262
+ refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias),
5263
+ now: () => Date.now()
5264
+ };
4311
5265
  }
4312
- async hasLogicalSession(alias, chatKey) {
4313
- const sessions = await this.sessions.listSessions(chatKey);
4314
- return sessions.some((session) => session.alias === alias);
5266
+ createSessionRecoveryOps() {
5267
+ return {
5268
+ resolveSessionAgentCommand: (session) => this.resolveSessionAgentCommand(session),
5269
+ setSessionTransportAgentCommand: (alias, command) => this.sessions.setSessionTransportAgentCommand(alias, command),
5270
+ getSession: (alias) => this.sessions.getSession(alias)
5271
+ };
4315
5272
  }
4316
- renderShortcutSessionCreationError(workspace, alias) {
5273
+ createSessionShortcutOps() {
4317
5274
  return {
4318
- text: [
4319
- `会话「${alias}」创建失败。`,
4320
- workspace.reused ? `- 复用工作区:${workspace.name}` : `- 已新增工作区:${workspace.name} -> ${workspace.cwd}`,
4321
- "- 会话未创建,请重试。"
4322
- ].join(`
4323
- `)
5275
+ resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
5276
+ ensureTransportSession: (session) => this.ensureTransportSession(session),
5277
+ checkTransportSession: (session) => this.checkTransportSession(session),
5278
+ refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
4324
5279
  };
4325
5280
  }
5281
+ replaceConfig(updated) {
5282
+ if (!this.config) {
5283
+ return;
5284
+ }
5285
+ this.config.transport = { ...updated.transport };
5286
+ this.config.logging = { ...updated.logging };
5287
+ this.config.wechat = { ...updated.wechat };
5288
+ this.config.agents = { ...updated.agents };
5289
+ this.config.workspaces = { ...updated.workspaces };
5290
+ }
4326
5291
  async executeCommand(chatKey, kind, startedAt, operation) {
4327
5292
  try {
4328
5293
  const response = await operation();
@@ -4345,42 +5310,6 @@ class CommandRouter {
4345
5310
  async ensureTransportSession(session) {
4346
5311
  await this.measureTransportCall("ensure_session", session, () => this.transport.ensureSession(session));
4347
5312
  }
4348
- async resetCurrentSession(chatKey) {
4349
- const session = await this.sessions.getCurrentSession(chatKey);
4350
- if (!session) {
4351
- return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
4352
- }
4353
- const resetSession = this.sessions.resolveSession(session.alias, session.agent, session.workspace, this.buildResetTransportSessionName(session));
4354
- try {
4355
- await this.ensureTransportSession(resetSession);
4356
- const exists = await this.checkTransportSession(resetSession);
4357
- if (!exists) {
4358
- return {
4359
- text: [
4360
- `会话「${session.alias}」重置失败。`,
4361
- "新的后端会话未创建成功,请稍后重试。"
4362
- ].join(`
4363
- `)
4364
- };
4365
- }
4366
- } catch (error) {
4367
- return this.renderTransportError(resetSession, error);
4368
- }
4369
- await this.sessions.attachSession(resetSession.alias, resetSession.agent, resetSession.workspace, resetSession.transportSession);
4370
- await this.refreshSessionTransportAgentCommand(resetSession.alias);
4371
- await this.sessions.useSession(chatKey, resetSession.alias);
4372
- await this.logger.info("session.reset", "reset current logical session", {
4373
- alias: resetSession.alias,
4374
- agent: resetSession.agent,
4375
- workspace: resetSession.workspace,
4376
- transportSession: resetSession.transportSession,
4377
- chatKey
4378
- });
4379
- return { text: `会话「${resetSession.alias}」已重置` };
4380
- }
4381
- buildResetTransportSessionName(session) {
4382
- return `${session.workspace}:${session.alias}:reset-${Date.now()}`;
4383
- }
4384
5313
  async checkTransportSession(session) {
4385
5314
  return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
4386
5315
  }
@@ -4404,18 +5333,6 @@ class CommandRouter {
4404
5333
  }
4405
5334
  await this.sessions.setSessionTransportAgentCommand(alias, transportAgentCommand);
4406
5335
  }
4407
- async tryRecoverMissingSession(session, error) {
4408
- const message = error instanceof Error ? error.message : String(error);
4409
- if (!message.includes("No acpx session found")) {
4410
- return null;
4411
- }
4412
- const transportAgentCommand = await this.resolveSessionAgentCommand(session);
4413
- if (!transportAgentCommand || transportAgentCommand === session.agentCommand) {
4414
- return null;
4415
- }
4416
- await this.sessions.setSessionTransportAgentCommand(session.alias, transportAgentCommand);
4417
- return await this.sessions.getSession(session.alias);
4418
- }
4419
5336
  async measureTransportCall(operation, session, callback) {
4420
5337
  const startedAt = Date.now();
4421
5338
  try {
@@ -4453,91 +5370,20 @@ class CommandRouter {
4453
5370
  }
4454
5371
  }
4455
5372
  }
4456
- async function pathExists(path11) {
4457
- try {
4458
- await access(path11);
4459
- return true;
4460
- } catch {
4461
- return false;
4462
- }
4463
- }
4464
- function normalizePathForWorkspace(path11) {
4465
- const expanded = path11.startsWith("~") ? homedir2() + path11.slice(1) : path11;
4466
- return normalize(expanded);
4467
- }
4468
- function sameWorkspacePath(left, right) {
4469
- const normalizedLeft = normalizePathForWorkspace(left);
4470
- const normalizedRight = normalizePathForWorkspace(right);
4471
- if (process.platform === "win32") {
4472
- return normalizedLeft.toLowerCase() === normalizedRight.toLowerCase();
4473
- }
4474
- return normalizedLeft === normalizedRight;
4475
- }
4476
- function summarizeTransportError(message) {
4477
- return message.replace(/\s+/g, " ").trim().slice(0, 200);
4478
- }
4479
- function summarizeTransportDiagnostic(output) {
4480
- const trimmed = output.replace(/\s+/g, " ").trim();
4481
- if (trimmed.length === 0) {
4482
- return;
4483
- }
4484
- return trimmed.slice(0, 200);
4485
- }
4486
- function summarizeTransportDiagnosticTail(output) {
4487
- const trimmed = output.replace(/\s+/g, " ").trim();
4488
- if (trimmed.length === 0) {
4489
- return;
4490
- }
4491
- return trimmed.slice(-200);
4492
- }
4493
- function summarizeTransportNdjson(output, prefix) {
4494
- const lines = output.split(`
4495
- `).map((line) => line.trim()).filter((line) => line.length > 0);
4496
- if (lines.length === 0) {
4497
- return {};
4498
- }
4499
- const methods = new Set;
4500
- let agentMessageChunkCount = 0;
4501
- let stopReason;
4502
- for (const line of lines) {
4503
- try {
4504
- const payload = JSON.parse(line);
4505
- if (typeof payload.method === "string" && payload.method.length > 0) {
4506
- methods.add(payload.method);
4507
- }
4508
- if (payload.params?.update?.sessionUpdate === "agent_message_chunk") {
4509
- agentMessageChunkCount += 1;
4510
- }
4511
- if (typeof payload.result?.stopReason === "string" && payload.result.stopReason.length > 0) {
4512
- stopReason = payload.result.stopReason;
4513
- }
4514
- } catch {
4515
- continue;
4516
- }
4517
- }
4518
- const summary = {
4519
- [`${prefix}LineCount`]: lines.length
4520
- };
4521
- if (methods.size > 0) {
4522
- summary[`${prefix}Methods`] = [...methods].join(",");
4523
- }
4524
- if (agentMessageChunkCount > 0) {
4525
- summary[`${prefix}AgentMessageChunkCount`] = agentMessageChunkCount;
4526
- }
4527
- if (stopReason) {
4528
- summary[`${prefix}StopReason`] = stopReason;
4529
- }
4530
- return summary;
4531
- }
4532
- function isPartialPromptOutputError(message) {
4533
- return message.includes("未收到最终回复");
4534
- }
4535
5373
  var init_command_router = __esm(() => {
4536
- init_agent_templates();
4537
5374
  init_app_logger();
4538
5375
  init_acpx_session_index();
4539
5376
  init_prompt_output();
4540
5377
  init_parse_command();
5378
+ init_permission_handler();
5379
+ init_config_handler();
5380
+ init_session_handler();
5381
+ init_help_handler();
5382
+ init_agent_handler();
5383
+ init_workspace_handler();
5384
+ init_session_shortcut_handler();
5385
+ init_session_recovery_handler();
5386
+ init_session_reset_handler();
4541
5387
  });
4542
5388
 
4543
5389
  // src/config/resolve-agent-command.ts
@@ -4556,12 +5402,12 @@ function isLegacyCodexCommand(command) {
4556
5402
  }
4557
5403
 
4558
5404
  // src/config/load-config.ts
4559
- import { readFile as readFile4 } from "node:fs/promises";
5405
+ import { readFile as readFile5 } from "node:fs/promises";
4560
5406
  function isRecord(value) {
4561
5407
  return typeof value === "object" && value !== null;
4562
5408
  }
4563
5409
  async function loadConfig(path11, options = {}) {
4564
- const raw = JSON.parse(await readFile4(path11, "utf8"));
5410
+ const raw = JSON.parse(await readFile5(path11, "utf8"));
4565
5411
  return parseConfig(raw, options);
4566
5412
  }
4567
5413
  function parseConfig(raw, options = {}) {
@@ -4581,8 +5427,8 @@ function parseConfig(raw, options = {}) {
4581
5427
  if ("permissionMode" in transport && transport.permissionMode !== "approve-all" && transport.permissionMode !== "approve-reads" && transport.permissionMode !== "deny-all") {
4582
5428
  throw new Error("transport.permissionMode must be approve-all, approve-reads, or deny-all");
4583
5429
  }
4584
- if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "allow" && transport.nonInteractivePermissions !== "deny" && transport.nonInteractivePermissions !== "fail") {
4585
- throw new Error("transport.nonInteractivePermissions must be allow, deny, or fail");
5430
+ if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "deny" && transport.nonInteractivePermissions !== "fail") {
5431
+ throw new Error("transport.nonInteractivePermissions must be deny or fail");
4586
5432
  }
4587
5433
  if (!isRecord(raw.agents)) {
4588
5434
  throw new Error("agents must be an object");
@@ -4591,9 +5437,13 @@ function parseConfig(raw, options = {}) {
4591
5437
  throw new Error("workspaces must be an object");
4592
5438
  }
4593
5439
  const logging = raw.logging;
5440
+ const wechat = raw.wechat;
4594
5441
  if (logging !== undefined && !isRecord(logging)) {
4595
5442
  throw new Error("logging must be an object");
4596
5443
  }
5444
+ if (wechat !== undefined && !isRecord(wechat)) {
5445
+ throw new Error("wechat must be an object");
5446
+ }
4597
5447
  if (isRecord(logging) && "level" in logging && logging.level !== "error" && logging.level !== "info" && logging.level !== "debug") {
4598
5448
  throw new Error("logging.level must be error, info, or debug");
4599
5449
  }
@@ -4602,6 +5452,9 @@ function parseConfig(raw, options = {}) {
4602
5452
  throw new Error(`logging.${field} must be a positive number`);
4603
5453
  }
4604
5454
  }
5455
+ if (isRecord(wechat) && "replyMode" in wechat && wechat.replyMode !== "stream" && wechat.replyMode !== "final") {
5456
+ throw new Error("wechat.replyMode must be stream or final");
5457
+ }
4605
5458
  for (const [name, agent] of Object.entries(raw.agents)) {
4606
5459
  if (!isRecord(agent) || typeof agent.driver !== "string" || agent.driver.length === 0) {
4607
5460
  throw new Error(`agent "${name}" must define a non-empty driver`);
@@ -4638,9 +5491,10 @@ function parseConfig(raw, options = {}) {
4638
5491
  }
4639
5492
  const transportType = transport.type === "acpx-cli" || transport.type === "acpx-bridge" ? transport.type : "acpx-bridge";
4640
5493
  const permissionMode = transport.permissionMode === "approve-all" || transport.permissionMode === "approve-reads" || transport.permissionMode === "deny-all" ? transport.permissionMode : DEFAULT_PERMISSION_MODE;
4641
- const nonInteractivePermissions = transport.nonInteractivePermissions === "allow" || transport.nonInteractivePermissions === "deny" || transport.nonInteractivePermissions === "fail" ? transport.nonInteractivePermissions : DEFAULT_NON_INTERACTIVE_PERMISSIONS;
5494
+ const nonInteractivePermissions = transport.nonInteractivePermissions === "deny" || transport.nonInteractivePermissions === "fail" ? transport.nonInteractivePermissions : DEFAULT_NON_INTERACTIVE_PERMISSIONS;
4642
5495
  const loggingLevel = logging?.level;
4643
5496
  const resolvedLoggingLevel = loggingLevel === "error" || loggingLevel === "info" || loggingLevel === "debug" ? loggingLevel : options.defaultLoggingLevel ?? DEFAULT_LOGGING_CONFIG.level;
5497
+ const replyMode = wechat?.replyMode === "stream" || wechat?.replyMode === "final" ? wechat.replyMode : DEFAULT_WECHAT_REPLY_MODE;
4644
5498
  return {
4645
5499
  transport: {
4646
5500
  ...typeof transport.command === "string" ? { command: transport.command } : {},
@@ -4655,11 +5509,14 @@ function parseConfig(raw, options = {}) {
4655
5509
  maxFiles: typeof logging?.maxFiles === "number" ? logging.maxFiles : DEFAULT_LOGGING_CONFIG.maxFiles,
4656
5510
  retentionDays: typeof logging?.retentionDays === "number" ? logging.retentionDays : DEFAULT_LOGGING_CONFIG.retentionDays
4657
5511
  },
5512
+ wechat: {
5513
+ replyMode
5514
+ },
4658
5515
  agents,
4659
5516
  workspaces
4660
5517
  };
4661
5518
  }
4662
- var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "fail";
5519
+ var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_WECHAT_REPLY_MODE = "stream";
4663
5520
  var init_load_config = __esm(() => {
4664
5521
  DEFAULT_LOGGING_CONFIG = {
4665
5522
  level: "info",
@@ -4670,8 +5527,8 @@ var init_load_config = __esm(() => {
4670
5527
  });
4671
5528
 
4672
5529
  // src/config/config-store.ts
4673
- import { mkdir as mkdir6, writeFile as writeFile4 } from "node:fs/promises";
4674
- import { dirname as dirname5 } from "node:path";
5530
+ import { mkdir as mkdir7, writeFile as writeFile5 } from "node:fs/promises";
5531
+ import { dirname as dirname6 } from "node:path";
4675
5532
 
4676
5533
  class ConfigStore {
4677
5534
  path;
@@ -4682,8 +5539,8 @@ class ConfigStore {
4682
5539
  return await loadConfig(this.path);
4683
5540
  }
4684
5541
  async save(config) {
4685
- await mkdir6(dirname5(this.path), { recursive: true });
4686
- await writeFile4(this.path, `${JSON.stringify(config, null, 2)}
5542
+ await mkdir7(dirname6(this.path), { recursive: true });
5543
+ await writeFile5(this.path, `${JSON.stringify(config, null, 2)}
4687
5544
  `, "utf8");
4688
5545
  }
4689
5546
  async upsertWorkspace(name, cwd, description) {
@@ -4723,13 +5580,22 @@ class ConfigStore {
4723
5580
  await this.save(config);
4724
5581
  return config;
4725
5582
  }
5583
+ async updateWechat(wechat) {
5584
+ const config = await this.load();
5585
+ config.wechat = {
5586
+ ...config.wechat,
5587
+ ...wechat
5588
+ };
5589
+ await this.save(config);
5590
+ return config;
5591
+ }
4726
5592
  }
4727
5593
  var init_config_store = __esm(() => {
4728
5594
  init_load_config();
4729
5595
  });
4730
5596
 
4731
5597
  // src/config/ensure-config.ts
4732
- import { readFile as readFile5 } from "node:fs/promises";
5598
+ import { readFile as readFile6 } from "node:fs/promises";
4733
5599
  async function ensureConfigExists(path11) {
4734
5600
  try {
4735
5601
  await loadConfig(path11);
@@ -4743,7 +5609,10 @@ async function ensureConfigExists(path11) {
4743
5609
  }
4744
5610
  async function loadDefaultConfigTemplate() {
4745
5611
  const templatePath = new URL("../../config.example.json", import.meta.url);
4746
- const template = JSON.parse(await readFile5(templatePath, "utf8"));
5612
+ return normalizeDefaultConfigTemplate(JSON.parse(await readFile6(templatePath, "utf8")));
5613
+ }
5614
+ function normalizeDefaultConfigTemplate(raw) {
5615
+ const template = parseConfig(raw);
4747
5616
  return {
4748
5617
  ...template,
4749
5618
  agents: Object.fromEntries(Object.entries(template.agents).map(([name, agent]) => [
@@ -4888,6 +5757,23 @@ class SessionService {
4888
5757
  session.last_used_at = new Date().toISOString();
4889
5758
  await this.persist();
4890
5759
  }
5760
+ async setCurrentSessionReplyMode(chatKey, replyMode) {
5761
+ const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
5762
+ if (!currentAlias) {
5763
+ throw new Error("no current session selected");
5764
+ }
5765
+ const session = this.state.sessions[currentAlias];
5766
+ if (!session) {
5767
+ throw new Error("no current session selected");
5768
+ }
5769
+ if (replyMode) {
5770
+ session.reply_mode = replyMode;
5771
+ } else {
5772
+ delete session.reply_mode;
5773
+ }
5774
+ session.last_used_at = new Date().toISOString();
5775
+ await this.persist();
5776
+ }
4891
5777
  async getCurrentSession(chatKey) {
4892
5778
  const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
4893
5779
  if (!currentAlias) {
@@ -4912,6 +5798,13 @@ class SessionService {
4912
5798
  }
4913
5799
  toResolvedSession(session) {
4914
5800
  const agentConfig = this.config.agents[session.agent];
5801
+ if (!agentConfig) {
5802
+ throw new Error(`session "${session.alias}" references agent "${session.agent}", but that agent is no longer registered`);
5803
+ }
5804
+ const workspaceConfig = this.config.workspaces[session.workspace];
5805
+ if (!workspaceConfig) {
5806
+ throw new Error(`session "${session.alias}" references workspace "${session.workspace}", but that workspace is no longer registered`);
5807
+ }
4915
5808
  return {
4916
5809
  alias: session.alias,
4917
5810
  agent: session.agent,
@@ -4919,7 +5812,8 @@ class SessionService {
4919
5812
  workspace: session.workspace,
4920
5813
  transportSession: session.transport_session,
4921
5814
  modeId: session.mode_id,
4922
- cwd: this.config.workspaces[session.workspace].cwd
5815
+ replyMode: session.reply_mode,
5816
+ cwd: workspaceConfig.cwd
4923
5817
  };
4924
5818
  }
4925
5819
  async setSessionTransportAgentCommand(alias, transportAgentCommand) {
@@ -4951,6 +5845,7 @@ class SessionService {
4951
5845
  transport_session: transportSession,
4952
5846
  ...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : existingSession?.transport_agent_command ? { transport_agent_command: existingSession.transport_agent_command } : {},
4953
5847
  mode_id: existingSession?.mode_id,
5848
+ reply_mode: existingSession?.reply_mode,
4954
5849
  created_at: existingSession?.created_at ?? now,
4955
5850
  last_used_at: now
4956
5851
  };
@@ -4959,6 +5854,15 @@ class SessionService {
4959
5854
  return this.toResolvedSession(session);
4960
5855
  }
4961
5856
  validateSession(alias, agent, workspace) {
5857
+ if (alias.trim().length === 0) {
5858
+ throw new Error("session alias must be a non-empty string");
5859
+ }
5860
+ if (agent.trim().length === 0) {
5861
+ throw new Error("agent must be a non-empty string");
5862
+ }
5863
+ if (workspace.trim().length === 0) {
5864
+ throw new Error("workspace must be a non-empty string");
5865
+ }
4962
5866
  if (!this.config.workspaces[workspace]) {
4963
5867
  throw new Error(`workspace "${workspace}" is not registered`);
4964
5868
  }
@@ -4978,8 +5882,28 @@ function createEmptyState() {
4978
5882
  }
4979
5883
 
4980
5884
  // src/state/state-store.ts
4981
- import { mkdir as mkdir7, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
4982
- import { dirname as dirname6 } from "node:path";
5885
+ import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
5886
+ import { dirname as dirname7 } from "node:path";
5887
+ function isRecord2(value) {
5888
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5889
+ }
5890
+ function parseState(raw, path11) {
5891
+ if (!isRecord2(raw)) {
5892
+ throw new Error(`state file "${path11}" must contain a JSON object`);
5893
+ }
5894
+ const sessions = raw.sessions;
5895
+ if (!isRecord2(sessions)) {
5896
+ throw new Error(`state file "${path11}" must contain an object field "sessions"`);
5897
+ }
5898
+ const chatContexts = raw.chat_contexts;
5899
+ if (!isRecord2(chatContexts)) {
5900
+ throw new Error(`state file "${path11}" must contain an object field "chat_contexts"`);
5901
+ }
5902
+ return {
5903
+ sessions,
5904
+ chat_contexts: chatContexts
5905
+ };
5906
+ }
4983
5907
 
4984
5908
  class StateStore {
4985
5909
  path;
@@ -4988,11 +5912,19 @@ class StateStore {
4988
5912
  }
4989
5913
  async load() {
4990
5914
  try {
4991
- const content = await readFile6(this.path, "utf8");
5915
+ const content = await readFile7(this.path, "utf8");
4992
5916
  if (content.trim() === "") {
4993
5917
  return createEmptyState();
4994
5918
  }
4995
- return JSON.parse(content);
5919
+ let parsed;
5920
+ try {
5921
+ parsed = JSON.parse(content);
5922
+ } catch (error) {
5923
+ throw new Error(`failed to parse state file "${this.path}"`, {
5924
+ cause: error
5925
+ });
5926
+ }
5927
+ return parseState(parsed, this.path);
4996
5928
  } catch (error) {
4997
5929
  if (error.code === "ENOENT") {
4998
5930
  return createEmptyState();
@@ -5001,8 +5933,8 @@ class StateStore {
5001
5933
  }
5002
5934
  }
5003
5935
  async save(state) {
5004
- await mkdir7(dirname6(this.path), { recursive: true });
5005
- await writeFile5(this.path, JSON.stringify(state, null, 2));
5936
+ await mkdir8(dirname7(this.path), { recursive: true });
5937
+ await writeFile6(this.path, JSON.stringify(state, null, 2));
5006
5938
  }
5007
5939
  }
5008
5940
  var init_state_store = () => {};
@@ -5014,12 +5946,17 @@ __export(exports_run_console, {
5014
5946
  });
5015
5947
  async function runConsole(paths, deps) {
5016
5948
  const runtime = await deps.buildApp(paths);
5949
+ const consumerLock = deps.consumerLock ?? deps.consumerLockFactory?.(runtime);
5017
5950
  const sdk = await deps.loadWeixinSdk();
5018
5951
  const setIntervalFn = deps.setInterval ?? ((fn, delay) => setInterval(fn, delay));
5019
5952
  const clearIntervalFn = deps.clearInterval ?? ((timer) => clearInterval(timer));
5020
5953
  const addProcessListener = deps.addProcessListener ?? ((signal, handler) => process.on(signal, handler));
5021
5954
  const removeProcessListener = deps.removeProcessListener ?? ((signal, handler) => process.off(signal, handler));
5955
+ const processPid = deps.processPid ?? process.pid;
5956
+ const now = deps.now ?? (() => new Date().toISOString());
5957
+ const hostname = deps.hostname ?? (() => "");
5022
5958
  let heartbeatTimer = null;
5959
+ let consumerLockAcquired = false;
5023
5960
  const shutdownController = new AbortController;
5024
5961
  const signalHandler = () => {
5025
5962
  shutdownController.abort();
@@ -5036,6 +5973,53 @@ async function runConsole(paths, deps) {
5036
5973
  deps.daemonRuntime?.heartbeat().catch(() => {});
5037
5974
  }, deps.heartbeatIntervalMs ?? 30000);
5038
5975
  }
5976
+ if (consumerLock) {
5977
+ const lockMeta = {
5978
+ pid: processPid,
5979
+ mode: deps.daemonRuntime ? "daemon" : "foreground",
5980
+ startedAt: now(),
5981
+ configPath: paths.configPath,
5982
+ statePath: paths.statePath,
5983
+ hostname: hostname() || undefined
5984
+ };
5985
+ await runtime.logger.info("weixin.consumer_lock.acquire_attempt", "attempting to acquire weixin consumer lock", {
5986
+ pid: lockMeta.pid,
5987
+ mode: lockMeta.mode,
5988
+ configPath: lockMeta.configPath,
5989
+ statePath: lockMeta.statePath,
5990
+ hostname: lockMeta.hostname
5991
+ });
5992
+ try {
5993
+ await consumerLock.acquire(lockMeta);
5994
+ consumerLockAcquired = true;
5995
+ await runtime.logger.info("weixin.consumer_lock.acquired", "acquired weixin consumer lock", {
5996
+ pid: lockMeta.pid,
5997
+ mode: lockMeta.mode,
5998
+ configPath: lockMeta.configPath,
5999
+ statePath: lockMeta.statePath
6000
+ });
6001
+ } catch (error) {
6002
+ if (error instanceof ActiveWeixinConsumerLockError) {
6003
+ await runtime.logger.error("weixin.consumer_lock.acquire_failed", "weixin consumer lock is already held by another process", {
6004
+ conflictType: "active_lock_holder",
6005
+ activePid: error.existing.pid,
6006
+ activeMode: error.existing.mode,
6007
+ activeConfigPath: error.existing.configPath,
6008
+ activeStatePath: error.existing.statePath,
6009
+ requestedPid: lockMeta.pid,
6010
+ requestedMode: lockMeta.mode
6011
+ });
6012
+ } else {
6013
+ await runtime.logger.error("weixin.consumer_lock.acquire_failed", "failed to acquire weixin consumer lock", {
6014
+ conflictType: deps.daemonRuntime ? "daemon_startup_lock_failure" : "foreground_startup_lock_failure",
6015
+ requestedPid: lockMeta.pid,
6016
+ requestedMode: lockMeta.mode,
6017
+ error: error instanceof Error ? error.message : String(error)
6018
+ });
6019
+ }
6020
+ throw error;
6021
+ }
6022
+ }
5039
6023
  if (!sdk.isLoggedIn()) {
5040
6024
  console.log("[weacpx] 未检测到登录凭证,正在启动扫码登录...");
5041
6025
  await sdk.login();
@@ -5056,17 +6040,30 @@ async function runConsole(paths, deps) {
5056
6040
  if (deps.daemonRuntime) {
5057
6041
  await deps.daemonRuntime.stop();
5058
6042
  }
6043
+ if (consumerLockAcquired) {
6044
+ await consumerLock?.release();
6045
+ await runtime.logger.info("weixin.consumer_lock.released", "released weixin consumer lock", {
6046
+ pid: processPid
6047
+ });
6048
+ }
5059
6049
  if (disposeError) {
5060
6050
  throw disposeError;
5061
6051
  }
5062
6052
  }
5063
6053
  }
6054
+ var init_run_console = __esm(() => {
6055
+ init_consumer_lock();
6056
+ });
5064
6057
 
5065
6058
  // src/transport/acpx-bridge/acpx-bridge-protocol.ts
5066
6059
  function encodeBridgeRequest(request) {
5067
6060
  return `${JSON.stringify(request)}
5068
6061
  `;
5069
6062
  }
6063
+ function encodeBridgePromptSegmentEvent(event) {
6064
+ return `${JSON.stringify(event)}
6065
+ `;
6066
+ }
5070
6067
 
5071
6068
  // src/transport/acpx-bridge/acpx-bridge-client.ts
5072
6069
  import { spawn as spawn2 } from "node:child_process";
@@ -5077,30 +6074,59 @@ class AcpxBridgeClient {
5077
6074
  writeLine;
5078
6075
  nextId = 1;
5079
6076
  pending = new Map;
6077
+ terminalError = null;
5080
6078
  constructor(writeLine) {
5081
6079
  this.writeLine = writeLine;
5082
6080
  }
5083
- request(method, params) {
6081
+ request(method, params, onEvent) {
6082
+ if (this.terminalError) {
6083
+ return Promise.reject(this.terminalError);
6084
+ }
5084
6085
  const id = String(this.nextId);
5085
6086
  this.nextId += 1;
5086
6087
  return awaitable((resolve2, reject) => {
5087
6088
  this.pending.set(id, {
5088
6089
  resolve: (value) => resolve2(value),
5089
- reject
6090
+ reject,
6091
+ onEvent
5090
6092
  });
5091
- this.writeLine(encodeBridgeRequest({
5092
- id,
5093
- method,
5094
- params
5095
- }));
6093
+ try {
6094
+ const didWrite = this.writeLine(encodeBridgeRequest({
6095
+ id,
6096
+ method,
6097
+ params
6098
+ }));
6099
+ if (didWrite === false) {
6100
+ this.pending.delete(id);
6101
+ reject(new Error("bridge write buffer is full"));
6102
+ }
6103
+ } catch (error) {
6104
+ this.pending.delete(id);
6105
+ reject(error);
6106
+ }
5096
6107
  });
5097
6108
  }
5098
6109
  handleLine(line) {
5099
- const response = JSON.parse(line);
5100
- const pending = this.pending.get(response.id);
6110
+ let message;
6111
+ try {
6112
+ message = JSON.parse(line);
6113
+ } catch {
6114
+ return;
6115
+ }
6116
+ const pending = this.pending.get(message.id);
5101
6117
  if (!pending) {
5102
6118
  return;
5103
6119
  }
6120
+ if ("event" in message) {
6121
+ if (message.event === "prompt.segment") {
6122
+ pending.onEvent?.({
6123
+ type: "prompt.segment",
6124
+ text: message.text
6125
+ });
6126
+ }
6127
+ return;
6128
+ }
6129
+ const response = message;
5104
6130
  this.pending.delete(response.id);
5105
6131
  if (response.ok) {
5106
6132
  pending.resolve(response.result);
@@ -5117,6 +6143,7 @@ class AcpxBridgeClient {
5117
6143
  pending.reject(new Error(response.error.message));
5118
6144
  }
5119
6145
  handleExit(error) {
6146
+ this.terminalError = error;
5120
6147
  const pendingRequests = [...this.pending.values()];
5121
6148
  this.pending.clear();
5122
6149
  for (const pending of pendingRequests) {
@@ -5148,13 +6175,11 @@ async function spawnAcpxBridgeClient(options = {}) {
5148
6175
  ...process.env,
5149
6176
  WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
5150
6177
  WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
5151
- WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "fail"
6178
+ WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny"
5152
6179
  },
5153
6180
  stdio: ["pipe", "pipe", "inherit"]
5154
6181
  });
5155
- const client = new AcpxBridgeClient((line) => {
5156
- child.stdin.write(line);
5157
- });
6182
+ const client = new AcpxBridgeClient((line) => child.stdin.write(line));
5158
6183
  const output = createInterface({
5159
6184
  input: child.stdout,
5160
6185
  crlfDelay: Infinity
@@ -5201,10 +6226,14 @@ class AcpxBridgeTransport {
5201
6226
  async ensureSession(session) {
5202
6227
  await this.client.request("ensureSession", this.toParams(session));
5203
6228
  }
5204
- async prompt(session, text, _reply) {
6229
+ async prompt(session, text, reply) {
5205
6230
  return await this.client.request("prompt", {
5206
6231
  ...this.toParams(session),
5207
6232
  text
6233
+ }, (event) => {
6234
+ if (event.type === "prompt.segment") {
6235
+ reply?.(event.text);
6236
+ }
5208
6237
  });
5209
6238
  }
5210
6239
  async setMode(session, modeId) {
@@ -5220,8 +6249,8 @@ class AcpxBridgeTransport {
5220
6249
  const result = await this.client.request("hasSession", this.toParams(session));
5221
6250
  return result.exists;
5222
6251
  }
5223
- async listSessions() {
5224
- return [];
6252
+ async updatePermissionPolicy(policy) {
6253
+ await this.client.request("updatePermissionPolicy", { ...policy });
5225
6254
  }
5226
6255
  async dispose() {
5227
6256
  await this.client.dispose?.();
@@ -5311,12 +6340,12 @@ function parseStreamingChunks(state, line) {
5311
6340
 
5312
6341
  // src/transport/acpx-cli/node-pty-helper.ts
5313
6342
  import { chmod as chmodFs } from "node:fs/promises";
5314
- import { dirname as dirname7, join as join3 } from "node:path";
6343
+ import { dirname as dirname8, join as join4 } from "node:path";
5315
6344
  function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
5316
6345
  if (platform === "win32") {
5317
6346
  return null;
5318
6347
  }
5319
- return join3(dirname7(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
6348
+ return join4(dirname8(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
5320
6349
  }
5321
6350
  async function ensureNodePtyHelperExecutable(helperPath, chmod = chmodFs) {
5322
6351
  if (!helperPath) {
@@ -5404,7 +6433,7 @@ class AcpxCliTransport {
5404
6433
  this.command = options.command ?? "acpx";
5405
6434
  this.sessionInitTimeoutMs = options.sessionInitTimeoutMs ?? 120000;
5406
6435
  this.permissionMode = options.permissionMode ?? "approve-all";
5407
- this.nonInteractivePermissions = options.nonInteractivePermissions ?? "fail";
6436
+ this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
5408
6437
  this.runCommand = runCommand;
5409
6438
  this.runPtyCommand = runPtyCommand;
5410
6439
  }
@@ -5448,6 +6477,10 @@ class AcpxCliTransport {
5448
6477
  message: output.trim()
5449
6478
  };
5450
6479
  }
6480
+ async updatePermissionPolicy(policy) {
6481
+ this.permissionMode = policy.permissionMode;
6482
+ this.nonInteractivePermissions = policy.nonInteractivePermissions;
6483
+ }
5451
6484
  async hasSession(session) {
5452
6485
  const result = await this.runCommand(this.command, this.buildArgs(session, [
5453
6486
  "sessions",
@@ -5456,9 +6489,6 @@ class AcpxCliTransport {
5456
6489
  ]));
5457
6490
  return result.code === 0;
5458
6491
  }
5459
- async listSessions() {
5460
- return [];
5461
- }
5462
6492
  async run(args, options) {
5463
6493
  const result = await this.runCommandWithTimeout(this.runCommand, args, options);
5464
6494
  if (result.code !== 0) {
@@ -5607,8 +6637,8 @@ __export(exports_main, {
5607
6637
  main: () => main2,
5608
6638
  buildApp: () => buildApp
5609
6639
  });
5610
- import { homedir as homedir3 } from "node:os";
5611
- import { dirname as dirname8, join as join4 } from "node:path";
6640
+ import { homedir as homedir5 } from "node:os";
6641
+ import { dirname as dirname9, join as join5 } from "node:path";
5612
6642
  import { fileURLToPath as fileURLToPath3 } from "node:url";
5613
6643
  async function buildApp(paths, deps = {}) {
5614
6644
  await ensureConfigExists(paths.configPath);
@@ -5670,7 +6700,7 @@ async function main2() {
5670
6700
  }
5671
6701
  }
5672
6702
  function resolveRuntimePaths() {
5673
- const home = process.env.HOME ?? homedir3();
6703
+ const home = process.env.HOME ?? homedir5();
5674
6704
  if (!home) {
5675
6705
  throw new Error("Unable to resolve the current user home directory");
5676
6706
  }
@@ -5686,9 +6716,9 @@ function resolveBridgeEntryPath() {
5686
6716
  return fileURLToPath3(new URL("./bridge/bridge-main.ts", import.meta.url));
5687
6717
  }
5688
6718
  function resolveAppLogPath(configPath) {
5689
- const rootDir = dirname8(configPath);
5690
- const runtimeDir = join4(rootDir, "runtime");
5691
- return join4(runtimeDir, "app.log");
6719
+ const rootDir = dirname9(configPath);
6720
+ const runtimeDir = join5(rootDir, "runtime");
6721
+ return join5(runtimeDir, "app.log");
5692
6722
  }
5693
6723
  var init_main = __esm(async () => {
5694
6724
  init_command_router();
@@ -5700,6 +6730,7 @@ var init_main = __esm(async () => {
5700
6730
  init_app_logger();
5701
6731
  init_session_service();
5702
6732
  init_state_store();
6733
+ init_run_console();
5703
6734
  init_acpx_bridge_client();
5704
6735
  init_acpx_cli_transport();
5705
6736
  init_weixin_sdk();
@@ -5707,7 +6738,7 @@ var init_main = __esm(async () => {
5707
6738
  });
5708
6739
 
5709
6740
  // src/cli.ts
5710
- import { homedir as homedir4 } from "node:os";
6741
+ import { homedir as homedir6 } from "node:os";
5711
6742
  import { sep } from "node:path";
5712
6743
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5713
6744
 
@@ -5788,7 +6819,7 @@ class DaemonController {
5788
6819
  return { state: "stopped", stale: true };
5789
6820
  }
5790
6821
  if (!status) {
5791
- return { state: "stopped" };
6822
+ return { state: "indeterminate", pid, reason: "missing-status" };
5792
6823
  }
5793
6824
  return {
5794
6825
  state: "running",
@@ -5801,6 +6832,9 @@ class DaemonController {
5801
6832
  if (current.state === "running") {
5802
6833
  return { state: "already-running", pid: current.pid };
5803
6834
  }
6835
+ if (current.state === "indeterminate") {
6836
+ throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
6837
+ }
5804
6838
  await this.statusStore.clear();
5805
6839
  const pid = await this.deps.spawnDetached();
5806
6840
  await this.writePid(pid);
@@ -6008,27 +7042,30 @@ async function spawnWindowsHiddenProcess(request) {
6008
7042
  async function defaultTerminateProcess(pid) {
6009
7043
  await terminateProcessTree(pid);
6010
7044
  }
6011
- async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand) {
7045
+ async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
7046
+ process.kill(targetPid, signal);
7047
+ }, isProcessRunning = defaultIsProcessRunning) {
6012
7048
  if (platform === "win32") {
6013
7049
  try {
6014
7050
  await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
6015
7051
  } catch {}
6016
7052
  return;
6017
7053
  }
7054
+ const targetPid = pid > 0 ? -pid : pid;
6018
7055
  try {
6019
- process.kill(pid, "SIGTERM");
7056
+ killProcess(targetPid, "SIGTERM");
6020
7057
  } catch {
6021
7058
  return;
6022
7059
  }
6023
7060
  const deadline = Date.now() + 5000;
6024
7061
  while (Date.now() < deadline) {
6025
- if (!defaultIsProcessRunning(pid)) {
7062
+ if (!isProcessRunning(targetPid)) {
6026
7063
  return;
6027
7064
  }
6028
7065
  await new Promise((resolve) => setTimeout(resolve, 100));
6029
7066
  }
6030
7067
  try {
6031
- process.kill(pid, "SIGKILL");
7068
+ killProcess(targetPid, "SIGKILL");
6032
7069
  } catch {}
6033
7070
  }
6034
7071
  async function defaultRunProcessCommand(command, args) {
@@ -6103,6 +7140,7 @@ class DaemonRuntime {
6103
7140
  }
6104
7141
 
6105
7142
  // src/cli.ts
7143
+ init_consumer_lock();
6106
7144
  var HELP_LINES = [
6107
7145
  "用法:",
6108
7146
  "weacpx login - 微信登录",
@@ -6139,6 +7177,11 @@ async function runCli(args, deps = {}) {
6139
7177
  }
6140
7178
  case "status": {
6141
7179
  const status = await controller.getStatus();
7180
+ if (status.state === "indeterminate") {
7181
+ print("weacpx 进程仍在运行,但状态元数据缺失");
7182
+ print(`PID: ${status.pid}`);
7183
+ return 1;
7184
+ }
6142
7185
  if (status.state !== "running") {
6143
7186
  print("weacpx 未运行");
6144
7187
  return 0;
@@ -6182,7 +7225,7 @@ async function defaultRun() {
6182
7225
  const [{ buildApp: buildApp2, resolveRuntimePaths: resolveRuntimePaths2 }, { loadWeixinSdk: loadWeixinSdk2 }, { runConsole: runConsole2 }] = await Promise.all([
6183
7226
  init_main().then(() => exports_main),
6184
7227
  Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk)),
6185
- Promise.resolve().then(() => exports_run_console)
7228
+ Promise.resolve().then(() => (init_run_console(), exports_run_console))
6186
7229
  ]);
6187
7230
  const runtimePaths = resolveRuntimePaths2();
6188
7231
  const daemonPaths = resolveDaemonPaths({ home: requireHome() });
@@ -6192,7 +7235,13 @@ async function defaultRun() {
6192
7235
  defaultLoggingLevel: resolveCliEntryPath().includes(`${sep}src${sep}`) ? "debug" : "info"
6193
7236
  }),
6194
7237
  loadWeixinSdk: loadWeixinSdk2,
6195
- daemonRuntime
7238
+ daemonRuntime,
7239
+ consumerLockFactory: (runtime) => createWeixinConsumerLock({
7240
+ lockFilePath: `${daemonPaths.runtimeDir}${sep}weixin-consumer.lock.json`,
7241
+ onDiagnostic: async (event, context) => {
7242
+ await runtime.logger.info(`weixin.consumer_lock.${event}`, "weixin consumer lock diagnostic", context);
7243
+ }
7244
+ })
6196
7245
  });
6197
7246
  }
6198
7247
  function createDefaultController() {
@@ -6205,7 +7254,7 @@ function createDefaultController() {
6205
7254
  });
6206
7255
  }
6207
7256
  function requireHome() {
6208
- const home = process.env.HOME ?? homedir4();
7257
+ const home = process.env.HOME ?? homedir6();
6209
7258
  if (!home) {
6210
7259
  throw new Error("Unable to resolve the current user home directory");
6211
7260
  }