weacpx 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -155,14 +155,21 @@ bun run dev
155
155
  | `/ss attach <alias> -a <name> --ws <name> --name <transport-session>` | 恢复已存在的会话 |
156
156
  | `/use <alias>` | 切换当前会话 |
157
157
  | `/status` | 查看当前会话状态 |
158
+ | `/mode` | 查看当前会话已保存的 mode |
159
+ | `/mode <id>` | 设置当前会话 mode,例如 `/mode plan` |
160
+ | `/session reset` | 重置当前会话上下文,保留 alias/agent/workspace,但重新绑定到一个新的后端 session |
161
+ | `/clear` | `/session reset` 的快捷别名 |
158
162
  | `/cancel` | 取消当前会话 |
159
- | `/stop` | 停止当前会话 |
163
+ | `/stop` | `/cancel` 的别名,用于取消当前会话 |
160
164
 
161
165
  说明:
162
166
 
163
167
  - `/ss <agent> -d <path>` 是最常用入口,会自动按目录名推导并创建或复用 workspace,再创建或复用 session
164
168
  - `/ss new <agent> -d <path>` 表示强制新建 session
165
169
  - `/use <alias>` 用来切换当前会话
170
+ - `/mode` 会显示当前逻辑会话里保存的 mode;如果还没设置过,会显示“未设置”
171
+ - `/mode <id>` 会把 mode 透传给底层 `acpx set-mode`,成功后再写回当前逻辑会话
172
+ - `/session reset` 和 `/clear` 会保留当前逻辑会话名,但重新创建一个新的后端 session,从空上下文重新开始
166
173
  - 非 `/` 开头的文本会发送到当前 session
167
174
 
168
175
  ### 权限策略
@@ -221,7 +228,7 @@ bun run dev
221
228
 
222
229
  | 命令 | 说明 |
223
230
  |------|------|
224
- | `/clear` | 清除当前聊天会话,上下文重新开始 |
231
+ | `/clear` | 重置当前聊天绑定的会话上下文,效果等同于 `/session reset` |
225
232
  | `/logout` | 清除当前机器上已保存的所有微信账号凭证 |
226
233
 
227
234
  说明:
@@ -327,6 +334,26 @@ bun run dry-run --chat-key wx:test -- \
327
334
  /ss attach demo -a codex --ws backend --name existing-demo
328
335
  ```
329
336
 
337
+ ### Adapter mode 参考
338
+
339
+ `acpx set-mode` / 计划中的 `/mode <id>` 本质上都是给底层 ACP session 发送 `session/set_mode`。
340
+ 这里的 `<id>` 不是 `weacpx` 或 `acpx` 统一规定的枚举,而是**各 adapter 自己定义**的值;填错时通常会收到 adapter 返回的 `Invalid params` 一类错误。
341
+
342
+ 基于 `acpx` 内置 adapter 文档和各上游公开文档,当前能确认的信息如下:
343
+
344
+ | adapter | 已确认可用的 mode id | 说明 |
345
+ |------|------|------|
346
+ | `codex` | `plan` | `acpx` 自身示例明确使用过 `acpx codex set-mode plan`。`codex-acp` 还暴露了 `mode` 运行时配置项,但上游目前没有公开一份完整、稳定的 mode id 列表。 |
347
+ | `cursor` | `agent`、`plan`、`ask` | Cursor 官方文档/更新日志公开提到 `Plan mode`、`Ask mode`;Cursor 官方论坛在 ACP `session/configure` 示例中展示过 `availableModes` 为 `agent` / `plan` / `ask`。 |
348
+ | 其他内置 adapter | 暂无公开、稳定的 mode id 列表 | 包括 `claude`、`copilot`、`gemini`、`qoder`、`qwen`、`kimi`、`kiro`、`iflow`、`opencode`、`trae`、`droid`、`kilocode` 等。即使某些产品本身有“Ask / Agent / Plan”之类概念,其 ACP `set-mode` 可接受的精确字符串也往往没有在官方文档中写死。 |
349
+
350
+ 建议:
351
+
352
+ - 对 `codex`,优先把 `plan` 当作已知可用值。
353
+ - 对 `cursor`,优先使用 `agent`、`plan`、`ask`。
354
+ - 对其他 adapter,不要在 `weacpx` 里写死候选值;最好把 `/mode <id>` 设计成透传,由 adapter 自己决定是否接受。
355
+ - 如果某个 adapter 后续补充了官方 mode 文档,再把它们补进这里。
356
+
330
357
  ## 更多文档
331
358
 
332
359
  - 配置参考:[docs/config-reference.md](./docs/config-reference.md)
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "transport": {
3
3
  "type": "acpx-bridge",
4
- "sessionInitTimeoutMs": 120000
4
+ "sessionInitTimeoutMs": 120000,
5
+ "permissionMode": "approve-all",
6
+ "nonInteractivePermissions": "fail"
5
7
  },
6
8
  "logging": {
7
9
  "level": "info",
@@ -4,25 +4,43 @@ var __getProtoOf = Object.getPrototypeOf;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
7
12
  var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
8
20
  target = mod != null ? __create(__getProtoOf(mod)) : {};
9
21
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
22
  for (let key of __getOwnPropNames(mod))
11
23
  if (!__hasOwnProp.call(to, key))
12
24
  __defProp(to, key, {
13
- get: () => mod[key],
25
+ get: __accessProp.bind(mod, key),
14
26
  enumerable: true
15
27
  });
28
+ if (canCache)
29
+ cache.set(mod, to);
16
30
  return to;
17
31
  };
18
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __returnValue = (v) => v;
34
+ function __exportSetter(name, newValue) {
35
+ this[name] = __returnValue.bind(null, newValue);
36
+ }
19
37
  var __export = (target, all) => {
20
38
  for (var name in all)
21
39
  __defProp(target, name, {
22
40
  get: all[name],
23
41
  enumerable: true,
24
42
  configurable: true,
25
- set: (newValue) => all[name] = () => newValue
43
+ set: __exportSetter.bind(all, name)
26
44
  });
27
45
  };
28
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -244,6 +262,14 @@ class BridgeServer {
244
262
  name: String(params.name),
245
263
  text: String(params.text)
246
264
  });
265
+ case "setMode":
266
+ return await this.runtime.setMode({
267
+ agent: String(params.agent),
268
+ agentCommand: asOptionalString(params.agentCommand),
269
+ cwd: String(params.cwd),
270
+ name: String(params.name),
271
+ modeId: String(params.modeId)
272
+ });
247
273
  case "cancel":
248
274
  return await this.runtime.cancel({
249
275
  agent: String(params.agent),
@@ -322,6 +348,19 @@ class BridgeRuntime {
322
348
  const result = await this.run(spawnSpec.command, spawnSpec.args);
323
349
  return { text: getPromptText(result) };
324
350
  }
351
+ async setMode(input) {
352
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
353
+ "set-mode",
354
+ "-s",
355
+ input.name,
356
+ input.modeId
357
+ ]));
358
+ const result = await this.run(spawnSpec.command, spawnSpec.args);
359
+ if (result.code !== 0) {
360
+ throw new Error(result.stderr || result.stdout || "set-mode failed");
361
+ }
362
+ return {};
363
+ }
325
364
  async cancel(input) {
326
365
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
327
366
  "cancel",
package/dist/cli.js CHANGED
@@ -5,25 +5,43 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
8
13
  var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
9
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
23
  for (let key of __getOwnPropNames(mod))
12
24
  if (!__hasOwnProp.call(to, key))
13
25
  __defProp(to, key, {
14
- get: () => mod[key],
26
+ get: __accessProp.bind(mod, key),
15
27
  enumerable: true
16
28
  });
29
+ if (canCache)
30
+ cache.set(mod, to);
17
31
  return to;
18
32
  };
19
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+ var __returnValue = (v) => v;
35
+ function __exportSetter(name, newValue) {
36
+ this[name] = __returnValue.bind(null, newValue);
37
+ }
20
38
  var __export = (target, all) => {
21
39
  for (var name in all)
22
40
  __defProp(target, name, {
23
41
  get: all[name],
24
42
  enumerable: true,
25
43
  configurable: true,
26
- set: (newValue) => all[name] = () => newValue
44
+ set: __exportSetter.bind(all, name)
27
45
  });
28
46
  };
29
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -2777,7 +2795,7 @@ async function handleSlashCommand(content, ctx, receivedAt, eventTimestamp) {
2777
2795
  return { handled: true };
2778
2796
  }
2779
2797
  case "/clear": {
2780
- ctx.onClear?.();
2798
+ await ctx.onClear?.();
2781
2799
  await sendReply(ctx, "✅ 会话已清除,重新开始对话");
2782
2800
  return { handled: true };
2783
2801
  }
@@ -3338,25 +3356,41 @@ var init_agent_templates = __esm(() => {
3338
3356
  function renderHelpText() {
3339
3357
  return [
3340
3358
  "可用命令:",
3341
- "/agents",
3342
- "/agent add <codex|claude>",
3343
- "/agent rm <name>",
3344
- "/workspaces",
3345
- "/workspace /ws",
3346
- "/ws new <name> -d <path>",
3347
- "/workspace rm <name>",
3348
- "/sessions",
3349
- "/session /ss",
3350
- "/ss <agent> -d <path>",
3351
- "/ss new <agent> -d <path>",
3352
- "/ss new <alias> -a <name> --ws <name>",
3353
- "/ss attach <alias> -a <name> --ws <name> --name <transport-session>",
3354
- "/pm 或 /permission",
3355
- "/pm set <allow|read|deny>",
3356
- "/pm auto [allow|deny|fail]",
3357
- "/use <alias>",
3358
- "/status",
3359
- "/cancel /stop"
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 - 停当前任务"
3360
3394
  ].join(`
3361
3395
  `);
3362
3396
  }
@@ -3497,6 +3531,27 @@ var init_app_logger = __esm(() => {
3497
3531
  };
3498
3532
  });
3499
3533
 
3534
+ // src/transport/acpx-session-index.ts
3535
+ import { readFile as readFile3 } from "node:fs/promises";
3536
+ import { homedir } from "node:os";
3537
+ import { resolve } from "node:path";
3538
+ async function resolveSessionAgentCommandFromIndex(session) {
3539
+ const home = process.env.HOME ?? homedir();
3540
+ if (!home) {
3541
+ return;
3542
+ }
3543
+ try {
3544
+ const raw = await readFile3(resolve(home, ".acpx", "sessions", "index.json"), "utf8");
3545
+ const parsed = JSON.parse(raw);
3546
+ const targetCwd = resolve(session.cwd);
3547
+ const match = parsed.entries?.find((entry) => entry.name === session.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
3548
+ return match?.agentCommand?.trim();
3549
+ } catch {
3550
+ return;
3551
+ }
3552
+ }
3553
+ var init_acpx_session_index = () => {};
3554
+
3500
3555
  // src/transport/prompt-output.ts
3501
3556
  function getPromptText(result) {
3502
3557
  const stdoutOutput = extractPromptOutput(result.stdout);
@@ -3649,12 +3704,18 @@ function parseCommand(input) {
3649
3704
  return { kind: "status" };
3650
3705
  if (command === "/cancel")
3651
3706
  return { kind: "cancel" };
3707
+ if (command === "/clear")
3708
+ return { kind: "session.reset" };
3709
+ if (command === "/mode" && parts.length === 1)
3710
+ return { kind: "mode.show" };
3652
3711
  if (command === "/permission" && parts.length === 1)
3653
3712
  return { kind: "permission.status" };
3654
3713
  if (command === "/session" && parts.length === 1)
3655
3714
  return { kind: "sessions" };
3656
3715
  if (command === "/workspace" && parts.length === 1)
3657
3716
  return { kind: "workspaces" };
3717
+ if (command === "/session" && parts[1] === "reset" && parts.length === 2)
3718
+ return { kind: "session.reset" };
3658
3719
  if (command === "/permission" && parts[1] === "set") {
3659
3720
  const mode = toPermissionMode(parts[2] ?? "");
3660
3721
  if (mode) {
@@ -3673,6 +3734,9 @@ function parseCommand(input) {
3673
3734
  if (command === "/use" && parts[1]) {
3674
3735
  return { kind: "session.use", alias: parts[1] };
3675
3736
  }
3737
+ if (command === "/mode" && parts[1]) {
3738
+ return { kind: "mode.set", modeId: parts[1] };
3739
+ }
3676
3740
  if (command === "/agent" && parts[1] === "add" && parts[2]) {
3677
3741
  return { kind: "agent.add", template: parts[2] };
3678
3742
  }
@@ -3714,7 +3778,7 @@ function parseCommand(input) {
3714
3778
  return { kind: "session.shortcut.new", agent: parts[2], cwd };
3715
3779
  }
3716
3780
  }
3717
- if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach") {
3781
+ if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach" && parts[1] !== "reset") {
3718
3782
  const cwd = readFlagValue(parts, ["--cwd", "-d"]);
3719
3783
  if (cwd) {
3720
3784
  return { kind: "session.shortcut", agent: parts[1], cwd };
@@ -3824,6 +3888,8 @@ var init_parse_command = __esm(() => {
3824
3888
  "/sessions",
3825
3889
  "/status",
3826
3890
  "/cancel",
3891
+ "/clear",
3892
+ "/mode",
3827
3893
  "/permission",
3828
3894
  "/session",
3829
3895
  "/workspace",
@@ -3835,19 +3901,21 @@ var init_parse_command = __esm(() => {
3835
3901
  // src/commands/command-router.ts
3836
3902
  import { access } from "node:fs/promises";
3837
3903
  import { basename as basename2, normalize } from "node:path";
3838
- import { homedir } from "node:os";
3904
+ import { homedir as homedir2 } from "node:os";
3839
3905
 
3840
3906
  class CommandRouter {
3841
3907
  sessions;
3842
3908
  transport;
3843
3909
  config;
3844
3910
  configStore;
3911
+ resolveSessionAgentCommand;
3845
3912
  logger;
3846
- constructor(sessions, transport, config, configStore, logger2) {
3913
+ constructor(sessions, transport, config, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex) {
3847
3914
  this.sessions = sessions;
3848
3915
  this.transport = transport;
3849
3916
  this.config = config;
3850
3917
  this.configStore = configStore;
3918
+ this.resolveSessionAgentCommand = resolveSessionAgentCommand;
3851
3919
  this.logger = logger2 ?? createNoopAppLogger();
3852
3920
  }
3853
3921
  async handle(chatKey, input, reply) {
@@ -3970,6 +4038,7 @@ class CommandRouter {
3970
4038
  return this.renderSessionCreationError(session, error);
3971
4039
  }
3972
4040
  await this.sessions.attachSession(command.alias, command.agent, command.workspace, session.transportSession);
4041
+ await this.refreshSessionTransportAgentCommand(command.alias);
3973
4042
  await this.sessions.useSession(chatKey, command.alias);
3974
4043
  await this.logger.info("session.created", "created and selected logical session", {
3975
4044
  alias: command.alias,
@@ -3995,6 +4064,7 @@ class CommandRouter {
3995
4064
  };
3996
4065
  }
3997
4066
  await this.sessions.attachSession(command.alias, command.agent, command.workspace, command.transportSession);
4067
+ await this.refreshSessionTransportAgentCommand(command.alias);
3998
4068
  await this.sessions.useSession(chatKey, command.alias);
3999
4069
  await this.logger.info("session.attached", "attached existing transport session", {
4000
4070
  alias: command.alias,
@@ -4011,6 +4081,29 @@ class CommandRouter {
4011
4081
  chatKey
4012
4082
  });
4013
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(`
4095
+ `)
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
+ }
4014
4107
  case "status": {
4015
4108
  const session = await this.sessions.getCurrentSession(chatKey);
4016
4109
  if (!session) {
@@ -4038,6 +4131,8 @@ class CommandRouter {
4038
4131
  return this.renderTransportError(session, error);
4039
4132
  }
4040
4133
  }
4134
+ case "session.reset":
4135
+ return await this.resetCurrentSession(chatKey);
4041
4136
  case "prompt": {
4042
4137
  const session = await this.sessions.getCurrentSession(chatKey);
4043
4138
  if (!session) {
@@ -4047,12 +4142,20 @@ class CommandRouter {
4047
4142
  const result = await this.promptTransportSession(session, command.text, reply);
4048
4143
  return { text: result.text };
4049
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
+ }
4050
4150
  return this.renderTransportError(session, error);
4051
4151
  }
4052
4152
  }
4053
4153
  }
4054
4154
  });
4055
4155
  }
4156
+ async clearSession(chatKey) {
4157
+ await this.resetCurrentSession(chatKey);
4158
+ }
4056
4159
  async handleSessionShortcut(chatKey, agent, cwdInput, createNew) {
4057
4160
  if (!this.config || !this.configStore) {
4058
4161
  return { text: "当前没有加载可写入的配置。" };
@@ -4096,6 +4199,7 @@ class CommandRouter {
4096
4199
  return this.renderShortcutSessionCreationError(workspace, alias);
4097
4200
  }
4098
4201
  await this.sessions.attachSession(alias, agent, workspace.name, session.transportSession);
4202
+ await this.refreshSessionTransportAgentCommand(alias);
4099
4203
  await this.sessions.useSession(chatKey, alias);
4100
4204
  await this.logger.info("session.shortcut.created", "created new logical session from shortcut", {
4101
4205
  alias,
@@ -4241,15 +4345,77 @@ class CommandRouter {
4241
4345
  async ensureTransportSession(session) {
4242
4346
  await this.measureTransportCall("ensure_session", session, () => this.transport.ensureSession(session));
4243
4347
  }
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
+ }
4244
4384
  async checkTransportSession(session) {
4245
4385
  return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
4246
4386
  }
4247
4387
  async promptTransportSession(session, text, reply) {
4248
4388
  return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text, reply));
4249
4389
  }
4390
+ async setModeTransportSession(session, modeId) {
4391
+ return await this.measureTransportCall("set_mode", session, () => this.transport.setMode(session, modeId));
4392
+ }
4250
4393
  async cancelTransportSession(session) {
4251
4394
  return await this.measureTransportCall("cancel", session, () => this.transport.cancel(session));
4252
4395
  }
4396
+ async refreshSessionTransportAgentCommand(alias) {
4397
+ const session = await this.sessions.getSession(alias);
4398
+ if (!session) {
4399
+ return;
4400
+ }
4401
+ const transportAgentCommand = await this.resolveSessionAgentCommand(session);
4402
+ if (!transportAgentCommand) {
4403
+ return;
4404
+ }
4405
+ await this.sessions.setSessionTransportAgentCommand(alias, transportAgentCommand);
4406
+ }
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
+ }
4253
4419
  async measureTransportCall(operation, session, callback) {
4254
4420
  const startedAt = Date.now();
4255
4421
  try {
@@ -4296,7 +4462,7 @@ async function pathExists(path11) {
4296
4462
  }
4297
4463
  }
4298
4464
  function normalizePathForWorkspace(path11) {
4299
- const expanded = path11.startsWith("~") ? homedir() + path11.slice(1) : path11;
4465
+ const expanded = path11.startsWith("~") ? homedir2() + path11.slice(1) : path11;
4300
4466
  return normalize(expanded);
4301
4467
  }
4302
4468
  function sameWorkspacePath(left, right) {
@@ -4369,6 +4535,7 @@ function isPartialPromptOutputError(message) {
4369
4535
  var init_command_router = __esm(() => {
4370
4536
  init_agent_templates();
4371
4537
  init_app_logger();
4538
+ init_acpx_session_index();
4372
4539
  init_prompt_output();
4373
4540
  init_parse_command();
4374
4541
  });
@@ -4389,12 +4556,12 @@ function isLegacyCodexCommand(command) {
4389
4556
  }
4390
4557
 
4391
4558
  // src/config/load-config.ts
4392
- import { readFile as readFile3 } from "node:fs/promises";
4559
+ import { readFile as readFile4 } from "node:fs/promises";
4393
4560
  function isRecord(value) {
4394
4561
  return typeof value === "object" && value !== null;
4395
4562
  }
4396
4563
  async function loadConfig(path11, options = {}) {
4397
- const raw = JSON.parse(await readFile3(path11, "utf8"));
4564
+ const raw = JSON.parse(await readFile4(path11, "utf8"));
4398
4565
  return parseConfig(raw, options);
4399
4566
  }
4400
4567
  function parseConfig(raw, options = {}) {
@@ -4562,7 +4729,7 @@ var init_config_store = __esm(() => {
4562
4729
  });
4563
4730
 
4564
4731
  // src/config/ensure-config.ts
4565
- import { readFile as readFile4 } from "node:fs/promises";
4732
+ import { readFile as readFile5 } from "node:fs/promises";
4566
4733
  async function ensureConfigExists(path11) {
4567
4734
  try {
4568
4735
  await loadConfig(path11);
@@ -4576,7 +4743,7 @@ async function ensureConfigExists(path11) {
4576
4743
  }
4577
4744
  async function loadDefaultConfigTemplate() {
4578
4745
  const templatePath = new URL("../../config.example.json", import.meta.url);
4579
- const template = JSON.parse(await readFile4(templatePath, "utf8"));
4746
+ const template = JSON.parse(await readFile5(templatePath, "utf8"));
4580
4747
  return {
4581
4748
  ...template,
4582
4749
  agents: Object.fromEntries(Object.entries(template.agents).map(([name, agent]) => [
@@ -4644,6 +4811,9 @@ class ConsoleAgent {
4644
4811
  });
4645
4812
  return await this.router.handle(request.conversationId, request.text, request.reply);
4646
4813
  }
4814
+ async clearSession(conversationId) {
4815
+ await this.router.clearSession?.(conversationId);
4816
+ }
4647
4817
  }
4648
4818
  function summarizeText(text) {
4649
4819
  const trimmed = text.trim();
@@ -4676,12 +4846,20 @@ class SessionService {
4676
4846
  agent,
4677
4847
  workspace,
4678
4848
  transport_session: transportSession,
4849
+ transport_agent_command: this.state.sessions[alias]?.transport_agent_command,
4679
4850
  created_at: this.state.sessions[alias]?.created_at ?? new Date().toISOString(),
4680
4851
  last_used_at: new Date().toISOString()
4681
4852
  });
4682
4853
  }
4683
- async attachSession(alias, agent, workspace, transportSession) {
4684
- return await this.createLogicalSession(alias, agent, workspace, transportSession);
4854
+ async attachSession(alias, agent, workspace, transportSession, transportAgentCommand) {
4855
+ return await this.createLogicalSession(alias, agent, workspace, transportSession, transportAgentCommand);
4856
+ }
4857
+ async getSession(alias) {
4858
+ const session = this.state.sessions[alias];
4859
+ if (!session) {
4860
+ return null;
4861
+ }
4862
+ return this.toResolvedSession(session);
4685
4863
  }
4686
4864
  async useSession(chatKey, alias) {
4687
4865
  const session = this.state.sessions[alias];
@@ -4692,6 +4870,24 @@ class SessionService {
4692
4870
  this.state.chat_contexts[chatKey] = { current_session: alias };
4693
4871
  await this.persist();
4694
4872
  }
4873
+ async setCurrentSessionMode(chatKey, modeId) {
4874
+ const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
4875
+ if (!currentAlias) {
4876
+ throw new Error("no current session selected");
4877
+ }
4878
+ const session = this.state.sessions[currentAlias];
4879
+ if (!session) {
4880
+ throw new Error("no current session selected");
4881
+ }
4882
+ const normalizedModeId = modeId?.trim();
4883
+ if (normalizedModeId) {
4884
+ session.mode_id = normalizedModeId;
4885
+ } else {
4886
+ delete session.mode_id;
4887
+ }
4888
+ session.last_used_at = new Date().toISOString();
4889
+ await this.persist();
4890
+ }
4695
4891
  async getCurrentSession(chatKey) {
4696
4892
  const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
4697
4893
  if (!currentAlias) {
@@ -4719,24 +4915,42 @@ class SessionService {
4719
4915
  return {
4720
4916
  alias: session.alias,
4721
4917
  agent: session.agent,
4722
- agentCommand: resolveAgentCommand(agentConfig.driver, agentConfig.command),
4918
+ agentCommand: session.transport_agent_command ?? resolveAgentCommand(agentConfig.driver, agentConfig.command),
4723
4919
  workspace: session.workspace,
4724
4920
  transportSession: session.transport_session,
4921
+ modeId: session.mode_id,
4725
4922
  cwd: this.config.workspaces[session.workspace].cwd
4726
4923
  };
4727
4924
  }
4925
+ async setSessionTransportAgentCommand(alias, transportAgentCommand) {
4926
+ const session = this.state.sessions[alias];
4927
+ if (!session) {
4928
+ throw new Error(`session "${alias}" does not exist`);
4929
+ }
4930
+ const normalized = transportAgentCommand?.trim();
4931
+ if (normalized) {
4932
+ session.transport_agent_command = normalized;
4933
+ } else {
4934
+ delete session.transport_agent_command;
4935
+ }
4936
+ session.last_used_at = new Date().toISOString();
4937
+ await this.persist();
4938
+ }
4728
4939
  async persist() {
4729
4940
  await this.stateStore.save(this.state);
4730
4941
  }
4731
- async createLogicalSession(alias, agent, workspace, transportSession) {
4942
+ async createLogicalSession(alias, agent, workspace, transportSession, transportAgentCommand) {
4732
4943
  this.validateSession(alias, agent, workspace);
4733
4944
  const existingSession = this.state.sessions[alias];
4734
4945
  const now = new Date().toISOString();
4946
+ const normalizedTransportAgentCommand = transportAgentCommand?.trim();
4735
4947
  const session = {
4736
4948
  alias,
4737
4949
  agent,
4738
4950
  workspace,
4739
4951
  transport_session: transportSession,
4952
+ ...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : existingSession?.transport_agent_command ? { transport_agent_command: existingSession.transport_agent_command } : {},
4953
+ mode_id: existingSession?.mode_id,
4740
4954
  created_at: existingSession?.created_at ?? now,
4741
4955
  last_used_at: now
4742
4956
  };
@@ -4764,7 +4978,7 @@ function createEmptyState() {
4764
4978
  }
4765
4979
 
4766
4980
  // src/state/state-store.ts
4767
- import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
4981
+ import { mkdir as mkdir7, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
4768
4982
  import { dirname as dirname6 } from "node:path";
4769
4983
 
4770
4984
  class StateStore {
@@ -4774,7 +4988,7 @@ class StateStore {
4774
4988
  }
4775
4989
  async load() {
4776
4990
  try {
4777
- const content = await readFile5(this.path, "utf8");
4991
+ const content = await readFile6(this.path, "utf8");
4778
4992
  if (content.trim() === "") {
4779
4993
  return createEmptyState();
4780
4994
  }
@@ -4803,7 +5017,15 @@ async function runConsole(paths, deps) {
4803
5017
  const sdk = await deps.loadWeixinSdk();
4804
5018
  const setIntervalFn = deps.setInterval ?? ((fn, delay) => setInterval(fn, delay));
4805
5019
  const clearIntervalFn = deps.clearInterval ?? ((timer) => clearInterval(timer));
5020
+ const addProcessListener = deps.addProcessListener ?? ((signal, handler) => process.on(signal, handler));
5021
+ const removeProcessListener = deps.removeProcessListener ?? ((signal, handler) => process.off(signal, handler));
4806
5022
  let heartbeatTimer = null;
5023
+ const shutdownController = new AbortController;
5024
+ const signalHandler = () => {
5025
+ shutdownController.abort();
5026
+ };
5027
+ addProcessListener("SIGINT", signalHandler);
5028
+ addProcessListener("SIGTERM", signalHandler);
4807
5029
  try {
4808
5030
  if (deps.daemonRuntime) {
4809
5031
  await deps.daemonRuntime.start({
@@ -4818,9 +5040,11 @@ async function runConsole(paths, deps) {
4818
5040
  console.log("[weacpx] 未检测到登录凭证,正在启动扫码登录...");
4819
5041
  await sdk.login();
4820
5042
  }
4821
- await sdk.start(runtime.agent);
5043
+ await sdk.start(runtime.agent, { abortSignal: shutdownController.signal });
4822
5044
  } finally {
4823
5045
  let disposeError = null;
5046
+ removeProcessListener("SIGINT", signalHandler);
5047
+ removeProcessListener("SIGTERM", signalHandler);
4824
5048
  if (heartbeatTimer !== null) {
4825
5049
  clearIntervalFn(heartbeatTimer);
4826
5050
  }
@@ -4859,9 +5083,9 @@ class AcpxBridgeClient {
4859
5083
  request(method, params) {
4860
5084
  const id = String(this.nextId);
4861
5085
  this.nextId += 1;
4862
- return awaitable((resolve, reject) => {
5086
+ return awaitable((resolve2, reject) => {
4863
5087
  this.pending.set(id, {
4864
- resolve: (value) => resolve(value),
5088
+ resolve: (value) => resolve2(value),
4865
5089
  reject
4866
5090
  });
4867
5091
  this.writeLine(encodeBridgeRequest({
@@ -4960,8 +5184,8 @@ async function spawnAcpxBridgeClient(options = {}) {
4960
5184
  return client;
4961
5185
  }
4962
5186
  function awaitable(executor) {
4963
- return new Promise((resolve, reject) => {
4964
- executor(resolve, reject);
5187
+ return new Promise((resolve2, reject) => {
5188
+ executor(resolve2, reject);
4965
5189
  });
4966
5190
  }
4967
5191
  var init_acpx_bridge_client = __esm(() => {
@@ -4983,6 +5207,12 @@ class AcpxBridgeTransport {
4983
5207
  text
4984
5208
  });
4985
5209
  }
5210
+ async setMode(session, modeId) {
5211
+ await this.client.request("setMode", {
5212
+ ...this.toParams(session),
5213
+ modeId
5214
+ });
5215
+ }
4986
5216
  async cancel(session) {
4987
5217
  return await this.client.request("cancel", this.toParams(session));
4988
5218
  }
@@ -5108,7 +5338,7 @@ import { createRequire as createRequire3 } from "node:module";
5108
5338
  import { spawn as spawn3 } from "node:child_process";
5109
5339
  import { spawn as spawnPty } from "node-pty";
5110
5340
  async function defaultRunner(command, args, options) {
5111
- return await new Promise((resolve, reject) => {
5341
+ return await new Promise((resolve2, reject) => {
5112
5342
  const spawnSpec = resolveSpawnCommand(command, args);
5113
5343
  const child = spawn3(spawnSpec.command, spawnSpec.args, { stdio: ["ignore", "pipe", "pipe"] });
5114
5344
  let stdout = "";
@@ -5131,14 +5361,14 @@ async function defaultRunner(command, args, options) {
5131
5361
  child.on("close", (code) => {
5132
5362
  if (timeoutId)
5133
5363
  clearTimeout(timeoutId);
5134
- resolve({ code: code ?? 1, stdout, stderr });
5364
+ resolve2({ code: code ?? 1, stdout, stderr });
5135
5365
  });
5136
5366
  });
5137
5367
  }
5138
5368
  async function defaultPtyRunner(command, args, options) {
5139
5369
  const helperPath = resolveNodePtyHelperPath(require3.resolve("node-pty/package.json"), process.platform, process.arch);
5140
5370
  await ensureNodePtyHelperExecutable(helperPath);
5141
- return await new Promise((resolve, reject) => {
5371
+ return await new Promise((resolve2, reject) => {
5142
5372
  const spawnSpec = resolveSpawnCommand(command, args);
5143
5373
  const child = spawnPty(spawnSpec.command, spawnSpec.args, {
5144
5374
  name: "xterm-color",
@@ -5158,7 +5388,7 @@ async function defaultPtyRunner(command, args, options) {
5158
5388
  child.onExit(({ exitCode }) => {
5159
5389
  if (timeoutId)
5160
5390
  clearTimeout(timeoutId);
5161
- resolve({ code: exitCode, stdout: output, stderr: "" });
5391
+ resolve2({ code: exitCode, stdout: output, stderr: "" });
5162
5392
  });
5163
5393
  });
5164
5394
  }
@@ -5199,6 +5429,14 @@ class AcpxCliTransport {
5199
5429
  const result = await this.runCommand(this.command, args);
5200
5430
  return { text: getPromptText(result) };
5201
5431
  }
5432
+ async setMode(session, modeId) {
5433
+ await this.run(this.buildArgs(session, [
5434
+ "set-mode",
5435
+ "-s",
5436
+ session.transportSession,
5437
+ modeId
5438
+ ]));
5439
+ }
5202
5440
  async cancel(session) {
5203
5441
  const output = await this.run(this.buildArgs(session, [
5204
5442
  "cancel",
@@ -5256,7 +5494,7 @@ class AcpxCliTransport {
5256
5494
  ]);
5257
5495
  }
5258
5496
  async runStreamingPrompt(command, args, reply, maxSegmentWaitMs = 30000) {
5259
- return await new Promise((resolve, reject) => {
5497
+ return await new Promise((resolve2, reject) => {
5260
5498
  const spawnSpec = resolveSpawnCommand(command, args);
5261
5499
  const child = spawn3(spawnSpec.command, spawnSpec.args, { stdio: ["ignore", "pipe", "pipe"] });
5262
5500
  let stdout = "";
@@ -5298,7 +5536,7 @@ class AcpxCliTransport {
5298
5536
  if (remaining.length > 0) {
5299
5537
  reply(remaining).catch(() => {});
5300
5538
  }
5301
- resolve({ code: code ?? 1, stdout, stderr });
5539
+ resolve2({ code: code ?? 1, stdout, stderr });
5302
5540
  });
5303
5541
  });
5304
5542
  }
@@ -5369,7 +5607,7 @@ __export(exports_main, {
5369
5607
  main: () => main2,
5370
5608
  buildApp: () => buildApp
5371
5609
  });
5372
- import { homedir as homedir2 } from "node:os";
5610
+ import { homedir as homedir3 } from "node:os";
5373
5611
  import { dirname as dirname8, join as join4 } from "node:path";
5374
5612
  import { fileURLToPath as fileURLToPath3 } from "node:url";
5375
5613
  async function buildApp(paths, deps = {}) {
@@ -5432,7 +5670,7 @@ async function main2() {
5432
5670
  }
5433
5671
  }
5434
5672
  function resolveRuntimePaths() {
5435
- const home = process.env.HOME ?? homedir2();
5673
+ const home = process.env.HOME ?? homedir3();
5436
5674
  if (!home) {
5437
5675
  throw new Error("Unable to resolve the current user home directory");
5438
5676
  }
@@ -5469,7 +5707,7 @@ var init_main = __esm(async () => {
5469
5707
  });
5470
5708
 
5471
5709
  // src/cli.ts
5472
- import { homedir as homedir3 } from "node:os";
5710
+ import { homedir as homedir4 } from "node:os";
5473
5711
  import { sep } from "node:path";
5474
5712
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5475
5713
 
@@ -5521,15 +5759,23 @@ class DaemonController {
5521
5759
  startupPollIntervalMs;
5522
5760
  startupTimeoutMs;
5523
5761
  onStartupPoll;
5762
+ shutdownPollIntervalMs;
5763
+ shutdownTimeoutMs;
5764
+ onShutdownPoll;
5524
5765
  constructor(paths, deps) {
5525
5766
  this.paths = paths;
5526
5767
  this.deps = deps;
5527
5768
  this.statusStore = new DaemonStatusStore(paths.statusFile);
5528
5769
  this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
5529
5770
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
5771
+ this.shutdownPollIntervalMs = deps.shutdownPollIntervalMs ?? 50;
5772
+ this.shutdownTimeoutMs = deps.shutdownTimeoutMs ?? 5000;
5530
5773
  this.onStartupPoll = deps.onStartupPoll ?? (async () => {
5531
5774
  await new Promise((resolve) => setTimeout(resolve, this.startupPollIntervalMs));
5532
5775
  });
5776
+ this.onShutdownPoll = deps.onShutdownPoll ?? (async () => {
5777
+ await new Promise((resolve) => setTimeout(resolve, this.shutdownPollIntervalMs));
5778
+ });
5533
5779
  }
5534
5780
  async getStatus() {
5535
5781
  const pid = await this.loadPid();
@@ -5568,6 +5814,7 @@ class DaemonController {
5568
5814
  }
5569
5815
  if (this.deps.isProcessRunning(pid)) {
5570
5816
  await this.deps.terminateProcess(pid);
5817
+ await this.waitForShutdown(pid);
5571
5818
  }
5572
5819
  await this.clearRuntimeFiles();
5573
5820
  return { state: "stopped", detail: "stopped" };
@@ -5608,6 +5855,19 @@ class DaemonController {
5608
5855
  }
5609
5856
  throw new Error(`weacpx daemon did not report ready state within ${this.startupTimeoutMs}ms (pid ${pid})`);
5610
5857
  }
5858
+ async waitForShutdown(pid) {
5859
+ const deadline = Date.now() + this.shutdownTimeoutMs;
5860
+ while (Date.now() < deadline) {
5861
+ if (!this.deps.isProcessRunning(pid)) {
5862
+ return;
5863
+ }
5864
+ await this.onShutdownPoll();
5865
+ }
5866
+ if (!this.deps.isProcessRunning(pid)) {
5867
+ return;
5868
+ }
5869
+ throw new Error(`weacpx daemon did not exit within ${this.shutdownTimeoutMs}ms (pid ${pid})`);
5870
+ }
5611
5871
  }
5612
5872
 
5613
5873
  // src/daemon/create-daemon-controller.ts
@@ -5843,7 +6103,15 @@ class DaemonRuntime {
5843
6103
  }
5844
6104
 
5845
6105
  // src/cli.ts
5846
- var HELP_LINES = ["用法:", "weacpx login", "weacpx logout", "weacpx run", "weacpx start", "weacpx status", "weacpx stop"];
6106
+ var HELP_LINES = [
6107
+ "用法:",
6108
+ "weacpx login - 微信登录",
6109
+ "weacpx logout - 退出登录",
6110
+ "weacpx run - 前台运行",
6111
+ "weacpx start - 后台启动",
6112
+ "weacpx status - 查看状态",
6113
+ "weacpx stop - 停止服务"
6114
+ ];
5847
6115
  async function runCli(args, deps = {}) {
5848
6116
  const command = args[0];
5849
6117
  const print = deps.print ?? ((line) => console.log(line));
@@ -5937,7 +6205,7 @@ function createDefaultController() {
5937
6205
  });
5938
6206
  }
5939
6207
  function requireHome() {
5940
- const home = process.env.HOME ?? homedir3();
6208
+ const home = process.env.HOME ?? homedir4();
5941
6209
  if (!home) {
5942
6210
  throw new Error("Unable to resolve the current user home directory");
5943
6211
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weacpx",
3
3
  "description": "用微信远程控制 `acpx` 会话的控制台, 底层基于 `weixin-agent-sdk` 与 `acpx`",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "main": "index.js",
6
6
  "directories": {
7
7
  "doc": "docs",
@@ -46,7 +46,7 @@
46
46
  "test:smoke": "node ./scripts/run-tests.mjs tests/smoke"
47
47
  },
48
48
  "dependencies": {
49
- "acpx": "^0.3.1",
49
+ "acpx": "^0.4.0",
50
50
  "node-pty": "^1.1.0",
51
51
  "qrcode-terminal": "^0.12.0"
52
52
  },
@@ -55,5 +55,10 @@
55
55
  },
56
56
  "engines": {
57
57
  "node": ">=22"
58
+ },
59
+ "devDependencies": {
60
+ "@types/bun": "^1.3.11",
61
+ "bun-types": "^1.3.11",
62
+ "typescript": "^6.0.2"
58
63
  }
59
64
  }