lark-remote 0.4.1 → 0.4.3
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/CHANGELOG.md +39 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/bridge/approval-coordinator.d.ts +2 -0
- package/dist/bridge/approval-coordinator.js +16 -7
- package/dist/bridge/index.d.ts +22 -0
- package/dist/bridge/index.js +138 -56
- package/dist/card/approval-render.js +9 -8
- package/dist/card/card-budget.js +39 -26
- package/dist/card/run-renderer.js +9 -2
- package/dist/card/text-truncate.d.ts +9 -10
- package/dist/card/text-truncate.js +48 -43
- package/dist/cli.js +17 -0
- package/dist/config/codex-config.d.ts +3 -0
- package/dist/config/codex-config.js +4 -1
- package/dist/config/dir.js +6 -6
- package/dist/config/index.d.ts +7 -5
- package/dist/config/index.js +52 -13
- package/dist/config/kimi-config.d.ts +4 -1
- package/dist/config/kimi-config.js +5 -2
- package/dist/config/pi-config.d.ts +2 -7
- package/dist/config/pi-config.js +6 -6
- package/dist/config/wizard.js +8 -4
- package/dist/connector/index.d.ts +19 -0
- package/dist/connector/index.js +134 -23
- package/dist/inbound/placeholder.d.ts +3 -2
- package/dist/inbound/placeholder.js +17 -7
- package/dist/index.js +28 -6
- package/dist/instance-lock.d.ts +31 -15
- package/dist/instance-lock.js +64 -62
- package/dist/platform/agent-stopper.d.ts +29 -7
- package/dist/platform/agent-stopper.js +40 -8
- package/dist/platform/identity.d.ts +59 -3
- package/dist/platform/identity.js +105 -30
- package/dist/platform/path.d.ts +0 -7
- package/dist/platform/path.js +1 -51
- package/dist/platform/shell.d.ts +14 -0
- package/dist/platform/shell.js +35 -2
- package/dist/platform/sleep-blocker.d.ts +6 -0
- package/dist/platform/sleep-blocker.js +31 -5
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +1 -1
- package/dist/platform/terminator-posix.d.ts +1 -29
- package/dist/platform/terminator-posix.js +32 -30
- package/dist/platform/terminator-win32.js +4 -2
- package/dist/platform/terminator.d.ts +2 -2
- package/dist/platform/terminator.js +17 -2
- package/dist/platform/types.d.ts +3 -0
- package/dist/router/card-action-feedback.d.ts +17 -0
- package/dist/router/card-action-feedback.js +31 -0
- package/dist/router/card-helpers.d.ts +38 -2
- package/dist/router/card-helpers.js +79 -2
- package/dist/router/config/claude.js +8 -5
- package/dist/router/index.d.ts +41 -6
- package/dist/router/index.js +254 -84
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/claude/session.d.ts +2 -6
- package/dist/runner/claude/session.js +7 -8
- package/dist/runner/codex/app-server/protocol-types.d.ts +4 -0
- package/dist/runner/codex/app-server/protocol-types.js +4 -0
- package/dist/runner/codex/app-server/runner.js +3 -0
- package/dist/runner/common/acp/base-acp-runner.js +7 -0
- package/dist/runner/common/connection-based-runner.d.ts +18 -0
- package/dist/runner/common/connection-based-runner.js +31 -0
- package/dist/runner/common/jsonrpc/client.js +6 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +20 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +27 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +29 -1
- package/dist/runner/common/jsonrpc/transport.js +79 -10
- package/dist/runner/common/spawning-runner.d.ts +49 -19
- package/dist/runner/common/spawning-runner.js +94 -54
- package/dist/runner/dsh/client.d.ts +1 -3
- package/dist/runner/dsh/client.js +0 -4
- package/dist/runner/dsh/types.d.ts +0 -36
- package/dist/runner/kimi/acp/runner.d.ts +18 -1
- package/dist/runner/kimi/acp/runner.js +100 -42
- package/dist/runner/kimi/acp/translator.d.ts +0 -1
- package/dist/runner/opencode/acp/translator.d.ts +0 -1
- package/dist/runner/pi/rpc/protocol-types.d.ts +4 -0
- package/dist/runner/pi/rpc/runner.js +3 -0
- package/dist/runner/resolve-agent-choices.js +0 -5
- package/package.json +2 -2
- package/dist/runner/common/process-stopper.d.ts +0 -7
- package/dist/runner/common/process-stopper.js +0 -8
- package/dist/runner/dsh/fake-dsh-server.d.ts +0 -60
- package/dist/runner/dsh/fake-dsh-server.js +0 -156
package/dist/platform/shell.js
CHANGED
|
@@ -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',
|
|
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
|
-
/**
|
|
4
|
-
|
|
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(${
|
|
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
|
-
// 杀软拦掉、
|
|
51
|
-
//
|
|
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)
|
package/dist/platform/spawn.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare function mergeProcessEnv(base?: NodeJS.ProcessEnv, overrides?: No
|
|
|
22
22
|
/**
|
|
23
23
|
* 是否需要为子进程建独立进程组(`spawn` 的 `detached: true`)。
|
|
24
24
|
*
|
|
25
|
-
* posix:
|
|
25
|
+
* posix:Terminator 的组杀用负 PID(`kill(-pgid)`),子进程必须是组长,
|
|
26
26
|
* 否则 kill 抛 ESRCH 被吞,停止/清理链路全失效。
|
|
27
27
|
*
|
|
28
28
|
* win32:组杀已由 terminator 的 `taskkill /PID <pid> /T /F` 树杀替代,不需要
|
package/dist/platform/spawn.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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`
|
|
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:
|
|
34
|
+
log: (message) => log('info', message),
|
|
20
35
|
});
|
|
21
36
|
}
|
|
22
|
-
return createPosixTerminator({ graceMs: deps.graceMs, log
|
|
37
|
+
return createPosixTerminator({ graceMs: deps.graceMs, log });
|
|
23
38
|
}
|
|
24
39
|
//# sourceMappingURL=terminator.js.map
|
package/dist/platform/types.d.ts
CHANGED
|
@@ -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
|
|
@@ -58,8 +58,9 @@ export declare function parseJumpOffset(inputValue: unknown, pageSize: number):
|
|
|
58
58
|
* Returns an array of body elements: `[page-label div, controls column_set]`;
|
|
59
59
|
* 调用方展开进 `body.elements`(+ 自己的 `hr`)。input 仍留在控件 column_set
|
|
60
60
|
* 内部,不用 form 容器(form 触发 300123 / 200621 整卡不可用)。CardKit 2.0
|
|
61
|
-
* input
|
|
62
|
-
* includeRawEvent +
|
|
61
|
+
* input 提交(移动端键盘回车/完成键,桌面端输入框右侧提交图标)时值经 raw
|
|
62
|
+
* `action.input_value` 回传,见 connector includeRawEvent +
|
|
63
|
+
* router/card-action-payload.ts。
|
|
63
64
|
*
|
|
64
65
|
* 每个调用方自带 callback cmd / extra value / 文案(语义差异保留,不强行统一)。
|
|
65
66
|
*
|
|
@@ -81,6 +82,41 @@ export declare function paginationBar(opts: {
|
|
|
81
82
|
prevText?: string;
|
|
82
83
|
nextText?: string;
|
|
83
84
|
}): object[];
|
|
85
|
+
/**
|
|
86
|
+
* 搜索输入框的组件 name(仅用于结构断言与可读性)。input 在非 form 容器下
|
|
87
|
+
* 提交值经 raw.action.input_value 回传,formValue 路径作为回退通道。
|
|
88
|
+
*/
|
|
89
|
+
export declare const SEARCH_INPUT_NAME = "searchInput";
|
|
90
|
+
/**
|
|
91
|
+
* Build the CardKit 2.0 keyword-search row shared by /ws and /ls list cards.
|
|
92
|
+
*
|
|
93
|
+
* 飞书 input 组件没有逐键回调,只有提交(键盘回车/完成键)时才触发一次
|
|
94
|
+
* callback,所以交互是「输完回车」确认制,placeholder 必须写明。
|
|
95
|
+
*
|
|
96
|
+
* 清空输入再回车在部分客户端不可靠(用户实测:手机飞书清空后回车没反应,
|
|
97
|
+
* 空串是否发回调也没有文档保证),因此**有 `currentQuery` 就自动**在输入框
|
|
98
|
+
* 右侧补一个「清除筛选」按钮:按钮 callback 不带 input_value,handler 靠
|
|
99
|
+
* `clearQuery` 标记强制清除,不依赖「读不到输入值」这一隐式条件。
|
|
100
|
+
*
|
|
101
|
+
* 与 `paginationBar` 的控件行同一套窄屏安全模式:input 用 `weighted/weight: 1`
|
|
102
|
+
* 吃满清除按钮之外的剩余宽度,清除按钮 `auto`,手机上输入框仍可点。禁用 form
|
|
103
|
+
* 容器(300123 / 200621)与 `tag: "action"`(200861)。
|
|
104
|
+
*
|
|
105
|
+
* 返回**单个** `column_set` 元素(`paginationBar` 返回两个,调用处写法不同:
|
|
106
|
+
* `elements.push(searchBar({...}))` vs `elements.push(...paginationBar({...}))`)。
|
|
107
|
+
*
|
|
108
|
+
* @param opts.cmd 提交回调的 cmd('ws.filter' / 'ls.filter')。
|
|
109
|
+
* @param opts.placeholder 输入框占位文案(必须含「输完回车」)。
|
|
110
|
+
* @param opts.currentQuery 当前筛选词,非空时写进 default_value 回显,并据此
|
|
111
|
+
* 显示「清除筛选」按钮。
|
|
112
|
+
* @param opts.extra 透传进 callback value 的附加字段(/ls 用 path/root)。
|
|
113
|
+
*/
|
|
114
|
+
export declare function searchBar(opts: {
|
|
115
|
+
cmd: string;
|
|
116
|
+
placeholder: string;
|
|
117
|
+
currentQuery?: string;
|
|
118
|
+
extra?: Record<string, unknown>;
|
|
119
|
+
}): object;
|
|
84
120
|
/**
|
|
85
121
|
* Build a session-history resume card (shared by auto-restore, /resume <id>,
|
|
86
122
|
* config-switch resume, and run-completion notification).
|
|
@@ -104,8 +104,9 @@ export function parseJumpOffset(inputValue, pageSize) {
|
|
|
104
104
|
* Returns an array of body elements: `[page-label div, controls column_set]`;
|
|
105
105
|
* 调用方展开进 `body.elements`(+ 自己的 `hr`)。input 仍留在控件 column_set
|
|
106
106
|
* 内部,不用 form 容器(form 触发 300123 / 200621 整卡不可用)。CardKit 2.0
|
|
107
|
-
* input
|
|
108
|
-
* includeRawEvent +
|
|
107
|
+
* input 提交(移动端键盘回车/完成键,桌面端输入框右侧提交图标)时值经 raw
|
|
108
|
+
* `action.input_value` 回传,见 connector includeRawEvent +
|
|
109
|
+
* router/card-action-payload.ts。
|
|
109
110
|
*
|
|
110
111
|
* 每个调用方自带 callback cmd / extra value / 文案(语义差异保留,不强行统一)。
|
|
111
112
|
*
|
|
@@ -192,6 +193,82 @@ export function paginationBar(opts) {
|
|
|
192
193
|
{ tag: 'column_set', columns: controls },
|
|
193
194
|
];
|
|
194
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* 搜索输入框的组件 name(仅用于结构断言与可读性)。input 在非 form 容器下
|
|
198
|
+
* 提交值经 raw.action.input_value 回传,formValue 路径作为回退通道。
|
|
199
|
+
*/
|
|
200
|
+
export const SEARCH_INPUT_NAME = 'searchInput';
|
|
201
|
+
/**
|
|
202
|
+
* Build the CardKit 2.0 keyword-search row shared by /ws and /ls list cards.
|
|
203
|
+
*
|
|
204
|
+
* 飞书 input 组件没有逐键回调,只有提交(键盘回车/完成键)时才触发一次
|
|
205
|
+
* callback,所以交互是「输完回车」确认制,placeholder 必须写明。
|
|
206
|
+
*
|
|
207
|
+
* 清空输入再回车在部分客户端不可靠(用户实测:手机飞书清空后回车没反应,
|
|
208
|
+
* 空串是否发回调也没有文档保证),因此**有 `currentQuery` 就自动**在输入框
|
|
209
|
+
* 右侧补一个「清除筛选」按钮:按钮 callback 不带 input_value,handler 靠
|
|
210
|
+
* `clearQuery` 标记强制清除,不依赖「读不到输入值」这一隐式条件。
|
|
211
|
+
*
|
|
212
|
+
* 与 `paginationBar` 的控件行同一套窄屏安全模式:input 用 `weighted/weight: 1`
|
|
213
|
+
* 吃满清除按钮之外的剩余宽度,清除按钮 `auto`,手机上输入框仍可点。禁用 form
|
|
214
|
+
* 容器(300123 / 200621)与 `tag: "action"`(200861)。
|
|
215
|
+
*
|
|
216
|
+
* 返回**单个** `column_set` 元素(`paginationBar` 返回两个,调用处写法不同:
|
|
217
|
+
* `elements.push(searchBar({...}))` vs `elements.push(...paginationBar({...}))`)。
|
|
218
|
+
*
|
|
219
|
+
* @param opts.cmd 提交回调的 cmd('ws.filter' / 'ls.filter')。
|
|
220
|
+
* @param opts.placeholder 输入框占位文案(必须含「输完回车」)。
|
|
221
|
+
* @param opts.currentQuery 当前筛选词,非空时写进 default_value 回显,并据此
|
|
222
|
+
* 显示「清除筛选」按钮。
|
|
223
|
+
* @param opts.extra 透传进 callback value 的附加字段(/ls 用 path/root)。
|
|
224
|
+
*/
|
|
225
|
+
export function searchBar(opts) {
|
|
226
|
+
const columns = [
|
|
227
|
+
{
|
|
228
|
+
tag: 'column',
|
|
229
|
+
width: 'weighted',
|
|
230
|
+
weight: 1,
|
|
231
|
+
vertical_align: 'center',
|
|
232
|
+
elements: [
|
|
233
|
+
{
|
|
234
|
+
tag: 'input',
|
|
235
|
+
name: SEARCH_INPUT_NAME,
|
|
236
|
+
placeholder: { tag: 'plain_text', content: opts.placeholder },
|
|
237
|
+
...(opts.currentQuery ? { default_value: opts.currentQuery } : {}),
|
|
238
|
+
max_length: 100,
|
|
239
|
+
behaviors: [
|
|
240
|
+
{
|
|
241
|
+
type: 'callback',
|
|
242
|
+
value: { cmd: opts.cmd, ...opts.extra },
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
if (opts.currentQuery) {
|
|
250
|
+
columns.push({
|
|
251
|
+
tag: 'column',
|
|
252
|
+
width: 'auto',
|
|
253
|
+
vertical_align: 'center',
|
|
254
|
+
elements: [
|
|
255
|
+
{
|
|
256
|
+
tag: 'button',
|
|
257
|
+
text: { tag: 'plain_text', content: '清除筛选' },
|
|
258
|
+
type: 'default',
|
|
259
|
+
size: 'small',
|
|
260
|
+
behaviors: [
|
|
261
|
+
{
|
|
262
|
+
type: 'callback',
|
|
263
|
+
value: { cmd: opts.cmd, clearQuery: true, ...opts.extra },
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return { tag: 'column_set', columns };
|
|
271
|
+
}
|
|
195
272
|
/**
|
|
196
273
|
* Build a session-history resume card (shared by auto-restore, /resume <id>,
|
|
197
274
|
* config-switch resume, and run-completion notification).
|
|
@@ -15,8 +15,10 @@ const PERMISSION_MODE_LABELS = {
|
|
|
15
15
|
plan: '计划模式(只读探索)',
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
//
|
|
82
|
-
//
|
|
83
|
+
// 权限模式(取值见 CLAUDE_PERMISSION_MODES:除自造的 default 外都是 CLI 的
|
|
84
|
+
// --permission-mode 枚举;bypassPermissions 为 schema 默认,保持旧行为无审批卡,
|
|
85
|
+
// 配置为其他值后激活交互式审批)
|
|
83
86
|
const currentPermissionMode = displayConfig.claude?.permissionMode ?? 'bypassPermissions';
|
|
84
|
-
// manual
|
|
87
|
+
// manual 不在下拉里:旧配置按 default 显示,避免打开卡片时该项空选
|
|
85
88
|
const displayedPermissionMode = currentPermissionMode === 'manual' ? 'default' : currentPermissionMode;
|
|
86
89
|
fields.push({
|
|
87
90
|
key: 'claude.permissionMode',
|