lark-remote 0.4.2 → 0.4.4

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/bridge/approval-coordinator.d.ts +2 -0
  3. package/dist/bridge/approval-coordinator.js +16 -7
  4. package/dist/bridge/index.d.ts +22 -0
  5. package/dist/bridge/index.js +138 -56
  6. package/dist/card/card-budget.js +39 -26
  7. package/dist/card/run-renderer.js +9 -2
  8. package/dist/card/run-state.js +81 -31
  9. package/dist/card/text-truncate.d.ts +9 -10
  10. package/dist/card/text-truncate.js +48 -43
  11. package/dist/card/tool-render.js +6 -2
  12. package/dist/cli.js +17 -0
  13. package/dist/config/codex-config.d.ts +3 -0
  14. package/dist/config/codex-config.js +4 -1
  15. package/dist/config/dir.js +6 -6
  16. package/dist/config/index.d.ts +7 -5
  17. package/dist/config/index.js +52 -13
  18. package/dist/config/kimi-config.d.ts +4 -1
  19. package/dist/config/kimi-config.js +5 -2
  20. package/dist/config/pi-config.d.ts +2 -7
  21. package/dist/config/pi-config.js +6 -6
  22. package/dist/config/wizard.js +8 -4
  23. package/dist/connector/index.d.ts +19 -0
  24. package/dist/connector/index.js +134 -23
  25. package/dist/inbound/placeholder.d.ts +3 -2
  26. package/dist/inbound/placeholder.js +17 -7
  27. package/dist/index.js +28 -6
  28. package/dist/instance-lock.d.ts +31 -15
  29. package/dist/instance-lock.js +64 -62
  30. package/dist/platform/agent-stopper.d.ts +29 -7
  31. package/dist/platform/agent-stopper.js +40 -8
  32. package/dist/platform/identity.d.ts +59 -3
  33. package/dist/platform/identity.js +105 -30
  34. package/dist/platform/path.d.ts +0 -7
  35. package/dist/platform/path.js +1 -51
  36. package/dist/platform/shell.d.ts +14 -0
  37. package/dist/platform/shell.js +35 -2
  38. package/dist/platform/sleep-blocker.d.ts +6 -0
  39. package/dist/platform/sleep-blocker.js +31 -5
  40. package/dist/platform/spawn.d.ts +1 -1
  41. package/dist/platform/spawn.js +1 -1
  42. package/dist/platform/terminator-posix.d.ts +1 -29
  43. package/dist/platform/terminator-posix.js +32 -30
  44. package/dist/platform/terminator-win32.js +4 -2
  45. package/dist/platform/terminator.d.ts +2 -2
  46. package/dist/platform/terminator.js +17 -2
  47. package/dist/platform/types.d.ts +3 -0
  48. package/dist/router/card-action-feedback.d.ts +17 -0
  49. package/dist/router/card-action-feedback.js +31 -0
  50. package/dist/router/config/claude.js +8 -5
  51. package/dist/router/index.d.ts +2 -0
  52. package/dist/router/index.js +35 -15
  53. package/dist/runner/claude/runner.js +1 -1
  54. package/dist/runner/claude/session.d.ts +2 -6
  55. package/dist/runner/claude/session.js +7 -8
  56. package/dist/runner/codex/app-server/protocol-types.d.ts +4 -0
  57. package/dist/runner/codex/app-server/protocol-types.js +4 -0
  58. package/dist/runner/codex/app-server/runner.js +3 -0
  59. package/dist/runner/common/acp/base-acp-runner.js +7 -0
  60. package/dist/runner/common/acp/base-translator.d.ts +43 -0
  61. package/dist/runner/common/acp/base-translator.js +83 -18
  62. package/dist/runner/common/connection-based-runner.d.ts +18 -0
  63. package/dist/runner/common/connection-based-runner.js +31 -0
  64. package/dist/runner/common/jsonrpc/client.js +6 -1
  65. package/dist/runner/common/jsonrpc/connection-manager.d.ts +20 -0
  66. package/dist/runner/common/jsonrpc/connection-manager.js +27 -1
  67. package/dist/runner/common/jsonrpc/transport.d.ts +29 -1
  68. package/dist/runner/common/jsonrpc/transport.js +79 -10
  69. package/dist/runner/common/spawning-runner.d.ts +49 -19
  70. package/dist/runner/common/spawning-runner.js +94 -54
  71. package/dist/runner/dsh/client.d.ts +1 -3
  72. package/dist/runner/dsh/client.js +0 -4
  73. package/dist/runner/dsh/types.d.ts +0 -36
  74. package/dist/runner/kimi/acp/runner.d.ts +18 -1
  75. package/dist/runner/kimi/acp/runner.js +100 -42
  76. package/dist/runner/kimi/acp/translator.d.ts +0 -1
  77. package/dist/runner/opencode/acp/translator.d.ts +22 -2
  78. package/dist/runner/opencode/acp/translator.js +60 -0
  79. package/dist/runner/pi/rpc/protocol-types.d.ts +4 -0
  80. package/dist/runner/pi/rpc/runner.js +3 -0
  81. package/dist/runner/resolve-agent-choices.js +0 -5
  82. package/package.json +2 -2
  83. package/dist/runner/common/process-stopper.d.ts +0 -7
  84. package/dist/runner/common/process-stopper.js +0 -8
  85. package/dist/runner/dsh/fake-dsh-server.d.ts +0 -66
  86. package/dist/runner/dsh/fake-dsh-server.js +0 -162
@@ -1,6 +1,16 @@
1
1
  import { currentPlatform, isWin32 } from './select.js';
2
2
  import { resolveExecutable } from './command.js';
3
3
  import { spawnProcess } from './spawn.js';
4
+ /** 首 token 是这些名字时按「要一个 shell」处理(win32 上它们只能来自 Git Bash)。 */
5
+ const SHELL_COMMANDS = new Set(['bash', 'sh', 'zsh', 'dash']);
6
+ /** 取命令名(去目录、去扩展名、小写):`C:\Program Files\Git\bin\bash.exe` → `bash`。 */
7
+ function commandBaseName(file) {
8
+ const base = file.split(/[/\\]/).pop() ?? file;
9
+ const dot = base.lastIndexOf('.');
10
+ return (dot > 0 ? base.slice(0, dot) : base).toLowerCase();
11
+ }
12
+ const MISSING_GIT_BASH_MESSAGE = 'Windows 上执行 bash 命令需要 Git Bash(未在 PATH 中找到 bash.exe);' +
13
+ '请安装 Git for Windows 后重试';
4
14
  /** shell 不可用(如 win32 上没装 Git Bash):调用方应转成明确错误卡。 */
5
15
  export class ShellUnavailableError extends Error {
6
16
  kind;
@@ -18,12 +28,33 @@ export function createBashShellBackend(deps = {}) {
18
28
  spawn(command, opts) {
19
29
  return spawnFn('bash', ['-c', command], { cwd: opts.cwd, ...opts.options });
20
30
  },
31
+ spawnArgv(file, args, opts = {}) {
32
+ // posix 不做任何翻译:调用方给的 argv 已经是最终形态(迁移前行为)。
33
+ return spawnFn(file, [...args], { cwd: opts.cwd, ...opts.options });
34
+ },
21
35
  };
22
36
  }
23
37
  /** win32 后端:默认 Git Bash,powershell/cmd 为显式用户选择。 */
24
38
  export function createWin32ShellBackend(deps = {}) {
25
39
  const spawnFn = deps.spawn ?? spawnProcess;
26
40
  const kind = deps.kind ?? 'bash';
41
+ /**
42
+ * argv 路径与「`!` 用哪个 shell」无关:ACP 的 terminal/create 要的是 bash
43
+ * 本身,因此三种后端的 spawnArgv 共用同一套语义(shell 名 → Git Bash,
44
+ * 其余按 PATH/PATHEXT 透传)。
45
+ */
46
+ const spawnArgv = (file, args, opts = {}) => {
47
+ const spawnOpts = { cwd: opts.cwd, ...opts.options, windowsHide: true };
48
+ if (!SHELL_COMMANDS.has(commandBaseName(file))) {
49
+ return spawnFn(file, [...args], spawnOpts);
50
+ }
51
+ const resolve = deps.resolve ?? resolveExecutable;
52
+ const spec = resolve('bash', { platform: 'win32', pathEnv: deps.pathEnv });
53
+ if (!spec) {
54
+ throw new ShellUnavailableError('bash', MISSING_GIT_BASH_MESSAGE);
55
+ }
56
+ return spawnFn(spec.file, [...args], spawnOpts);
57
+ };
27
58
  if (kind === 'powershell') {
28
59
  return {
29
60
  kind,
@@ -34,6 +65,7 @@ export function createWin32ShellBackend(deps = {}) {
34
65
  windowsHide: true,
35
66
  });
36
67
  },
68
+ spawnArgv,
37
69
  };
38
70
  }
39
71
  if (kind === 'cmd') {
@@ -46,6 +78,7 @@ export function createWin32ShellBackend(deps = {}) {
46
78
  windowsHide: true,
47
79
  });
48
80
  },
81
+ spawnArgv,
49
82
  };
50
83
  }
51
84
  return {
@@ -54,8 +87,7 @@ export function createWin32ShellBackend(deps = {}) {
54
87
  const resolve = deps.resolve ?? resolveExecutable;
55
88
  const spec = resolve('bash', { platform: 'win32', pathEnv: deps.pathEnv });
56
89
  if (!spec) {
57
- throw new ShellUnavailableError('bash', 'Windows 上执行 bash 命令需要 Git Bash(未在 PATH 中找到 bash.exe);' +
58
- '请安装 Git for Windows 后重试');
90
+ throw new ShellUnavailableError('bash', MISSING_GIT_BASH_MESSAGE);
59
91
  }
60
92
  // 执行语义由 cross-spawn 负责(垫片 cmd.exe 受控执行);这里只保证
61
93
  // bash 存在性并给出明确的「需要 Git Bash」错误
@@ -65,6 +97,7 @@ export function createWin32ShellBackend(deps = {}) {
65
97
  windowsHide: true,
66
98
  });
67
99
  },
100
+ spawnArgv,
68
101
  };
69
102
  }
70
103
  /** 平台分发的唯一入口:posix → bash,win32 → 默认 Git Bash。 */
@@ -12,6 +12,12 @@ export interface SleepBlockerOptions {
12
12
  /**
13
13
  * PowerShell 托管脚本:声明 P/Invoke → 设置持续唤醒 → 轮询父 pid 直至父进程消失。
14
14
  * 单字符串经 `-Command` 传入(不经 shell,内嵌双引号安全);父 pid 是 number,插值安全。
15
+ *
16
+ * 首句 `$ErrorActionPreference = 'Stop'` 把非终止错误**升级为终止错误**:P/Invoke
17
+ * 声明或调用一旦出问题,helper 立刻退出,从而走到 startSleepBlocker 的 `exit`
18
+ * 告警路径。否则「进程活着但 execution state 没设置上」属于静默态,日志里查不出来。
19
+ * 看门循环自带 `-ErrorAction SilentlyContinue`,不受该设置影响(父进程消失本就是
20
+ * 正常结束路径)。
15
21
  */
16
22
  export declare function win32SleepBlockerScript(parentPid: number): string;
17
23
  export declare function startSleepBlocker(opts: SleepBlockerOptions): SleepBlocker | null;
@@ -1,15 +1,39 @@
1
1
  import { spawnProcess } from './spawn.js';
2
2
  import { getLogger } from '../logger/index.js';
3
- /** ES_CONTINUOUS (0x80000000) | ES_SYSTEM_REQUIRED (0x00000001)。 */
4
- const ES_FLAGS = '0x80000001';
3
+ /**
4
+ * ES_CONTINUOUS (0x80000000) | ES_SYSTEM_REQUIRED (0x00000001)。
5
+ *
6
+ * **必须按无符号 32 位取值**:`0x80000001` = 2147483649 超出 Int32 正数上界
7
+ * (2147483647),而 PowerShell 的数字字面量按 Int32 解析 —— 十六进制写法一旦
8
+ * 进了脚本就会被解析成 **-2147483647**,传给 P/Invoke 声明的 `uint` 形参时抛
9
+ * `MethodArgumentConversionInvalidCastArgument`(2026-09-22 真机复现)。
10
+ */
11
+ const ES_FLAGS_UINT32 = (0x80000000 | 0x00000001) >>> 0;
12
+ /**
13
+ * 插入 PowerShell 脚本的 ES flags 字面量,**十进制**(`2147483649`)。
14
+ *
15
+ * 上面那个异常是**非终止错误**:PowerShell 不中断脚本,后面的看门循环照跑 ——
16
+ * 于是 helper 进程一直活着、`SetThreadExecutionState` 却从未被调用,防休眠彻底
17
+ * 失效,而「helper 退出」这条唯一的告警通道恰好不触发(日志里只剩一句 `active`,
18
+ * 系统照记 Kernel-Power id=42 / System Idle 睡眠)。十进制字面量走
19
+ * Int64 → UInt32 转换,可安全传递。
20
+ */
21
+ const WIN32_ES_FLAGS_LITERAL = String(ES_FLAGS_UINT32);
5
22
  /**
6
23
  * PowerShell 托管脚本:声明 P/Invoke → 设置持续唤醒 → 轮询父 pid 直至父进程消失。
7
24
  * 单字符串经 `-Command` 传入(不经 shell,内嵌双引号安全);父 pid 是 number,插值安全。
25
+ *
26
+ * 首句 `$ErrorActionPreference = 'Stop'` 把非终止错误**升级为终止错误**:P/Invoke
27
+ * 声明或调用一旦出问题,helper 立刻退出,从而走到 startSleepBlocker 的 `exit`
28
+ * 告警路径。否则「进程活着但 execution state 没设置上」属于静默态,日志里查不出来。
29
+ * 看门循环自带 `-ErrorAction SilentlyContinue`,不受该设置影响(父进程消失本就是
30
+ * 正常结束路径)。
8
31
  */
9
32
  export function win32SleepBlockerScript(parentPid) {
10
33
  return [
34
+ `$ErrorActionPreference = 'Stop'`,
11
35
  `Add-Type -TypeDefinition 'using System.Runtime.InteropServices; public static class SleepBlocker { [DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags); }'`,
12
- `[SleepBlocker]::SetThreadExecutionState(${ES_FLAGS}) | Out-Null`,
36
+ `[SleepBlocker]::SetThreadExecutionState(${WIN32_ES_FLAGS_LITERAL}) | Out-Null`,
13
37
  `while (Get-Process -Id ${parentPid} -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 30 }`,
14
38
  ].join('; ');
15
39
  }
@@ -47,8 +71,10 @@ export function startSleepBlocker(opts) {
47
71
  logger.warn(`[sleep-blocker] ${command} error:`, err);
48
72
  });
49
73
  // helper 意外死亡 = 防休眠静默失效(win32 上 Add-Type 动态编译被企业策略/
50
- // 杀软拦掉、Get-Process 权限失败;darwin 上 caffeinate 早退)。桥此时还活着,
51
- // 是唯一能留痕的时机——否则日志里只剩启动那句 "active",误以为防休眠在生效。
74
+ // 杀软拦掉、P/Invoke 调用抛错——脚本首句已用 $ErrorActionPreference='Stop' 把
75
+ // 非终止错误升级为终止错误,确保这类失败能落到这条分支;darwin 上 caffeinate
76
+ // 早退)。桥此时还活着,是唯一能留痕的时机——否则日志里只剩启动那句 "active",
77
+ // 误以为防休眠在生效。
52
78
  let stopping = false;
53
79
  proc.on('exit', (code, signal) => {
54
80
  if (stopping)
@@ -22,7 +22,7 @@ export declare function mergeProcessEnv(base?: NodeJS.ProcessEnv, overrides?: No
22
22
  /**
23
23
  * 是否需要为子进程建独立进程组(`spawn` 的 `detached: true`)。
24
24
  *
25
- * posix:ProcessStopper 的组杀用负 PID(`kill(-pgid)`),子进程必须是组长,
25
+ * posix:Terminator 的组杀用负 PID(`kill(-pgid)`),子进程必须是组长,
26
26
  * 否则 kill 抛 ESRCH 被吞,停止/清理链路全失效。
27
27
  *
28
28
  * win32:组杀已由 terminator 的 `taskkill /PID <pid> /T /F` 树杀替代,不需要
@@ -53,7 +53,7 @@ const COMMAND_NOT_FOUND_PATTERN = /is not recognized as an internal or external
53
53
  /**
54
54
  * 是否需要为子进程建独立进程组(`spawn` 的 `detached: true`)。
55
55
  *
56
- * posix:ProcessStopper 的组杀用负 PID(`kill(-pgid)`),子进程必须是组长,
56
+ * posix:Terminator 的组杀用负 PID(`kill(-pgid)`),子进程必须是组长,
57
57
  * 否则 kill 抛 ESRCH 被吞,停止/清理链路全失效。
58
58
  *
59
59
  * win32:组杀已由 terminator 的 `taskkill /PID <pid> /T /F` 树杀替代,不需要
@@ -1,17 +1,5 @@
1
- /**
2
- * posix 终止器:`src/runner/common/process-stopper.ts` 的原样迁移(design.md §3.2)。
3
- *
4
- * 语义与迁移前逐条对齐:SIGTERM 打负 PID 进程组 → 等 graceMs → SIGKILL;
5
- * immediate 则两个信号连发。本次只新增两样东西:
6
- * - 返回 {@link TerminateResult} 让终止途经可观测(与 win32 同构);
7
- * - `cleanupOnExit`(Terminator 接口要求的进程级退出清理)。
8
- *
9
- * `ProcessStopper` 保留为兼容壳:行为(含日志文案)零变化,既有调用方与
10
- * 测试无需改动;M2 调用点迁移时逐个换成 Terminator 后即可删除。
11
- */
12
- import type { ChildProcess } from 'node:child_process';
13
1
  import type { Terminator } from './terminator.js';
14
- export type LogLevel = 'debug' | 'info';
2
+ export type LogLevel = 'debug' | 'info' | 'warn';
15
3
  export type TerminatorLogger = (level: LogLevel, message: string) => void;
16
4
  export interface PosixTerminatorDeps {
17
5
  /** 优雅等待窗口(毫秒),对应现状 stopGraceMs */
@@ -20,19 +8,3 @@ export interface PosixTerminatorDeps {
20
8
  log?: TerminatorLogger;
21
9
  }
22
10
  export declare function createPosixTerminator(deps: PosixTerminatorDeps): Terminator;
23
- interface ProcessStopperOptions {
24
- graceMs: number;
25
- }
26
- interface StopOptions {
27
- immediate?: boolean;
28
- }
29
- /**
30
- * 迁移前的类名(兼容壳):返回 void、接受 null proc。
31
- * 新代码请用 {@link createPosixTerminator} / `createTerminator`。
32
- */
33
- export declare class ProcessStopper {
34
- private graceMs;
35
- constructor(opts: ProcessStopperOptions);
36
- stop(proc: ChildProcess | null, opts?: StopOptions): Promise<void>;
37
- }
38
- export {};
@@ -4,6 +4,8 @@ function defaultLog(level, message) {
4
4
  const logger = getLogger();
5
5
  if (level === 'debug')
6
6
  logger.debug(message);
7
+ else if (level === 'warn')
8
+ logger.warn(message);
7
9
  else
8
10
  logger.info(message);
9
11
  }
@@ -11,6 +13,24 @@ function defaultLog(level, message) {
11
13
  function isAlive(proc) {
12
14
  return proc.exitCode === null && proc.signalCode === null;
13
15
  }
16
+ /**
17
+ * 向进程组发一个信号;返回失败原因,null 表示已送达或无需送达。
18
+ *
19
+ * ESRCH(进程组已经没了)与杀成功同等处置——这正是「停止」想要的结果;
20
+ * 其余失败(EPERM 等)必须报出去,否则调用方拿到假成功,事后既不知也没得查。
21
+ */
22
+ function signalGroup(pgid, signal) {
23
+ try {
24
+ process.kill(pgid, signal);
25
+ return null;
26
+ }
27
+ catch (err) {
28
+ const code = err.code;
29
+ if (code === 'ESRCH')
30
+ return null;
31
+ return `${signal} 未送达 pgid=${pgid}: ${code ?? err.message}`;
32
+ }
33
+ }
14
34
  async function stopPosix(proc, opts, log) {
15
35
  if (!isAlive(proc)) {
16
36
  return { requested: false, via: 'already-exited' };
@@ -22,19 +42,17 @@ async function stopPosix(proc, opts, log) {
22
42
  // 负 PID = 整个进程组:shell 包装层与孙进程一并带走
23
43
  const pgid = -pid;
24
44
  log('debug', `[process-stopper] sending SIGTERM to pgid=${pgid} immediate=${opts.immediate}`);
25
- try {
26
- process.kill(pgid, 'SIGTERM');
27
- }
28
- catch {
29
- /* process may have exited */
45
+ const termErr = signalGroup(pgid, 'SIGTERM');
46
+ if (termErr) {
47
+ log('warn', `[process-stopper] ${termErr}`);
48
+ return { requested: false, via: 'cooperative', error: termErr };
30
49
  }
31
50
  if (opts.immediate) {
32
51
  // 不等待:SIGKILL 紧随其后;进程已死时 kill 抛 ESRCH,忽略即可
33
- try {
34
- process.kill(pgid, 'SIGKILL');
35
- }
36
- catch {
37
- /* process already gone */
52
+ const killErr = signalGroup(pgid, 'SIGKILL');
53
+ if (killErr) {
54
+ log('warn', `[process-stopper] ${killErr}`);
55
+ return { requested: false, via: 'taskkill', error: killErr };
38
56
  }
39
57
  return { requested: true, via: 'taskkill' };
40
58
  }
@@ -54,11 +72,10 @@ async function stopPosix(proc, opts, log) {
54
72
  clearTimeout(graceTimer);
55
73
  if (!exited) {
56
74
  log('info', `[process-stopper] process group ${pgid} did not exit within grace period, sending SIGKILL`);
57
- try {
58
- process.kill(pgid, 'SIGKILL');
59
- }
60
- catch {
61
- /* process already gone */
75
+ const killErr = signalGroup(pgid, 'SIGKILL');
76
+ if (killErr) {
77
+ log('warn', `[process-stopper] ${killErr}`);
78
+ return { requested: false, via: 'taskkill', error: killErr };
62
79
  }
63
80
  return { requested: true, via: 'taskkill' };
64
81
  }
@@ -85,19 +102,4 @@ export function createPosixTerminator(deps) {
85
102
  },
86
103
  };
87
104
  }
88
- /**
89
- * 迁移前的类名(兼容壳):返回 void、接受 null proc。
90
- * 新代码请用 {@link createPosixTerminator} / `createTerminator`。
91
- */
92
- export class ProcessStopper {
93
- graceMs;
94
- constructor(opts) {
95
- this.graceMs = opts.graceMs;
96
- }
97
- async stop(proc, opts) {
98
- if (!proc)
99
- return;
100
- await stopPosix(proc, { graceMs: this.graceMs, immediate: opts?.immediate === true }, defaultLog);
101
- }
102
- }
103
105
  //# sourceMappingURL=terminator-posix.js.map
@@ -74,10 +74,12 @@ export function createWin32Terminator(deps) {
74
74
  return { requested: true, via: 'taskkill' };
75
75
  }
76
76
  const agent = deps.agent;
77
- const stopper = agent === undefined ? undefined : stoppers.get(agent);
77
+ // 按 agent + pid 查:同一 agent 可能有多条长驻连接(每 workspace 一条),
78
+ // 只有 pid 能把通道归属钉到手上这个 proc 上。
79
+ const stopper = agent === undefined ? undefined : stoppers.get(agent, proc.pid);
78
80
  if (!stopper) {
79
81
  // 无通道是合法状态:显式记录后走树杀,不做无意义的优雅空转
80
- log?.(`win32 优雅停止跳过:agent=${agent ?? '(unknown)'} 无协议停止通道,直接树杀`);
82
+ log?.(`win32 优雅停止跳过:agent=${agent ?? '(unknown)'} pid=${proc.pid} 无协议停止通道,直接树杀`);
81
83
  killTree(proc.pid);
82
84
  return { requested: true, via: 'skipped-no-channel' };
83
85
  }
@@ -13,7 +13,7 @@ export type { AgentStopper } from './agent-stopper.js';
13
13
  /**
14
14
  * 进程终止器。
15
15
  *
16
- * `stop` 的 `immediate` 语义与现状 `process-stopper` 的 `stop({immediate})` 对齐。
16
+ * `stop` 的 `immediate` 语义与原 `process-stopper` 的 `stop({immediate})` 对齐。
17
17
  * 设计文档声明返回 `Promise<void>`,这里收窄为 {@link TerminateResult}:降级
18
18
  * 必须可观测(优雅段被跳过、走了树杀、进程早已退出),否则 win32 上「等一个
19
19
  * 不会来的退出事件」这类退化无法被发现。
@@ -35,7 +35,7 @@ export interface TerminatorDeps {
35
35
  /** 协议通道注册表注入(测试隔离);默认进程级单例 */
36
36
  stoppers?: AgentStopperRegistry;
37
37
  /** 日志注入(测试用);默认走 logger 单例 */
38
- log?: (level: 'debug' | 'info', message: string) => void;
38
+ log?: (level: 'debug' | 'info' | 'warn', message: string) => void;
39
39
  }
40
40
  /**
41
41
  * 唯一的终止器选择入口:posix 走组杀,win32 走「协议通道 + taskkill 树杀」。
@@ -1,24 +1,39 @@
1
+ import { getLogger } from '../logger/index.js';
1
2
  import { currentPlatform, isWin32 } from './select.js';
2
3
  import { createPosixTerminator } from './terminator-posix.js';
3
4
  import { createWin32Terminator } from './terminator-win32.js';
4
5
  // 注册表实现在 agent-stopper.ts(避免与 terminator-win32.ts 循环依赖),
5
6
  // 这里 re-export 以维持调用方的单一入口。
6
7
  export { AgentStopperRegistry, agentStopperRegistry } from './agent-stopper.js';
8
+ /** 默认日志出口:seam 底层(terminator-win32/posix)不反向依赖 logger 单例,由这里接线。 */
9
+ function defaultTerminatorLog(level, message) {
10
+ const logger = getLogger();
11
+ if (level === 'debug')
12
+ logger.debug(message);
13
+ else if (level === 'warn')
14
+ logger.warn(message);
15
+ else
16
+ logger.info(message);
17
+ }
7
18
  /**
8
19
  * 唯一的终止器选择入口:posix 走组杀,win32 走「协议通道 + taskkill 树杀」。
9
20
  * 其余模块一律拿这里的 Terminator,禁止散落平台 if(§2.2)。
10
21
  */
11
22
  export function createTerminator(deps) {
12
23
  const platform = deps.platform ?? currentPlatform;
24
+ // 默认日志出口必须真的接上:win32 的「无协议通道 → 直接树杀」「协议通道抛错
25
+ // → 转树杀」以及 posix 的 grace 超时都是设计要求的可观测降级信号(§3.2),
26
+ // 缺省 undefined 会让这些信号全部静默消失。
27
+ const log = deps.log ?? defaultTerminatorLog;
13
28
  if (isWin32(platform)) {
14
29
  return createWin32Terminator({
15
30
  graceMs: deps.graceMs,
16
31
  agent: deps.agent,
17
32
  stoppers: deps.stoppers,
18
33
  // win32 侧日志不带级别(无 debug 级输出),统一按 info 透传
19
- log: deps.log ? (message) => deps.log?.('info', message) : undefined,
34
+ log: (message) => log('info', message),
20
35
  });
21
36
  }
22
- return createPosixTerminator({ graceMs: deps.graceMs, log: deps.log });
37
+ return createPosixTerminator({ graceMs: deps.graceMs, log });
23
38
  }
24
39
  //# sourceMappingURL=terminator.js.map
@@ -18,6 +18,9 @@ export interface TerminateResult {
18
18
  /**
19
19
  * 终止途经:协议停止 / 强制终止 / 进程已退出 / 无通道跳过优雅段。
20
20
  * 强制终止在 posix 下是 SIGKILL 组杀、win32 下是 taskkill 树杀,同枚举值。
21
+ * `requested: false` 时取值为**尝试过的那条途经**,配合 {@link error} 判读。
21
22
  */
22
23
  via: 'cooperative' | 'taskkill' | 'already-exited' | 'skipped-no-channel';
24
+ /** 请求未送达的原因(ESRCH/进程已消失不算失败,不带此字段) */
25
+ error?: string;
23
26
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 非直返 card action 的异步回执判定(入口层的出站侧)。
3
+ *
4
+ * `src/index.ts` 的 card action 分发只有两类出口:`DIRECT_RETURN_CMDS` 同步
5
+ * return 给飞书回调(toast 能弹),其余走 `bridge.enqueueImmediate` /
6
+ * `bridge.enqueue`——fire-and-forget,回调响应早已被飞书收走,handler 返回的
7
+ * toast 无处可去。本模块给出异步出口应转发的文案:失败/告警换成一条持久文本
8
+ * 消息(卡片只承载成功态,失败在刷新后的卡片上完全看不出来),成功类不回执
9
+ * (逐次回执会把聊天刷成日志)。
10
+ */
11
+ /**
12
+ * 从 `router.handleCardAction` 的返回值里取出应异步回给用户的文本。
13
+ *
14
+ * 返回 undefined = 无需回执。结构不合期望时同样返回 undefined(判定函数不得
15
+ * 成为 fire-and-forget 任务里新的抛错源)。
16
+ */
17
+ export declare function actionFeedbackText(res: unknown): string | undefined;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 非直返 card action 的异步回执判定(入口层的出站侧)。
3
+ *
4
+ * `src/index.ts` 的 card action 分发只有两类出口:`DIRECT_RETURN_CMDS` 同步
5
+ * return 给飞书回调(toast 能弹),其余走 `bridge.enqueueImmediate` /
6
+ * `bridge.enqueue`——fire-and-forget,回调响应早已被飞书收走,handler 返回的
7
+ * toast 无处可去。本模块给出异步出口应转发的文案:失败/告警换成一条持久文本
8
+ * 消息(卡片只承载成功态,失败在刷新后的卡片上完全看不出来),成功类不回执
9
+ * (逐次回执会把聊天刷成日志)。
10
+ */
11
+ /** 只转发这两类;success/info/loading 由原地刷新的卡片表达。 */
12
+ const FORWARDABLE = new Set(['error', 'warning']);
13
+ /**
14
+ * 从 `router.handleCardAction` 的返回值里取出应异步回给用户的文本。
15
+ *
16
+ * 返回 undefined = 无需回执。结构不合期望时同样返回 undefined(判定函数不得
17
+ * 成为 fire-and-forget 任务里新的抛错源)。
18
+ */
19
+ export function actionFeedbackText(res) {
20
+ const toast = res
21
+ ?.toast;
22
+ if (typeof toast !== 'object' || toast === null)
23
+ return undefined;
24
+ const { type, content } = toast;
25
+ if (typeof type !== 'string' || !FORWARDABLE.has(type))
26
+ return undefined;
27
+ if (typeof content !== 'string')
28
+ return undefined;
29
+ return content.trim() || undefined;
30
+ }
31
+ //# sourceMappingURL=card-action-feedback.js.map
@@ -15,8 +15,10 @@ const PERMISSION_MODE_LABELS = {
15
15
  plan: '计划模式(只读探索)',
16
16
  };
17
17
  /**
18
- * 下拉选项排除 manual:manual 是 default 的别名(claude CLI 等价),
19
- * 只保留 default 一个,避免下拉出现两个等价项。schema 仍接受 manual(旧配置向后兼容)。
18
+ * 下拉不展示 manual:CLI 认这个值(claude --help),但它和 'default' 是两个不同的
19
+ * 配置口径(显式传参 vs 省略参数),并列展示用户无从分辨取舍,所以只留 'default'。
20
+ * 代价:配置里手写的 manual 在卡片上按 'default' 显示,改动其它项保存时会被写回
21
+ * 'default'(见 buildFields 的 displayedPermissionMode)。
20
22
  */
21
23
  const PERMISSION_MODE_OPTIONS = CLAUDE_PERMISSION_MODES.filter((m) => m !== 'manual').map((m) => ({
22
24
  text: `${m}(${PERMISSION_MODE_LABELS[m] ?? ''})`,
@@ -78,10 +80,11 @@ export class ClaudeConfigBuilder {
78
80
  options: CLAUDE_EFFORTS,
79
81
  currentValue: currentEffort,
80
82
  });
81
- // 权限模式(Claude 官方 --permission-mode 枚举;bypassPermissions 为 schema 默认,
82
- // 保持旧行为无审批卡,配置为其他值后激活交互式审批)
83
+ // 权限模式(取值见 CLAUDE_PERMISSION_MODES:除自造的 default 外都是 CLI 的
84
+ // --permission-mode 枚举;bypassPermissions 为 schema 默认,保持旧行为无审批卡,
85
+ // 配置为其他值后激活交互式审批)
83
86
  const currentPermissionMode = displayConfig.claude?.permissionMode ?? 'bypassPermissions';
84
- // manual 是 default 的别名:旧配置按 default 显示(下拉无 manual 项,避免空选)
87
+ // manual 不在下拉里:旧配置按 default 显示,避免打开卡片时该项空选
85
88
  const displayedPermissionMode = currentPermissionMode === 'manual' ? 'default' : currentPermissionMode;
86
89
  fields.push({
87
90
  key: 'claude.permissionMode',
@@ -8,6 +8,8 @@ interface CommandContext {
8
8
  userId: string;
9
9
  chatId: string;
10
10
  messageId: string;
11
+ /** 本轮(装配窗口)全部入站 messageId,`messageId` 是最后一条。见 BridgeContext 同名字段(§B8)。 */
12
+ turnMessageIds?: string[];
11
13
  }
12
14
  interface CommandResult {
13
15
  text?: string;
@@ -1624,13 +1624,15 @@ export class CommandRouter {
1624
1624
  if (!name) {
1625
1625
  return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1626
1626
  }
1627
- // Execute the removal (consumes the write-side effect; the returned text
1628
- // is discarded — feedback flows through the toast + refreshed card).
1629
- this.cmdWs(['remove', name], ctx);
1627
+ // Execute the removal and let its result decide the toast: the card can be
1628
+ // a stale list (alias already deleted elsewhere), and an unconditional
1629
+ // "已删除" would then contradict what the refreshed card shows.
1630
+ const result = this.cmdWs(['remove', name], ctx);
1631
+ const text = result.text ?? `删除 workspace "${name}" 失败`;
1630
1632
  // Rebuild the /ws list card and update it in place, preserving the page
1631
1633
  // the user was on when they clicked 删除.
1632
1634
  await this.refreshListCard('ws', value, ctx);
1633
- return { toast: { type: 'success', content: `已删除 workspace "${name}"` } };
1635
+ return { toast: { type: text.startsWith('已删除') ? 'success' : 'error', content: text } };
1634
1636
  }
1635
1637
  /**
1636
1638
  * Handle ws.page: paginate the /ws list in place.
@@ -2711,19 +2713,37 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2711
2713
  const browseRoot = rootDir ? path.resolve(cwd, rootDir) : targetDir;
2712
2714
  try {
2713
2715
  const entries = fs.readdirSync(targetDir, { withFileTypes: true });
2714
- // Separate directories and files
2715
- const dirs = entries
2716
- .filter((e) => e.isDirectory())
2717
- .sort((a, b) => a.name.localeCompare(b.name));
2718
- const files = entries.filter((e) => e.isFile()).sort((a, b) => a.name.localeCompare(b.name));
2716
+ // 逐条目分类。`Dirent.isFile()/isDirectory()` 对符号链接恒 false(旧实现
2717
+ // 因此完全不列链接);必须 stat 解析目标。单条目 stat 失败(悬空链接、
2718
+ // readdir 与 stat 之间被删、无权限)只让该条目降级为「大小未知」,不能
2719
+ // 让整次列目录失败——一个坏邻居毁掉整张卡是旧实现的第二宗罪。
2720
+ const dirs = [];
2721
+ const files = [];
2722
+ for (const entry of entries) {
2723
+ let isDir = entry.isDirectory();
2724
+ let size;
2725
+ if (!isDir) {
2726
+ try {
2727
+ const st = fs.statSync(path.join(targetDir, entry.name));
2728
+ isDir = st.isDirectory();
2729
+ if (!isDir)
2730
+ size = st.size;
2731
+ }
2732
+ catch {
2733
+ // 目标拿不到:按文件列出,size 留空
2734
+ }
2735
+ }
2736
+ if (isDir)
2737
+ dirs.push(entry.name);
2738
+ else
2739
+ files.push({ name: entry.name, size });
2740
+ }
2741
+ dirs.sort((a, b) => a.localeCompare(b));
2742
+ files.sort((a, b) => a.name.localeCompare(b.name));
2719
2743
  // Merge dirs and files for pagination, preserving type info
2720
2744
  const allItems = [
2721
- ...dirs.map((d) => ({ name: d.name, isDir: true })),
2722
- ...files.map((f) => ({
2723
- name: f.name,
2724
- isDir: false,
2725
- size: fs.statSync(path.join(targetDir, f.name)).size,
2726
- })),
2745
+ ...dirs.map((name) => ({ name, isDir: true })),
2746
+ ...files.map((f) => ({ name: f.name, isDir: false, size: f.size })),
2727
2747
  ];
2728
2748
  // 关键词筛选:只匹配当前层条目名(不递归子目录)。大小写不敏感的子串包含
2729
2749
  // ——禁止 new RegExp(q)(用户搜 `foo(` 会直接抛)。插在排序之后、分页之前。
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * 架构:ClaudeRunner 是 workspace-lifetime 的薄包装,进程与协议细节在
9
9
  * ClaudeSession(src/runner/claude/session.ts,extends SpawningRunner 复用
10
- * pid 文件/killOrphan/ProcessStopper/心跳/退出分发器机制)。每次 run() 委托
10
+ * pid 文件/killOrphan/Terminator/心跳/退出分发器机制)。每次 run() 委托
11
11
  * session:确保进程(可 --resume)→ 写 user 消息 → 消费事件直到本 turn result。
12
12
  */
13
13
  import { ClaudeSessionReader } from '../../session/claude/index.js';
@@ -25,7 +25,7 @@
25
25
  * 生命周期:一个 workspace 一个长驻进程(lifetime='workspace')。每次
26
26
  * run(message) = 写一条 user 消息 + 消费 stdout 事件直到本 turn 的 result;
27
27
  * 进程在 turn 之间保持存活(stdin 不关闭),/stop / /new / /cd / 看门狗超时
28
- * 时经 ProcessStopper 组杀,下条消息按 SessionStore 的 sessionId --resume。
28
+ * 时经 Terminator 组杀,下条消息按 SessionStore 的 sessionId --resume。
29
29
  */
30
30
  import { SpawningRunner } from '../common/spawning-runner.js';
31
31
  import type { AgentEvent, SpawnOptions } from '../types.js';
@@ -54,7 +54,7 @@ export interface PermissionResult {
54
54
  }
55
55
  /**
56
56
  * ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
57
- * ProcessStopper/SpawnHeartbeat/退出分发器机制(P1-1/P1-10/P1-11 契约保持),
57
+ * Terminator/SpawnHeartbeat/退出分发器机制(P1-1/P1-10/P1-11 契约保持),
58
58
  * 但 run() 覆盖为「长驻 + 按 turn 消费」:进程跨 turn 存活,turn 结束以
59
59
  * stream-json result 事件(非 compact)为界,而不是进程退出。
60
60
  */
@@ -107,10 +107,6 @@ export declare class ClaudeSession extends SpawningRunner {
107
107
  * 直到本 turn 的 result。进程在 turn 结束后保持存活,供下一条消息复用。
108
108
  */
109
109
  run(message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
110
- /**
111
- * 写一条用户消息到 stdin(--input-format stream-json 的 user 事件)。
112
- */
113
- sendUserMessage(message: string): Promise<void>;
114
110
  /**
115
111
  * 回写审批响应(允许/拒绝)。
116
112
  */