lark-remote 0.1.9 → 0.1.11
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 +31 -0
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/dist/bridge/approval-coordinator.d.ts +3 -0
- package/dist/bridge/approval-coordinator.js +6 -0
- package/dist/bridge/index.js +4 -0
- package/dist/index.js +6 -2
- package/dist/order/index.d.ts +8 -0
- package/dist/order/index.js +24 -0
- package/dist/router/index.d.ts +16 -0
- package/dist/router/index.js +172 -3
- package/dist/runner/claude/session.js +4 -0
- package/dist/runner/common/acp/protocol-types.d.ts +38 -1
- package/dist/runner/common/acp/protocol-types.js +6 -0
- package/dist/runner/dsh/runner.d.ts +7 -0
- package/dist/runner/dsh/runner.js +34 -3
- package/dist/runner/kimi/acp/runner.d.ts +51 -0
- package/dist/runner/kimi/acp/runner.js +311 -3
- package/dist/runner/registry.d.ts +3 -2
- package/dist/runner/registry.js +3 -2
- package/dist/runner/types.d.ts +2 -2
- package/dist/session/common/pagination.d.ts +1 -1
- package/dist/session/common/pagination.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.11] - 2026-08-24
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- kimi ACP 支持 Bash 命令自主执行:`terminal/*` reverse RPC 将 Bash 工具下放到本地子进程(一次性 `bash -c <script>`),yolo/auto 模式自主跑命令,manual 模式仍走审批;run 卡片显示带真实命令与本地输出的 Bash 面板,并过滤 kimi 自产的双份 Bash 通知
|
|
13
|
+
|
|
14
|
+
### 变更
|
|
15
|
+
|
|
16
|
+
- kimi ACP 能力声明:`fs` 保持关闭(走本地磁盘兜底,避免未实现的 `fs/*` RPC 报 `METHOD_NOT_FOUND`),`terminal` 打开
|
|
17
|
+
- 文档清理:移除设计文档中对内部文档名(`lessons-redlines`)与本地仓库路径(`/Users/.../lark-remote`)的引用
|
|
18
|
+
|
|
19
|
+
## [0.1.10] - 2026-08-22
|
|
20
|
+
|
|
21
|
+
### 新增
|
|
22
|
+
|
|
23
|
+
- `/order` 卡片指令文本编辑:卡片「编辑」按钮弹出输入框原地修改(保留别名/使用统计/创建时间),CLI `/order edit <orderId|序号> <新文本>` 备选入口;长度上限 200 字符、空白提交报错、文本未变化短路写盘
|
|
24
|
+
|
|
25
|
+
### 修复
|
|
26
|
+
|
|
27
|
+
- cardAction 直返白名单补齐:`order.textInput` 与 `approval.planFeedback` 不再被 fire-and-forget 的 `enqueueImmediate` 吞掉返回值(编辑卡停留编辑界面 / 计划审批意见 toast 消失)
|
|
28
|
+
- DSH `selectModel` 写放大:`session.selectModel`(会写服务端全局默认)改为每 session 首次 run 对齐一次,不再每次 run 重复写;启动期 `/stop` 会取消刚创建的 session 并产出 `interrupted` 终态而非静默返回
|
|
29
|
+
- order 末页删空守卫:删除末页唯一指令后 `offset` 回退到非空页(不再出现空白卡)
|
|
30
|
+
- 计划审批意见门控:仅 `ExitPlanMode` 工具审批接受修改意见,其他审批类型拒绝
|
|
31
|
+
- 计划文件跨 run 重置:`doStartProcess` 清空会话内追踪的计划文件,避免旧计划被 `resolvePlanContent` 误读
|
|
32
|
+
- finalizeRun fallback 卡按 `compactSupported` 门控 Compact 按钮(无 `runCompact` 能力的 agent 不再渲染死按钮)
|
|
33
|
+
|
|
34
|
+
### 变更
|
|
35
|
+
|
|
36
|
+
- ESLint 告警清零(0 errors 0 warnings)
|
|
37
|
+
- 注释与文档同步 DSH 接入("5 个 agent"→"6 个 agent")
|
|
38
|
+
|
|
8
39
|
## [0.1.9] - 2026-08-20
|
|
9
40
|
|
|
10
41
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@ English | [简体中文](README.md)
|
|
|
6
6
|
[](https://www.npmjs.com/package/lark-remote)
|
|
7
7
|
[](https://github.com/bungabungawoda/lark-remote/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
|
-
A bridge between Feishu (Lark) private chat and local coding agents. Talk to Claude Code (or Codex / opencode / pi / Kimi) from Feishu — the agent reads/writes files and runs commands in a local directory you pick, with execution streamed live into a single CardKit 2.0 card.
|
|
9
|
+
A bridge between Feishu (Lark) private chat and local coding agents. Talk to Claude Code (or Codex / opencode / pi / Kimi / DSH) from Feishu — the agent reads/writes files and runs commands in a local directory you pick, with execution streamed live into a single CardKit 2.0 card.
|
|
10
10
|
|
|
11
11
|
Designed for a single-user, peer-to-peer private chat. The agent's system prompt is never modified.
|
|
12
12
|
|
|
@@ -32,7 +32,7 @@ In Codex approval mode, an approval card arrives before any command runs — app
|
|
|
32
32
|
- OS: macOS / Linux. **Windows is not supported yet** (relies on POSIX behaviors such as Unix signals, bash, and file locks)
|
|
33
33
|
- Node.js 20+ ([Bun](https://bun.sh/) for development)
|
|
34
34
|
- A Feishu custom app (either way): scan-to-create (a QR code pops up in the terminal on first launch; scan it with the Feishu app to create the app and write credentials automatically); or create one manually in the Open Platform — enable bot capability, subscribe to `im.message.receive_v1` and `card.action.trigger` via **long connection** (WebSocket, no public address needed), with at least the `im:message` scope.
|
|
35
|
-
- Claude Code CLI installed locally and logged in once in a terminal (`claude` → browser OAuth). Same idea for other agents (codex / opencode / pi / kimi): install the corresponding CLI first.
|
|
35
|
+
- Claude Code CLI installed locally and logged in once in a terminal (`claude` → browser OAuth). Same idea for other agents (codex / opencode / pi / kimi): install the corresponding CLI first. DSH connects to a local DSH Web Host via HTTP+WebSocket (no local subprocess), default address `http://127.0.0.1:3080`.
|
|
36
36
|
|
|
37
37
|
## Installation
|
|
38
38
|
|
|
@@ -64,7 +64,7 @@ feishu:
|
|
|
64
64
|
appId: cli_xxx
|
|
65
65
|
appSecret: xxx
|
|
66
66
|
|
|
67
|
-
# default agent: claude | codex | opencode | pi | kimi (default: claude)
|
|
67
|
+
# default agent: claude | codex | opencode | pi | kimi | dsh (default: claude)
|
|
68
68
|
defaultAgent: claude
|
|
69
69
|
|
|
70
70
|
claude:
|
|
@@ -124,7 +124,7 @@ Your original message also gets an emoji reaction: `Typing` while the run is in
|
|
|
124
124
|
| `/reconnect` | - | Reconnect to Feishu |
|
|
125
125
|
| `/restart` | - | In-place bridge self-restart (new process takes over with the same config) |
|
|
126
126
|
| `/config get\|set` | `/c` | View/change runtime config (agent-aware card) |
|
|
127
|
-
| `/order save\|list` | `/o` | Save or
|
|
127
|
+
| `/order save\|list\|edit` | `/o` | Save, list, or edit frequently used prompts |
|
|
128
128
|
| `!<cmd>` | - | Run a bash command with streaming card output (bypasses the serial queue) |
|
|
129
129
|
| `/exit` | `/e` | Exit the bridge |
|
|
130
130
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/lark-remote)
|
|
7
7
|
[](https://github.com/bungabungawoda/lark-remote/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
|
-
飞书私聊 ↔ 本地 Coding Agent 桥接。在飞书里和 Claude Code(或 Codex / opencode / pi / Kimi)对话,agent 在你指定的本地目录里读写文件、跑命令,执行过程以 CardKit 2.0 卡片**单卡实时流式**呈现。
|
|
9
|
+
飞书私聊 ↔ 本地 Coding Agent 桥接。在飞书里和 Claude Code(或 Codex / opencode / pi / Kimi / DSH)对话,agent 在你指定的本地目录里读写文件、跑命令,执行过程以 CardKit 2.0 卡片**单卡实时流式**呈现。
|
|
10
10
|
|
|
11
11
|
单用户、p2p 私聊场景设计,不修改 agent 的 system prompt。
|
|
12
12
|
|
|
@@ -32,7 +32,7 @@ Codex 审批模式下,执行命令前会收到审批卡片,可直接在飞
|
|
|
32
32
|
- 操作系统:macOS / Linux。**暂不支持 Windows**(依赖 Unix 信号、bash、文件锁等 POSIX 行为)
|
|
33
33
|
- Node.js 20+(开发时用 [Bun](https://bun.sh/))
|
|
34
34
|
- 飞书自建应用(二选一):扫码创建(首次启动终端弹二维码,飞书 App 扫码即自动创建并写入凭据);或手动在开放平台建应用——开启机器人能力,订阅 `im.message.receive_v1` 和 `card.action.trigger`,订阅方式选「长连接」(WebSocket,无需公网地址),权限至少 `im:message`。
|
|
35
|
-
- Claude Code CLI:本地安装并在终端完成一次登录(`claude` → 浏览器 OAuth)。使用其他 agent(codex / opencode / pi / kimi)同理,先装好对应 CLI
|
|
35
|
+
- Claude Code CLI:本地安装并在终端完成一次登录(`claude` → 浏览器 OAuth)。使用其他 agent(codex / opencode / pi / kimi)同理,先装好对应 CLI;DSH 需本地 DSH Web Host 在跑(默认 `http://127.0.0.1:3080`),lark-remote 通过 HTTP+WebSocket 直连、不 spawn 本地子进程。
|
|
36
36
|
|
|
37
37
|
## 安装
|
|
38
38
|
|
|
@@ -64,7 +64,7 @@ feishu:
|
|
|
64
64
|
appId: cli_xxx
|
|
65
65
|
appSecret: xxx
|
|
66
66
|
|
|
67
|
-
# 默认 agent:claude | codex | opencode | pi | kimi,默认 claude
|
|
67
|
+
# 默认 agent:claude | codex | opencode | pi | kimi | dsh,默认 claude
|
|
68
68
|
defaultAgent: claude
|
|
69
69
|
|
|
70
70
|
claude:
|
|
@@ -124,7 +124,7 @@ bridge 启动后不在终端输出,运行日志写入 `~/.lark-remote/logs/YYY
|
|
|
124
124
|
| `/reconnect` | - | 重连飞书 |
|
|
125
125
|
| `/restart` | - | 原地自重启 bridge(新进程同 config 接管) |
|
|
126
126
|
| `/config get\|set` | `/c` | 查改运行时配置(agent-aware 卡片) |
|
|
127
|
-
| `/order save\|list\|alias` | `/o` | 收藏常用指令;`/order alias` 注册快捷别名(输入 `$name` 展开) |
|
|
127
|
+
| `/order save\|list\|edit\|alias` | `/o` | 收藏常用指令;`/order edit` 修改指令文本(保留别名/使用统计);`/order alias` 注册快捷别名(输入 `$name` 展开) |
|
|
128
128
|
| `!<cmd>` | - | 执行 bash 命令并流式输出到卡片(绕过串行队列) |
|
|
129
129
|
| `/exit` | `/e` | 退出 bridge |
|
|
130
130
|
|
|
@@ -121,6 +121,9 @@ export declare class ApprovalCoordinator {
|
|
|
121
121
|
* 计划审批修改意见(ExitPlanMode):输入框提交 → coordinator 记录并回流
|
|
122
122
|
* 卡片回显,随后「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
|
|
123
123
|
* 同一请求可多次修改(新 nonce 来自新渲染)。
|
|
124
|
+
* 仅 kind === 'tool' 且 toolName === 'ExitPlanMode' 的计划审批接受反馈;
|
|
125
|
+
* 其余审批类型(command/file/permissions/question)没有反馈类决策,
|
|
126
|
+
* 存意见无意义(卡片侧也不渲染该输入框,此处兜底防异常卡片 payload)。
|
|
124
127
|
*/
|
|
125
128
|
planFeedback(action: {
|
|
126
129
|
text: string;
|
|
@@ -274,6 +274,9 @@ export class ApprovalCoordinator {
|
|
|
274
274
|
* 计划审批修改意见(ExitPlanMode):输入框提交 → coordinator 记录并回流
|
|
275
275
|
* 卡片回显,随后「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
|
|
276
276
|
* 同一请求可多次修改(新 nonce 来自新渲染)。
|
|
277
|
+
* 仅 kind === 'tool' 且 toolName === 'ExitPlanMode' 的计划审批接受反馈;
|
|
278
|
+
* 其余审批类型(command/file/permissions/question)没有反馈类决策,
|
|
279
|
+
* 存意见无意义(卡片侧也不渲染该输入框,此处兜底防异常卡片 payload)。
|
|
277
280
|
*/
|
|
278
281
|
async planFeedback(action, ctx) {
|
|
279
282
|
const tracked = this.approvals.get(ctx.requestId);
|
|
@@ -283,6 +286,9 @@ export class ApprovalCoordinator {
|
|
|
283
286
|
if (tracked.state !== 'pending') {
|
|
284
287
|
throw new Error(`Approval request ${ctx.requestId} is no longer pending`);
|
|
285
288
|
}
|
|
289
|
+
if (tracked.kind !== 'tool' || tracked.view.toolName !== 'ExitPlanMode') {
|
|
290
|
+
throw new Error('仅计划审批(ExitPlanMode)支持修改意见');
|
|
291
|
+
}
|
|
286
292
|
const text = action.text.trim();
|
|
287
293
|
if (!text) {
|
|
288
294
|
throw new Error('请输入修改意见');
|
package/dist/bridge/index.js
CHANGED
|
@@ -1287,6 +1287,10 @@ export class Bridge {
|
|
|
1287
1287
|
showToolUse: this.config.output.showToolUse,
|
|
1288
1288
|
showToolResult: this.config.output.showToolResult,
|
|
1289
1289
|
agentKind: activeRun.agentKind,
|
|
1290
|
+
// 与 createRunSession 的 renderOptions 一致:fallback 卡也要按
|
|
1291
|
+
// runner 能力门控 Compact 按钮(缺省 undefined = 显示按钮,
|
|
1292
|
+
// DSH 等无 runCompact 的 agent 会渲染出点了报错的死按钮)。
|
|
1293
|
+
compactSupported: this.hasRunCompact(cwd, activeRun.agentKind),
|
|
1290
1294
|
}),
|
|
1291
1295
|
}, ctx);
|
|
1292
1296
|
runCardSent = sent;
|
package/dist/index.js
CHANGED
|
@@ -532,17 +532,21 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
532
532
|
// 直返列表外,过期/重复 nonce/非法选项等错误 toast 被静默吞掉)。
|
|
533
533
|
// 审批响应尤其不能落串行队列:run 任务占用队列头直到 turn 结束,审批响应
|
|
534
534
|
// 排在后面会形成死锁(run 不结束不执行、run 结束 coordinator 已删响应空转)。
|
|
535
|
-
// order.aliasInput / order.aliasRemove 也返回 toast
|
|
535
|
+
// order.aliasInput / order.aliasRemove / order.textInput 也返回 toast+card,
|
|
536
|
+
// 需直返避免被吞(order.textInput 曾漏在列表外,编辑卡停留在编辑界面)。
|
|
537
|
+
// approval.planFeedback 同 answer 系列:附意见的 toast 需直返 + 不落串行队列。
|
|
536
538
|
if (actionValue.cmd === 'queue.input' ||
|
|
537
539
|
actionValue.cmd === 'order.aliasInput' ||
|
|
538
540
|
actionValue.cmd === 'order.aliasRemove' ||
|
|
541
|
+
actionValue.cmd === 'order.textInput' ||
|
|
539
542
|
actionValue.cmd === 'config.save' ||
|
|
540
543
|
actionValue.cmd === 'approval.respond' ||
|
|
541
544
|
actionValue.cmd === 'approval.toggle' ||
|
|
542
545
|
actionValue.cmd === 'approval.answer' ||
|
|
543
546
|
actionValue.cmd === 'approval.answerSubmit' ||
|
|
544
547
|
actionValue.cmd === 'approval.answerCustom' ||
|
|
545
|
-
actionValue.cmd === 'approval.answerNote'
|
|
548
|
+
actionValue.cmd === 'approval.answerNote' ||
|
|
549
|
+
actionValue.cmd === 'approval.planFeedback') {
|
|
546
550
|
return router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
547
551
|
}
|
|
548
552
|
// order.exec → equivalent queued message (Plan A): resolve the order text
|
package/dist/order/index.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export declare class OrderStore {
|
|
|
22
22
|
private persist;
|
|
23
23
|
save(text: string): OrderEntry;
|
|
24
24
|
updateUsedAt(id: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* 修改指令文本。保留 usedAt / alias / createdAt(编辑不应重置使用统计或别名)。
|
|
27
|
+
* newText===oldText 短路 persist,避免空写盘。长度 > MAX_TEXT_LENGTH 抛错(与 save 一致)。
|
|
28
|
+
* 内部统一 trim:卡片(handleOrderTextInput)与 CLI(/order edit)都直接传原始文本,
|
|
29
|
+
* 避免双 trim 语义混淆、也避免两条入口存出不同结果。纯空白(trim 后空)抛错。
|
|
30
|
+
* @returns 更新后的 entry;id 不存在返回 undefined。
|
|
31
|
+
*/
|
|
32
|
+
updateText(id: string, newText: string): OrderEntry | undefined;
|
|
25
33
|
/**
|
|
26
34
|
* 给指令绑定别名(或解绑)。别名全局唯一,撞名抛错;名称必须通过
|
|
27
35
|
* `ALIAS_NAME_PATTERN`(字母/数字/下划线、不能数字开头、≤20 字符)。
|
package/dist/order/index.js
CHANGED
|
@@ -84,6 +84,30 @@ export class OrderStore {
|
|
|
84
84
|
this.persist();
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* 修改指令文本。保留 usedAt / alias / createdAt(编辑不应重置使用统计或别名)。
|
|
89
|
+
* newText===oldText 短路 persist,避免空写盘。长度 > MAX_TEXT_LENGTH 抛错(与 save 一致)。
|
|
90
|
+
* 内部统一 trim:卡片(handleOrderTextInput)与 CLI(/order edit)都直接传原始文本,
|
|
91
|
+
* 避免双 trim 语义混淆、也避免两条入口存出不同结果。纯空白(trim 后空)抛错。
|
|
92
|
+
* @returns 更新后的 entry;id 不存在返回 undefined。
|
|
93
|
+
*/
|
|
94
|
+
updateText(id, newText) {
|
|
95
|
+
const trimmed = newText.trim();
|
|
96
|
+
if (trimmed === '') {
|
|
97
|
+
throw new Error('指令文本不能为空');
|
|
98
|
+
}
|
|
99
|
+
if (trimmed.length > MAX_TEXT_LENGTH) {
|
|
100
|
+
throw new Error(`指令文本超过 ${MAX_TEXT_LENGTH} 字符限制`);
|
|
101
|
+
}
|
|
102
|
+
const entry = this.data.find((e) => e.id === id);
|
|
103
|
+
if (!entry)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (entry.text === trimmed)
|
|
106
|
+
return entry; // no-op: 短路 persist
|
|
107
|
+
entry.text = trimmed;
|
|
108
|
+
this.persist();
|
|
109
|
+
return entry;
|
|
110
|
+
}
|
|
87
111
|
/**
|
|
88
112
|
* 给指令绑定别名(或解绑)。别名全局唯一,撞名抛错;名称必须通过
|
|
89
113
|
* `ALIAS_NAME_PATTERN`(字母/数字/下划线、不能数字开头、≤20 字符)。
|
package/dist/router/index.d.ts
CHANGED
|
@@ -308,6 +308,22 @@ export declare class CommandRouter {
|
|
|
308
308
|
private handleOrderAliasInput;
|
|
309
309
|
/** Handle order.aliasRemove: 删除某条指令的别名并原地重渲染列表卡。 */
|
|
310
310
|
private handleOrderAliasRemove;
|
|
311
|
+
/**
|
|
312
|
+
* Handle order.textEdit: 展示指令文本编辑卡(input + ✓ 提交图标)。
|
|
313
|
+
* 预填当前 text(完整内容,不截断——input 组件有自有滚动条);
|
|
314
|
+
* 提交 `order.textInput` 处理(trim + 长度校验 + OrderStore.updateText)。
|
|
315
|
+
* 完全镜像 handleOrderAliasEdit 的 4 段结构(reload → find → 构造 editCard →
|
|
316
|
+
* updateCardInPlace)。
|
|
317
|
+
*/
|
|
318
|
+
private handleOrderTextEdit;
|
|
319
|
+
/**
|
|
320
|
+
* Handle order.textInput: 提交指令文本编辑。校验 orderId → OrderStore.updateText
|
|
321
|
+
* (store 内部统一 trim + 空/超长校验)。成功 toast + 原地重渲染列表卡(同
|
|
322
|
+
* handleOrderAliasInput 语义:toast-only 响应会让飞书停留在 pre-click 编辑卡,
|
|
323
|
+
* 必须把 card 放进 callback 响应体);失败仅 error toast。
|
|
324
|
+
* 保留 usedAt / alias / createdAt(OrderStore.updateText 只动 text 字段)。
|
|
325
|
+
*/
|
|
326
|
+
private handleOrderTextInput;
|
|
311
327
|
/**
|
|
312
328
|
* Handle ws.use card action: switch to the workspace, then refresh the /ws
|
|
313
329
|
* list card in place so "recent" sort order is immediately visible.
|
package/dist/router/index.js
CHANGED
|
@@ -115,6 +115,8 @@ export function isImmediateAction(cmd) {
|
|
|
115
115
|
cmd === 'order.aliasEdit' ||
|
|
116
116
|
cmd === 'order.aliasInput' ||
|
|
117
117
|
cmd === 'order.aliasRemove' ||
|
|
118
|
+
cmd === 'order.textEdit' ||
|
|
119
|
+
cmd === 'order.textInput' ||
|
|
118
120
|
cmd === 'config.toggle' ||
|
|
119
121
|
cmd === 'config.set' ||
|
|
120
122
|
cmd === 'config.input' ||
|
|
@@ -367,6 +369,10 @@ export class CommandRouter {
|
|
|
367
369
|
return await this.handleOrderAliasInput(value, ctx);
|
|
368
370
|
case 'order.aliasRemove':
|
|
369
371
|
return await this.handleOrderAliasRemove(value, ctx);
|
|
372
|
+
case 'order.textEdit':
|
|
373
|
+
return await this.handleOrderTextEdit(value, ctx);
|
|
374
|
+
case 'order.textInput':
|
|
375
|
+
return await this.handleOrderTextInput(value, ctx);
|
|
370
376
|
case 'config.toggle':
|
|
371
377
|
case 'config.set':
|
|
372
378
|
case 'config.input':
|
|
@@ -1321,6 +1327,122 @@ export class CommandRouter {
|
|
|
1321
1327
|
card: { type: 'raw', data: result.card },
|
|
1322
1328
|
};
|
|
1323
1329
|
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Handle order.textEdit: 展示指令文本编辑卡(input + ✓ 提交图标)。
|
|
1332
|
+
* 预填当前 text(完整内容,不截断——input 组件有自有滚动条);
|
|
1333
|
+
* 提交 `order.textInput` 处理(trim + 长度校验 + OrderStore.updateText)。
|
|
1334
|
+
* 完全镜像 handleOrderAliasEdit 的 4 段结构(reload → find → 构造 editCard →
|
|
1335
|
+
* updateCardInPlace)。
|
|
1336
|
+
*/
|
|
1337
|
+
async handleOrderTextEdit(value, ctx) {
|
|
1338
|
+
const orderId = value.orderId;
|
|
1339
|
+
const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
1340
|
+
if (!orderId) {
|
|
1341
|
+
return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
|
|
1342
|
+
}
|
|
1343
|
+
this.orderStore.reload();
|
|
1344
|
+
const order = this.orderStore.get().find((o) => o.id === orderId);
|
|
1345
|
+
if (!order) {
|
|
1346
|
+
return { toast: { type: 'error', content: '指令不存在或已被删除' } };
|
|
1347
|
+
}
|
|
1348
|
+
// 列表卡显示的是截断版(≤100 字符 + ...),编辑卡预览也保持一致;
|
|
1349
|
+
// input 组件 default_value 预填完整 text(不受截断影响),用户提交后生效完整文本。
|
|
1350
|
+
const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;
|
|
1351
|
+
// CardKit 2.0 input 自带 ✓ 提交图标(input_value 经 raw 回传,红线)。
|
|
1352
|
+
// 不用 form 容器(触发 300123 无 submit button / 200621 嵌套 column)。
|
|
1353
|
+
const editCard = {
|
|
1354
|
+
schema: '2.0',
|
|
1355
|
+
config: { wide_screen_mode: true },
|
|
1356
|
+
header: {
|
|
1357
|
+
template: 'blue',
|
|
1358
|
+
title: { tag: 'plain_text', content: '✏️ 编辑指令' },
|
|
1359
|
+
},
|
|
1360
|
+
body: {
|
|
1361
|
+
elements: [
|
|
1362
|
+
{
|
|
1363
|
+
tag: 'div',
|
|
1364
|
+
text: { tag: 'lark_md', content: `**当前指令:**\n\`${displayText}\`` },
|
|
1365
|
+
},
|
|
1366
|
+
{ tag: 'hr' },
|
|
1367
|
+
{
|
|
1368
|
+
tag: 'div',
|
|
1369
|
+
text: {
|
|
1370
|
+
tag: 'lark_md',
|
|
1371
|
+
content: '💡 修改后点击右侧 ✓ 提交;空白提交 = 报错;最多 200 字符',
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
tag: 'column_set',
|
|
1376
|
+
columns: [
|
|
1377
|
+
{
|
|
1378
|
+
tag: 'column',
|
|
1379
|
+
width: 'weighted',
|
|
1380
|
+
weight: 3,
|
|
1381
|
+
elements: [
|
|
1382
|
+
{
|
|
1383
|
+
tag: 'input',
|
|
1384
|
+
name: 'text',
|
|
1385
|
+
placeholder: { tag: 'plain_text', content: '输入新的指令文本...' },
|
|
1386
|
+
default_value: order.text,
|
|
1387
|
+
// max_length 与 MAX_TEXT_LENGTH=200 对齐:超长在输入侧直接截断,
|
|
1388
|
+
// 避免提交后才报错(后端 updateText 仍会二次校验,双保险)。
|
|
1389
|
+
max_length: 200,
|
|
1390
|
+
behaviors: [
|
|
1391
|
+
{
|
|
1392
|
+
type: 'callback',
|
|
1393
|
+
value: { cmd: 'order.textInput', orderId, offset },
|
|
1394
|
+
},
|
|
1395
|
+
],
|
|
1396
|
+
},
|
|
1397
|
+
],
|
|
1398
|
+
},
|
|
1399
|
+
],
|
|
1400
|
+
},
|
|
1401
|
+
],
|
|
1402
|
+
},
|
|
1403
|
+
};
|
|
1404
|
+
await this.bridge.updateCardInPlace(editCard, ctx);
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Handle order.textInput: 提交指令文本编辑。校验 orderId → OrderStore.updateText
|
|
1408
|
+
* (store 内部统一 trim + 空/超长校验)。成功 toast + 原地重渲染列表卡(同
|
|
1409
|
+
* handleOrderAliasInput 语义:toast-only 响应会让飞书停留在 pre-click 编辑卡,
|
|
1410
|
+
* 必须把 card 放进 callback 响应体);失败仅 error toast。
|
|
1411
|
+
* 保留 usedAt / alias / createdAt(OrderStore.updateText 只动 text 字段)。
|
|
1412
|
+
*/
|
|
1413
|
+
async handleOrderTextInput(value, ctx) {
|
|
1414
|
+
const orderId = value.orderId;
|
|
1415
|
+
const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
1416
|
+
const raw = value.inputValue ?? value.formValue?.['text'];
|
|
1417
|
+
if (!orderId) {
|
|
1418
|
+
return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
|
|
1419
|
+
}
|
|
1420
|
+
this.orderStore.reload();
|
|
1421
|
+
const order = this.orderStore.get().find((o) => o.id === orderId);
|
|
1422
|
+
if (!order) {
|
|
1423
|
+
return { toast: { type: 'error', content: '指令不存在或已被删除' } };
|
|
1424
|
+
}
|
|
1425
|
+
let updated;
|
|
1426
|
+
try {
|
|
1427
|
+
updated = this.orderStore.updateText(order.id, raw ?? '');
|
|
1428
|
+
}
|
|
1429
|
+
catch (err) {
|
|
1430
|
+
return { toast: { type: 'error', content: err.message } };
|
|
1431
|
+
}
|
|
1432
|
+
if (!updated) {
|
|
1433
|
+
// reload + find 后 updateText 不应返回 undefined;防御性兜底(如并发删除)。
|
|
1434
|
+
return { toast: { type: 'error', content: '指令不存在或已被删除' } };
|
|
1435
|
+
}
|
|
1436
|
+
// 成功:重渲染列表卡(保持页码)。必须把 card 放进 callback 响应体让飞书
|
|
1437
|
+
// 原地替换 pre-click 编辑卡——toast-only 响应会停留在编辑界面(飞书保留
|
|
1438
|
+
// 点击前那张卡),见 handleQueueInput / handleOrderAliasInput 同款注释。
|
|
1439
|
+
const result = this.cmdOrder([], ctx, offset);
|
|
1440
|
+
const preview = updated.text.length > 50 ? updated.text.slice(0, 50) + '...' : updated.text;
|
|
1441
|
+
return {
|
|
1442
|
+
toast: { type: 'success', content: `✅ 已更新指令: ${preview}` },
|
|
1443
|
+
card: { type: 'raw', data: result.card },
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1324
1446
|
/**
|
|
1325
1447
|
* Handle ws.use card action: switch to the workspace, then refresh the /ws
|
|
1326
1448
|
* list card in place so "recent" sort order is immediately visible.
|
|
@@ -1897,8 +2019,8 @@ export class CommandRouter {
|
|
|
1897
2019
|
{ cmd: 'resume', label: '/resume /r [agent] [list|id|N]', desc: '列出或切换 Agent session' },
|
|
1898
2020
|
{
|
|
1899
2021
|
cmd: 'order',
|
|
1900
|
-
label: '/order /o save|list',
|
|
1901
|
-
desc: '收藏指令 /order save
|
|
2022
|
+
label: '/order /o save|list|edit',
|
|
2023
|
+
desc: '收藏指令 /order save,编辑指令 /order edit <id|N> <新文本>,指令可起别名(卡片上 +别名)',
|
|
1902
2024
|
},
|
|
1903
2025
|
];
|
|
1904
2026
|
const bodyElements = [];
|
|
@@ -3550,6 +3672,31 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3550
3672
|
},
|
|
3551
3673
|
],
|
|
3552
3674
|
},
|
|
3675
|
+
{
|
|
3676
|
+
// 编辑:弹蓝色 header 输入卡(CardKit 2.0 input + ✓ 提交),
|
|
3677
|
+
// 镜像 order.aliasEdit 流程。order.textEdit 是 control-only action
|
|
3678
|
+
// (isImmediateAction 注册),不 spawn agent。
|
|
3679
|
+
tag: 'column',
|
|
3680
|
+
width: 'auto',
|
|
3681
|
+
elements: [
|
|
3682
|
+
{
|
|
3683
|
+
tag: 'button',
|
|
3684
|
+
text: { tag: 'plain_text', content: '编辑' },
|
|
3685
|
+
type: 'default',
|
|
3686
|
+
size: 'small',
|
|
3687
|
+
behaviors: [
|
|
3688
|
+
{
|
|
3689
|
+
type: 'callback',
|
|
3690
|
+
value: {
|
|
3691
|
+
cmd: 'order.textEdit',
|
|
3692
|
+
orderId: order.id,
|
|
3693
|
+
offset: safeOffset,
|
|
3694
|
+
},
|
|
3695
|
+
},
|
|
3696
|
+
],
|
|
3697
|
+
},
|
|
3698
|
+
],
|
|
3699
|
+
},
|
|
3553
3700
|
{
|
|
3554
3701
|
tag: 'column',
|
|
3555
3702
|
width: 'auto',
|
|
@@ -3648,8 +3795,30 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3648
3795
|
return { text: `保存失败: ${err.message}` };
|
|
3649
3796
|
}
|
|
3650
3797
|
}
|
|
3798
|
+
// /order edit <orderId|序号> <新文本> — CLI 备选路径(卡片是主入口)。
|
|
3799
|
+
// 与 /order save / alias add|rm 对称;复用 resolveOrderTarget。
|
|
3800
|
+
// 与卡片路径(handleOrderTextInput)对齐 trim 语义,避免「卡片 trim、CLI 不 trim」
|
|
3801
|
+
// 两条入口对同一文本存出不同结果。
|
|
3802
|
+
if (sub === 'edit') {
|
|
3803
|
+
const target = args[1];
|
|
3804
|
+
const newText = args.slice(2).join(' ').trim();
|
|
3805
|
+
if (!target || !newText) {
|
|
3806
|
+
return { text: '用法: /order edit <orderId|序号> <新文本>' };
|
|
3807
|
+
}
|
|
3808
|
+
const order = this.resolveOrderTarget(target);
|
|
3809
|
+
if (!order)
|
|
3810
|
+
return { text: `指令不存在: ${target}` };
|
|
3811
|
+
try {
|
|
3812
|
+
this.orderStore.updateText(order.id, newText);
|
|
3813
|
+
const preview = newText.length > 50 ? newText.slice(0, 50) + '...' : newText;
|
|
3814
|
+
return { text: `✅ 已更新指令: ${preview}` };
|
|
3815
|
+
}
|
|
3816
|
+
catch (err) {
|
|
3817
|
+
return { text: `保存失败: ${err.message}` };
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3651
3820
|
return {
|
|
3652
|
-
text: '用法: /order [list|save <text>|alias add <orderId|序号> <别名名>|alias rm <orderId|序号>]',
|
|
3821
|
+
text: '用法: /order [list|save <text>|edit <orderId|序号> <新文本>|alias add <orderId|序号> <别名名>|alias rm <orderId|序号>]',
|
|
3653
3822
|
};
|
|
3654
3823
|
}
|
|
3655
3824
|
/** /order alias add:按 orderId 或列表序号(1 基)解析目标指令并绑定别名。 */
|
|
@@ -312,6 +312,10 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
312
312
|
this.cwd = opts.cwd;
|
|
313
313
|
this.autoApprove = false;
|
|
314
314
|
this.pendingToolInputs.clear();
|
|
315
|
+
// 每次 run 独立:上次 run 会话内跟踪到的计划文件不得泄漏到本次
|
|
316
|
+
// (否则本次 ExitPlanMode 无 plan/planFilePath 时 resolvePlanContent
|
|
317
|
+
// 会误读上一次的计划文件当当前计划)。
|
|
318
|
+
this.planFilePath = '';
|
|
315
319
|
let proc;
|
|
316
320
|
try {
|
|
317
321
|
proc = await this.spawnChild(opts);
|
|
@@ -39,7 +39,7 @@ export declare const RpcErrorCode: {
|
|
|
39
39
|
export interface InitializeParams {
|
|
40
40
|
/** Protocol version — must be 1 (number, not string). */
|
|
41
41
|
protocolVersion: number;
|
|
42
|
-
/** Client capabilities:
|
|
42
|
+
/** Client capabilities: declare only reverse-RPC surfaces we implement. */
|
|
43
43
|
clientCapabilities: {
|
|
44
44
|
fs: {
|
|
45
45
|
readTextFile: boolean;
|
|
@@ -209,6 +209,12 @@ export declare const ServerRequestMethod: {
|
|
|
209
209
|
readonly REQUEST_PERMISSION: "session/request_permission";
|
|
210
210
|
/** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
|
|
211
211
|
readonly ELICITATION_CREATE: "elicitation/create";
|
|
212
|
+
/** kimi terminal 工具 reverse RPC:Bash 执行下放客户端(acp-terminal)。 */
|
|
213
|
+
readonly TERMINAL_CREATE: "terminal/create";
|
|
214
|
+
readonly TERMINAL_OUTPUT: "terminal/output";
|
|
215
|
+
readonly TERMINAL_WAIT_FOR_EXIT: "terminal/wait_for_exit";
|
|
216
|
+
readonly TERMINAL_KILL: "terminal/kill";
|
|
217
|
+
readonly TERMINAL_RELEASE: "terminal/release";
|
|
212
218
|
};
|
|
213
219
|
export interface PermissionOption {
|
|
214
220
|
/** Opaque option identifier — echo back in response outcome.optionId. */
|
|
@@ -269,3 +275,34 @@ export interface ElicitationCreateResponse {
|
|
|
269
275
|
/** accept 时的答案,key 为 q0..qn(按问题顺序)。 */
|
|
270
276
|
content?: Record<string, string | string[]>;
|
|
271
277
|
}
|
|
278
|
+
export interface TerminalCreateParams {
|
|
279
|
+
sessionId: string;
|
|
280
|
+
/** 恒为 'bash'(服务端 spawn 只接受 bash -c)。 */
|
|
281
|
+
command: string;
|
|
282
|
+
/** ['-c', <script>]:-c 在 args[0],脚本在 args[1]。 */
|
|
283
|
+
args: string[];
|
|
284
|
+
env?: Array<{
|
|
285
|
+
name: string;
|
|
286
|
+
value: string;
|
|
287
|
+
}>;
|
|
288
|
+
/** 可能为 null → 回退 session cwd。 */
|
|
289
|
+
cwd?: string | null;
|
|
290
|
+
/** 输出缓冲上限(字节),默认 4MB。 */
|
|
291
|
+
outputByteLimit?: number;
|
|
292
|
+
}
|
|
293
|
+
export interface TerminalOutputParams {
|
|
294
|
+
sessionId: string;
|
|
295
|
+
terminalId: string;
|
|
296
|
+
}
|
|
297
|
+
export interface TerminalWaitForExitParams {
|
|
298
|
+
sessionId: string;
|
|
299
|
+
terminalId: string;
|
|
300
|
+
}
|
|
301
|
+
export interface TerminalKillParams {
|
|
302
|
+
sessionId: string;
|
|
303
|
+
terminalId: string;
|
|
304
|
+
}
|
|
305
|
+
export interface TerminalReleaseParams {
|
|
306
|
+
sessionId: string;
|
|
307
|
+
terminalId: string;
|
|
308
|
+
}
|
|
@@ -73,5 +73,11 @@ export const ServerRequestMethod = {
|
|
|
73
73
|
REQUEST_PERMISSION: 'session/request_permission',
|
|
74
74
|
/** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
|
|
75
75
|
ELICITATION_CREATE: 'elicitation/create',
|
|
76
|
+
/** kimi terminal 工具 reverse RPC:Bash 执行下放客户端(acp-terminal)。 */
|
|
77
|
+
TERMINAL_CREATE: 'terminal/create',
|
|
78
|
+
TERMINAL_OUTPUT: 'terminal/output',
|
|
79
|
+
TERMINAL_WAIT_FOR_EXIT: 'terminal/wait_for_exit',
|
|
80
|
+
TERMINAL_KILL: 'terminal/kill',
|
|
81
|
+
TERMINAL_RELEASE: 'terminal/release',
|
|
76
82
|
};
|
|
77
83
|
//# sourceMappingURL=protocol-types.js.map
|
|
@@ -44,6 +44,13 @@ export declare class DshRunner implements AgentRunner {
|
|
|
44
44
|
private activeAbort;
|
|
45
45
|
private activeSessionId;
|
|
46
46
|
private running;
|
|
47
|
+
/**
|
|
48
|
+
* 已对齐模型的 session 集合:同一 session 二次 run 不再重发 selectModel
|
|
49
|
+
* (session.selectModel 会写服务端全局默认,每次 run 都写是写放大)。
|
|
50
|
+
* configuredModel 在实例生命周期内不变(/config 改模型会 clearRunners
|
|
51
|
+
* 重建实例 + 换新 sessionId),故「每 session 对齐一次」足以覆盖配置变化。
|
|
52
|
+
*/
|
|
53
|
+
private readonly alignedSessions;
|
|
47
54
|
constructor(opts: DshRunnerOptions);
|
|
48
55
|
get isRunning(): boolean;
|
|
49
56
|
getStatusInfo(): AgentStatusInfo;
|
|
@@ -28,6 +28,13 @@ export class DshRunner {
|
|
|
28
28
|
activeAbort = null;
|
|
29
29
|
activeSessionId = null;
|
|
30
30
|
running = false;
|
|
31
|
+
/**
|
|
32
|
+
* 已对齐模型的 session 集合:同一 session 二次 run 不再重发 selectModel
|
|
33
|
+
* (session.selectModel 会写服务端全局默认,每次 run 都写是写放大)。
|
|
34
|
+
* configuredModel 在实例生命周期内不变(/config 改模型会 clearRunners
|
|
35
|
+
* 重建实例 + 换新 sessionId),故「每 session 对齐一次」足以覆盖配置变化。
|
|
36
|
+
*/
|
|
37
|
+
alignedSessions = new Set();
|
|
31
38
|
constructor(opts) {
|
|
32
39
|
this.sessionReader = opts.sessionReader;
|
|
33
40
|
this.configuredModel = opts.model;
|
|
@@ -70,9 +77,32 @@ export class DshRunner {
|
|
|
70
77
|
sessionId = created.sessionId;
|
|
71
78
|
}
|
|
72
79
|
this.activeSessionId = sessionId;
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
80
|
+
// Startup abort:createSession 挂起期间 stop() 已 abort(此时 sessionId 尚
|
|
81
|
+
// 未知,stop() 无法 cancel)。createSession 返回后立即检查——已 abort 则
|
|
82
|
+
// cancel 这个刚建的 session 并中断,不让启动流程继续(selectModel/prompt)。
|
|
83
|
+
// 必须 yield interrupted result(不能裸 return):调用方依赖 result 收尾
|
|
84
|
+
// run 卡,否则 for-await 自然结束后无终态、卡片停留「运行中」。
|
|
85
|
+
if (abort.signal.aborted) {
|
|
86
|
+
try {
|
|
87
|
+
await this.client.unary('session.cancel', { sessionId });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* 取消失败仅告警,session 已 abort 无需继续 */
|
|
91
|
+
}
|
|
92
|
+
getLogger().info(`[dsh] run aborted during startup, cancelled session ${sessionId}`);
|
|
93
|
+
terminalEmitted = true;
|
|
94
|
+
yield {
|
|
95
|
+
type: 'result',
|
|
96
|
+
subtype: 'interrupted',
|
|
97
|
+
session_id: sessionId,
|
|
98
|
+
errorMessage: 'run stopped',
|
|
99
|
+
};
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// 模型/推理强度对齐(§4.3):会话首次 run 时 selectModel 一次
|
|
103
|
+
// (该调用同时写服务端全局默认,故不重复写);同一 session 后续 run
|
|
104
|
+
// 跳过。失败仅告警不阻断 run(模型校验由 DSH 负责)。
|
|
105
|
+
if (this.configuredModel && !this.alignedSessions.has(sessionId)) {
|
|
76
106
|
try {
|
|
77
107
|
await this.client.selectModel({
|
|
78
108
|
sessionId,
|
|
@@ -80,6 +110,7 @@ export class DshRunner {
|
|
|
80
110
|
model: this.configuredModel,
|
|
81
111
|
...(this.reasoningEffort ? { reasoningEffort: this.reasoningEffort } : {}),
|
|
82
112
|
});
|
|
113
|
+
this.alignedSessions.add(sessionId);
|
|
83
114
|
}
|
|
84
115
|
catch (err) {
|
|
85
116
|
getLogger().warn(`[dsh] selectModel failed for session ${sessionId} (run continues): ${err.message}`);
|
|
@@ -46,6 +46,21 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
|
|
|
46
46
|
private readonly compactPollTimeoutMs;
|
|
47
47
|
/** Pending approval requests: requestId → kind + view + options. */
|
|
48
48
|
private pendingApprovals;
|
|
49
|
+
/** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
|
|
50
|
+
private terminals;
|
|
51
|
+
/** terminalId 自增序号(term-1, term-2, …)。 */
|
|
52
|
+
private terminalSeq;
|
|
53
|
+
/**
|
|
54
|
+
* kimi 服务端对 Bash 工具的 tool_call/tool_call_update 通知(title='Bash')。
|
|
55
|
+
* Bash 的真实执行走 terminal/* reverse RPC 下放客户端,tool 事件由本 runner
|
|
56
|
+
* 自产(tool_use 带命令、tool_result 带本地输出);这些通知必须过滤,否则
|
|
57
|
+
* 卡片出现双份 Bash 面板(通知版无 rawInput、terminal embed 无 rawOutput,
|
|
58
|
+
* 只能渲染「_无输出_」)。收集到的 toolCallId 用于过滤对应的 update。
|
|
59
|
+
*/
|
|
60
|
+
private bashToolCallIds;
|
|
61
|
+
/** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
|
|
62
|
+
private activeCwd;
|
|
63
|
+
private readonly processStopper;
|
|
49
64
|
/** Tracker for the in-flight prompt request — needed for cancellation. */
|
|
50
65
|
private promptSettled;
|
|
51
66
|
constructor(opts: KimiAcpRunnerOptions);
|
|
@@ -102,5 +117,41 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
|
|
|
102
117
|
*/
|
|
103
118
|
private waitForCompactionRecord;
|
|
104
119
|
private handleNotification;
|
|
120
|
+
/**
|
|
121
|
+
* Filter kimi's own Bash tool notifications so the run card doesn't get a
|
|
122
|
+
* duplicate, output-less Bash panel alongside the runner-produced one.
|
|
123
|
+
*
|
|
124
|
+
* - tool_call title='Bash' → remember the toolCallId, drop the notification
|
|
125
|
+
* (the tool_use comes from terminal/create with the real command).
|
|
126
|
+
* - tool_call_update for a remembered id → drop (streaming args deltas and
|
|
127
|
+
* the terminal embed final update; the tool_result is emitted locally).
|
|
128
|
+
* Returns true when the notification should be swallowed.
|
|
129
|
+
*/
|
|
130
|
+
private filterBashNotifications;
|
|
105
131
|
private handleServerRequest;
|
|
132
|
+
/**
|
|
133
|
+
* Handle a kimi terminal/* reverse RPC by spawning a one-shot local process.
|
|
134
|
+
*
|
|
135
|
+
* 契约(kimi-code acp-terminal/acpTerminalRunner.ts):
|
|
136
|
+
* create → 立即回 {terminalId},进程异步跑,不阻塞 RPC;
|
|
137
|
+
* output → 回累计 stdout(非增量,服务端按 emitted 偏移切片);
|
|
138
|
+
* wait_for_exit → 未退出则等 exit 再回 {exitCode, signal};
|
|
139
|
+
* kill/release → 杀进程并回 {}。
|
|
140
|
+
*/
|
|
141
|
+
private handleTerminalRequest;
|
|
142
|
+
/**
|
|
143
|
+
* terminal/create: spawn 一次性进程(bash -c <script>),流式累积
|
|
144
|
+
* stdout(+stderr) 到缓冲,立即回 {terminalId}。
|
|
145
|
+
*/
|
|
146
|
+
private handleTerminalCreate;
|
|
147
|
+
/** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
|
|
148
|
+
private handleTerminalOutput;
|
|
149
|
+
/** terminal/wait_for_exit: 未退出则等 exit promise 再回 {exitCode, signal}。 */
|
|
150
|
+
private handleTerminalWaitForExit;
|
|
151
|
+
/** terminal/kill: 立即杀进程组,回 {}。 */
|
|
152
|
+
private handleTerminalKill;
|
|
153
|
+
/** terminal/release: 从 map 删除;进程仍在则一并杀,回 {}。 */
|
|
154
|
+
private handleTerminalRelease;
|
|
155
|
+
/** Kill every live terminal (dispose/connection-close 清理钩子)。 */
|
|
156
|
+
private cleanupTerminals;
|
|
106
157
|
}
|
|
@@ -11,19 +11,31 @@
|
|
|
11
11
|
* pattern, same forceFinish/stopRequested semantics, same turn idle timeout,
|
|
12
12
|
* same ConnectionLostError retry.
|
|
13
13
|
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
14
16
|
import { ConnectionManager as KimiAcpConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
|
|
15
17
|
import { KimiAcpTranslator } from './translator.js';
|
|
16
|
-
import { RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
|
|
18
|
+
import { NotificationMethod, RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
|
|
17
19
|
import { findOptionIdByKind } from '../../common/acp/protocol-helpers.js';
|
|
18
20
|
import { mapAnswersByIndex } from '../../question-common.js';
|
|
19
21
|
import { getLogger } from '../../../logger/index.js';
|
|
20
22
|
import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
|
|
23
|
+
import { ProcessStopper } from '../../common/process-stopper.js';
|
|
21
24
|
/** R2: how long to wait for the background compaction record after the
|
|
22
25
|
* /compact prompt settles (design doc §6.1). Real kimi 0.36.0 lands
|
|
23
26
|
* context.apply_compaction ~8s after end_turn. */
|
|
24
27
|
const COMPACT_POLL_TIMEOUT_MS = 30_000;
|
|
25
28
|
/** Poll interval for the wire.jsonl compaction record. */
|
|
26
29
|
const COMPACT_POLL_INTERVAL_MS = 1_000;
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// kimi terminal 工具(Bash 执行下放客户端)
|
|
32
|
+
// 契约:kimi-code/packages/acp-server/src/acp-terminal/acpTerminalRunner.ts
|
|
33
|
+
// 一次性 bash -c <script>,非交互 PTY、无 stdin;服务端每 250ms 轮询
|
|
34
|
+
// terminal/output,客户端只回累计 stdout。创建/轮询/等待/杀/释放五个
|
|
35
|
+
// reverse RPC 全部由本 runner 本地 spawn 子进程实现。
|
|
36
|
+
// =============================================================================
|
|
37
|
+
/** terminal/create 未显式给 outputByteLimit 时的缓冲上限(对齐服务端默认 4MB)。 */
|
|
38
|
+
const TERMINAL_DEFAULT_OUTPUT_LIMIT = 4 * 1024 * 1024;
|
|
27
39
|
/**
|
|
28
40
|
* Map user-facing permission mode to ACP mode id.
|
|
29
41
|
*
|
|
@@ -162,6 +174,21 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
162
174
|
compactPollTimeoutMs;
|
|
163
175
|
/** Pending approval requests: requestId → kind + view + options. */
|
|
164
176
|
pendingApprovals = new Map();
|
|
177
|
+
/** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
|
|
178
|
+
terminals = new Map();
|
|
179
|
+
/** terminalId 自增序号(term-1, term-2, …)。 */
|
|
180
|
+
terminalSeq = 0;
|
|
181
|
+
/**
|
|
182
|
+
* kimi 服务端对 Bash 工具的 tool_call/tool_call_update 通知(title='Bash')。
|
|
183
|
+
* Bash 的真实执行走 terminal/* reverse RPC 下放客户端,tool 事件由本 runner
|
|
184
|
+
* 自产(tool_use 带命令、tool_result 带本地输出);这些通知必须过滤,否则
|
|
185
|
+
* 卡片出现双份 Bash 面板(通知版无 rawInput、terminal embed 无 rawOutput,
|
|
186
|
+
* 只能渲染「_无输出_」)。收集到的 toolCallId 用于过滤对应的 update。
|
|
187
|
+
*/
|
|
188
|
+
bashToolCallIds = new Set();
|
|
189
|
+
/** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
|
|
190
|
+
activeCwd = null;
|
|
191
|
+
processStopper = new ProcessStopper({ graceMs: 2_000 });
|
|
165
192
|
/** Tracker for the in-flight prompt request — needed for cancellation. */
|
|
166
193
|
promptSettled = false;
|
|
167
194
|
constructor(opts) {
|
|
@@ -178,8 +205,13 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
178
205
|
initializeParams: {
|
|
179
206
|
protocolVersion: 1,
|
|
180
207
|
clientCapabilities: {
|
|
208
|
+
// fs 保持关闭:kimi 服务端在 fs 能力为 false 时走本地磁盘兜底
|
|
209
|
+
// (acpFsService.ts AcpHostFileSystem.inner),kimi 是本地子进程,
|
|
210
|
+
// 本地盘即工作区盘,读写正常;声明 true 则服务端改发 fs/* reverse
|
|
211
|
+
// RPC,lark-remote 未实现该面,文件读写会 METHOD_NOT_FOUND 失败。
|
|
181
212
|
fs: { readTextFile: false, writeTextFile: false },
|
|
182
|
-
terminal
|
|
213
|
+
// terminal 常开:yolo/auto 自主跑命令;manual 走 session/request_permission 审批。
|
|
214
|
+
terminal: true,
|
|
183
215
|
// AskUserQuestion 走 elicitation form(原生多题+多选;form 失败时
|
|
184
216
|
// kimi 服务端自动回退 request_permission 桥,客户端两者都处理)。
|
|
185
217
|
// 注意:ACP SDK 对 elicitation.form 的 zod schema 是对象
|
|
@@ -223,6 +255,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
223
255
|
this.currentTranslator = null;
|
|
224
256
|
this.promptSettled = false;
|
|
225
257
|
this.activeSessionId = null;
|
|
258
|
+
this.activeCwd = null;
|
|
226
259
|
}
|
|
227
260
|
async releaseConnection(cwd) {
|
|
228
261
|
await this.connectionManager.release(cwd);
|
|
@@ -231,6 +264,8 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
231
264
|
this.connectionManager.notifyIdle(cwd);
|
|
232
265
|
}
|
|
233
266
|
async disposeConnections() {
|
|
267
|
+
// 关闭前 kill/release 所有存活 terminal,避免孤儿 bash 进程。
|
|
268
|
+
await this.cleanupTerminals();
|
|
234
269
|
await this.connectionManager.disposeAll();
|
|
235
270
|
}
|
|
236
271
|
/**
|
|
@@ -245,10 +280,14 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
245
280
|
const client = await this.connectionManager.acquire(opts.cwd);
|
|
246
281
|
this.connectionManager.notifyActivity(opts.cwd);
|
|
247
282
|
this.currentClient = client;
|
|
283
|
+
this.activeCwd = opts.cwd;
|
|
248
284
|
client.setHooks({
|
|
249
285
|
onNotification: (method, params) => this.handleNotification(method, params),
|
|
250
286
|
onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
|
|
251
|
-
onClose: () =>
|
|
287
|
+
onClose: () => {
|
|
288
|
+
void this.cleanupTerminals();
|
|
289
|
+
this.failTurn('Kimi ACP connection closed');
|
|
290
|
+
},
|
|
252
291
|
});
|
|
253
292
|
const sessionId = opts.sessionId;
|
|
254
293
|
if (!sessionId) {
|
|
@@ -382,11 +421,13 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
382
421
|
const client = await this.connectionManager.acquire(opts.cwd);
|
|
383
422
|
this.connectionManager.notifyActivity(opts.cwd);
|
|
384
423
|
this.currentClient = client;
|
|
424
|
+
this.activeCwd = opts.cwd;
|
|
385
425
|
client.setHooks({
|
|
386
426
|
onNotification: (method, params) => this.handleNotification(method, params),
|
|
387
427
|
onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
|
|
388
428
|
onClose: () => {
|
|
389
429
|
getLogger().warn(`[${this.logTag}] client connection closed`);
|
|
430
|
+
void this.cleanupTerminals();
|
|
390
431
|
this.failTurn('Kimi ACP connection closed');
|
|
391
432
|
},
|
|
392
433
|
});
|
|
@@ -494,10 +535,61 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
494
535
|
getLogger().warn(`[${this.logTag}] compaction record not observed within ${this.compactPollTimeoutMs}ms (baseline=${baselineCount}); proceeding without confirmation`);
|
|
495
536
|
}
|
|
496
537
|
handleNotification(method, params) {
|
|
538
|
+
// Terminal-backed Bash 的通知走 runner 自产事件(见 bashToolCallIds),
|
|
539
|
+
// 不交给 translator;其余通知(Read/Edit/text/thinking)正常翻译。
|
|
540
|
+
if (method === NotificationMethod.SESSION_UPDATE && this.filterBashNotifications(params)) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
497
543
|
const events = this.currentTranslator?.handleNotification(method, params) ?? [];
|
|
498
544
|
this.pushEvents(events);
|
|
499
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Filter kimi's own Bash tool notifications so the run card doesn't get a
|
|
548
|
+
* duplicate, output-less Bash panel alongside the runner-produced one.
|
|
549
|
+
*
|
|
550
|
+
* - tool_call title='Bash' → remember the toolCallId, drop the notification
|
|
551
|
+
* (the tool_use comes from terminal/create with the real command).
|
|
552
|
+
* - tool_call_update for a remembered id → drop (streaming args deltas and
|
|
553
|
+
* the terminal embed final update; the tool_result is emitted locally).
|
|
554
|
+
* Returns true when the notification should be swallowed.
|
|
555
|
+
*/
|
|
556
|
+
filterBashNotifications(params) {
|
|
557
|
+
if (typeof params !== 'object' || params === null)
|
|
558
|
+
return false;
|
|
559
|
+
const update = params.update;
|
|
560
|
+
if (!update || typeof update !== 'object')
|
|
561
|
+
return false;
|
|
562
|
+
if (update.sessionUpdate === 'tool_call') {
|
|
563
|
+
if (update.title === 'Bash') {
|
|
564
|
+
if (typeof update.toolCallId === 'string') {
|
|
565
|
+
this.bashToolCallIds.add(update.toolCallId);
|
|
566
|
+
}
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
if (update.sessionUpdate === 'tool_call_update') {
|
|
572
|
+
const toolCallId = update.toolCallId;
|
|
573
|
+
if (typeof toolCallId === 'string' && this.bashToolCallIds.has(toolCallId)) {
|
|
574
|
+
// 终态后清掉 id,避免集合随会话无限增长(漏清无碍:id 全局唯一)。
|
|
575
|
+
if (update.status === 'completed' || update.status === 'failed') {
|
|
576
|
+
this.bashToolCallIds.delete(toolCallId);
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
500
584
|
handleServerRequest(id, method, params) {
|
|
585
|
+
// kimi terminal 工具:Bash 执行下放客户端(acp-terminal reverse RPC)。
|
|
586
|
+
// 请求本身是纯 request/response I/O,不交给 translator(避免落入下方
|
|
587
|
+
// 「空事件 → 拒绝」兜底);对应的 tool_use/tool_result 事件由
|
|
588
|
+
// handleTerminalCreate 自产(带本地命令与输出,见 bashToolCallIds)。
|
|
589
|
+
if (method.startsWith('terminal/')) {
|
|
590
|
+
void this.handleTerminalRequest(id, method, params);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
501
593
|
const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
|
|
502
594
|
if (events.length === 0) {
|
|
503
595
|
// Unhandled/unsupported server request must be explicitly responded to
|
|
@@ -527,5 +619,221 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
527
619
|
}
|
|
528
620
|
this.pushEvents(events);
|
|
529
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Handle a kimi terminal/* reverse RPC by spawning a one-shot local process.
|
|
624
|
+
*
|
|
625
|
+
* 契约(kimi-code acp-terminal/acpTerminalRunner.ts):
|
|
626
|
+
* create → 立即回 {terminalId},进程异步跑,不阻塞 RPC;
|
|
627
|
+
* output → 回累计 stdout(非增量,服务端按 emitted 偏移切片);
|
|
628
|
+
* wait_for_exit → 未退出则等 exit 再回 {exitCode, signal};
|
|
629
|
+
* kill/release → 杀进程并回 {}。
|
|
630
|
+
*/
|
|
631
|
+
async handleTerminalRequest(id, method, params) {
|
|
632
|
+
try {
|
|
633
|
+
switch (method) {
|
|
634
|
+
case ServerRequestMethod.TERMINAL_CREATE:
|
|
635
|
+
this.handleTerminalCreate(id, params);
|
|
636
|
+
return;
|
|
637
|
+
case ServerRequestMethod.TERMINAL_OUTPUT:
|
|
638
|
+
this.handleTerminalOutput(id, params);
|
|
639
|
+
return;
|
|
640
|
+
case ServerRequestMethod.TERMINAL_WAIT_FOR_EXIT:
|
|
641
|
+
await this.handleTerminalWaitForExit(id, params);
|
|
642
|
+
return;
|
|
643
|
+
case ServerRequestMethod.TERMINAL_KILL:
|
|
644
|
+
await this.handleTerminalKill(id, params);
|
|
645
|
+
return;
|
|
646
|
+
case ServerRequestMethod.TERMINAL_RELEASE:
|
|
647
|
+
await this.handleTerminalRelease(id, params);
|
|
648
|
+
return;
|
|
649
|
+
default:
|
|
650
|
+
this.currentClient?.respondError(id, RpcErrorCode.METHOD_NOT_FOUND, `Unsupported server request: ${method}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
getLogger().warn(`[${this.logTag}] terminal request failed method=${method}: ${err.message}`);
|
|
655
|
+
this.currentClient?.respondError(id, RpcErrorCode.INTERNAL_ERROR, `Terminal request failed: ${err.message}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* terminal/create: spawn 一次性进程(bash -c <script>),流式累积
|
|
660
|
+
* stdout(+stderr) 到缓冲,立即回 {terminalId}。
|
|
661
|
+
*/
|
|
662
|
+
handleTerminalCreate(id, params) {
|
|
663
|
+
const env = params.env
|
|
664
|
+
? Object.fromEntries(params.env.map((e) => [e.name, e.value]))
|
|
665
|
+
: undefined;
|
|
666
|
+
const cwd = params.cwd ?? this.activeCwd ?? undefined;
|
|
667
|
+
const outputLimit = params.outputByteLimit ?? TERMINAL_DEFAULT_OUTPUT_LIMIT;
|
|
668
|
+
const proc = spawn(params.command, params.args ?? [], {
|
|
669
|
+
cwd,
|
|
670
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
671
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
672
|
+
// ProcessStopper 用负 PID 杀进程组(kill(-pgid));子进程必须是组长
|
|
673
|
+
// 才能命中,否则 kill(-pid) 抛 ESRCH 被吞 → kill/release/清理全失效。
|
|
674
|
+
// 与 JsonlRpcTransport / spawning-runner 的 detached:true 同模式。
|
|
675
|
+
detached: true,
|
|
676
|
+
});
|
|
677
|
+
// 每个流独立 decoder:多字节字符跨 chunk 拆分时由 decoder 保留不完整
|
|
678
|
+
// 序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符。
|
|
679
|
+
const stdoutDecoder = new StringDecoder('utf8');
|
|
680
|
+
const stderrDecoder = new StringDecoder('utf8');
|
|
681
|
+
const append = (chunk, decoder) => {
|
|
682
|
+
if (handle.truncated)
|
|
683
|
+
return;
|
|
684
|
+
const remaining = handle.outputLimit - handle.outputBytes;
|
|
685
|
+
if (remaining > 0 && chunk.length <= remaining) {
|
|
686
|
+
handle.output += decoder.write(chunk);
|
|
687
|
+
handle.outputBytes += chunk.length;
|
|
688
|
+
}
|
|
689
|
+
else if (remaining > 0) {
|
|
690
|
+
// 只喂 remaining 字节;decoder 内部保留末尾不完整序列,不会在字节
|
|
691
|
+
// 边界产生替换符。outputBytes 按「喂入字节」计,保证不超过上限。
|
|
692
|
+
handle.output += decoder.write(chunk.subarray(0, remaining));
|
|
693
|
+
handle.outputBytes += remaining;
|
|
694
|
+
}
|
|
695
|
+
// 契约(手册):truncated = 缓冲 >= limit——恰好填满也标 true。
|
|
696
|
+
if (handle.outputBytes >= handle.outputLimit)
|
|
697
|
+
handle.truncated = true;
|
|
698
|
+
};
|
|
699
|
+
proc.stdout?.on('data', (chunk) => append(chunk, stdoutDecoder));
|
|
700
|
+
proc.stderr?.on('data', (chunk) => append(chunk, stderrDecoder));
|
|
701
|
+
// spawn 失败(如 ENOENT)只发 'error' 不发 'exit'——一并 resolve,防止
|
|
702
|
+
// wait_for_exit 永久挂起(runner 层 ENOENT 兜底红线)。
|
|
703
|
+
// 用 resolveExit 承接,避免回调在 handle 初始化前引用它(TDZ)。
|
|
704
|
+
let resolveExit;
|
|
705
|
+
const exitPromise = new Promise((resolve) => {
|
|
706
|
+
resolveExit = resolve;
|
|
707
|
+
});
|
|
708
|
+
// 与 resolveExit 同款 TDZ 规避:tool_result 发射闭包在 terminalId 初始化
|
|
709
|
+
// 后赋值,exit/error 回调执行时经 emitResult 间接调用。
|
|
710
|
+
let emitTerminalResult = undefined;
|
|
711
|
+
const emitResult = () => {
|
|
712
|
+
emitTerminalResult?.();
|
|
713
|
+
};
|
|
714
|
+
const handle = {
|
|
715
|
+
proc,
|
|
716
|
+
output: '',
|
|
717
|
+
outputBytes: 0,
|
|
718
|
+
outputLimit,
|
|
719
|
+
truncated: false,
|
|
720
|
+
exitCode: null,
|
|
721
|
+
signal: null,
|
|
722
|
+
exitPromise,
|
|
723
|
+
};
|
|
724
|
+
proc.once('exit', (code, signal) => {
|
|
725
|
+
handle.exitCode = code;
|
|
726
|
+
handle.signal = signal;
|
|
727
|
+
resolveExit?.();
|
|
728
|
+
emitResult();
|
|
729
|
+
});
|
|
730
|
+
proc.once('error', (err) => {
|
|
731
|
+
getLogger().warn(`[${this.logTag}] terminal process error: ${err.message}`);
|
|
732
|
+
if (handle.exitCode === null && handle.signal === null) {
|
|
733
|
+
handle.exitCode = 1;
|
|
734
|
+
handle.signal = null;
|
|
735
|
+
}
|
|
736
|
+
resolveExit?.();
|
|
737
|
+
emitResult();
|
|
738
|
+
});
|
|
739
|
+
const terminalId = `term-${++this.terminalSeq}`;
|
|
740
|
+
this.terminals.set(terminalId, handle);
|
|
741
|
+
// Bash 工具事件由本 runner 自产(kimi 通知已过滤):create 即推 tool_use
|
|
742
|
+
// 带真实命令,让 run 卡立刻出现可识别的 Bash 面板。
|
|
743
|
+
const script = Array.isArray(params.args) && params.args.length > 1 ? params.args[1] : params.command;
|
|
744
|
+
let resultEmitted = false;
|
|
745
|
+
emitTerminalResult = () => {
|
|
746
|
+
if (resultEmitted)
|
|
747
|
+
return;
|
|
748
|
+
resultEmitted = true;
|
|
749
|
+
this.pushEvents([
|
|
750
|
+
{
|
|
751
|
+
type: 'user',
|
|
752
|
+
message: {
|
|
753
|
+
content: [
|
|
754
|
+
{
|
|
755
|
+
type: 'tool_result',
|
|
756
|
+
tool_use_id: terminalId,
|
|
757
|
+
content: handle.output,
|
|
758
|
+
is_error: handle.exitCode !== 0,
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
},
|
|
762
|
+
timestamp: new Date().toISOString(),
|
|
763
|
+
},
|
|
764
|
+
]);
|
|
765
|
+
};
|
|
766
|
+
this.pushEvents([
|
|
767
|
+
{
|
|
768
|
+
type: 'assistant',
|
|
769
|
+
message: {
|
|
770
|
+
content: [
|
|
771
|
+
{
|
|
772
|
+
type: 'tool_use',
|
|
773
|
+
id: terminalId,
|
|
774
|
+
name: 'Bash',
|
|
775
|
+
input: { command: script, cwd },
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
},
|
|
779
|
+
timestamp: new Date().toISOString(),
|
|
780
|
+
},
|
|
781
|
+
]);
|
|
782
|
+
getLogger().info(`[${this.logTag}] terminal created id=${terminalId} command=${params.command} args=${JSON.stringify(params.args ?? [])}`);
|
|
783
|
+
this.currentClient?.respond(id, { terminalId });
|
|
784
|
+
}
|
|
785
|
+
/** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
|
|
786
|
+
handleTerminalOutput(id, params) {
|
|
787
|
+
const handle = this.terminals.get(params.terminalId);
|
|
788
|
+
if (!handle) {
|
|
789
|
+
this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
this.currentClient?.respond(id, {
|
|
793
|
+
output: handle.output,
|
|
794
|
+
truncated: handle.truncated,
|
|
795
|
+
exitStatus: { exitCode: handle.exitCode, signal: handle.signal },
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/** terminal/wait_for_exit: 未退出则等 exit promise 再回 {exitCode, signal}。 */
|
|
799
|
+
async handleTerminalWaitForExit(id, params) {
|
|
800
|
+
const handle = this.terminals.get(params.terminalId);
|
|
801
|
+
if (!handle) {
|
|
802
|
+
this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
await handle.exitPromise;
|
|
806
|
+
this.currentClient?.respond(id, { exitCode: handle.exitCode, signal: handle.signal });
|
|
807
|
+
}
|
|
808
|
+
/** terminal/kill: 立即杀进程组,回 {}。 */
|
|
809
|
+
async handleTerminalKill(id, params) {
|
|
810
|
+
const handle = this.terminals.get(params.terminalId);
|
|
811
|
+
if (!handle) {
|
|
812
|
+
this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
await this.processStopper.stop(handle.proc, { immediate: true });
|
|
816
|
+
this.currentClient?.respond(id, {});
|
|
817
|
+
}
|
|
818
|
+
/** terminal/release: 从 map 删除;进程仍在则一并杀,回 {}。 */
|
|
819
|
+
async handleTerminalRelease(id, params) {
|
|
820
|
+
const handle = this.terminals.get(params.terminalId);
|
|
821
|
+
if (!handle) {
|
|
822
|
+
this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
this.terminals.delete(params.terminalId);
|
|
826
|
+
// stop 对已退出进程是 no-op;仍在跑则立即 SIGTERM+SIGKILL。
|
|
827
|
+
await this.processStopper.stop(handle.proc, { immediate: true });
|
|
828
|
+
this.currentClient?.respond(id, {});
|
|
829
|
+
}
|
|
830
|
+
/** Kill every live terminal (dispose/connection-close 清理钩子)。 */
|
|
831
|
+
async cleanupTerminals() {
|
|
832
|
+
const handles = [...this.terminals.values()];
|
|
833
|
+
this.terminals.clear();
|
|
834
|
+
for (const handle of handles) {
|
|
835
|
+
await this.processStopper.stop(handle.proc, { immediate: true });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
530
838
|
}
|
|
531
839
|
//# sourceMappingURL=runner.js.map
|
|
@@ -5,8 +5,9 @@ import type { AgentKind, Runner } from './index.js';
|
|
|
5
5
|
* up `config.defaultAgent` here to pick the concrete runner.
|
|
6
6
|
*
|
|
7
7
|
* Singleton caching: the factory MAY cache its instance internally. Most
|
|
8
|
-
* current agents (claude, opencode, pi, kimi) are spawn-per-message; codex is
|
|
9
|
-
* workspace-lifetime (app-server)
|
|
8
|
+
* current spawn-type agents (claude, opencode, pi, kimi) are spawn-per-message; codex is
|
|
9
|
+
* workspace-lifetime (app-server); dsh is HTTP+WS direct connection (no local
|
|
10
|
+
* subprocess). Each factory returns a runner per call.
|
|
10
11
|
*
|
|
11
12
|
* Dynamic config reload: factory can read latest config via registry's
|
|
12
13
|
* configContainer, enabling runtime config changes to take effect.
|
package/dist/runner/registry.js
CHANGED
|
@@ -9,8 +9,9 @@ let globalRegistry;
|
|
|
9
9
|
* up `config.defaultAgent` here to pick the concrete runner.
|
|
10
10
|
*
|
|
11
11
|
* Singleton caching: the factory MAY cache its instance internally. Most
|
|
12
|
-
* current agents (claude, opencode, pi, kimi) are spawn-per-message; codex is
|
|
13
|
-
* workspace-lifetime (app-server)
|
|
12
|
+
* current spawn-type agents (claude, opencode, pi, kimi) are spawn-per-message; codex is
|
|
13
|
+
* workspace-lifetime (app-server); dsh is HTTP+WS direct connection (no local
|
|
14
|
+
* subprocess). Each factory returns a runner per call.
|
|
14
15
|
*
|
|
15
16
|
* Dynamic config reload: factory can read latest config via registry's
|
|
16
17
|
* configContainer, enabling runtime config changes to take effect.
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -337,7 +337,7 @@ export interface SpawnOptions {
|
|
|
337
337
|
settings?: string;
|
|
338
338
|
}
|
|
339
339
|
/**
|
|
340
|
-
* Seam (§3): the contract any agent runner (claude/codex/opencode/pi/kimi)
|
|
340
|
+
* Seam (§3): the contract any agent runner (claude/codex/opencode/pi/kimi/dsh)
|
|
341
341
|
* must satisfy for the router/bridge.
|
|
342
342
|
* Declared here so callers depend on the interface, not the concrete
|
|
343
343
|
* runner classes — test stubs satisfy this structurally.
|
|
@@ -474,7 +474,7 @@ export interface AgentRunner extends Runner {
|
|
|
474
474
|
}
|
|
475
475
|
/** Agent self-describing status info for /status display. */
|
|
476
476
|
export interface AgentStatusInfo {
|
|
477
|
-
/** Agent kind (claude/codex/opencode/pi/kimi). */
|
|
477
|
+
/** Agent kind (claude/codex/opencode/pi/kimi/dsh). */
|
|
478
478
|
kind: AgentKind;
|
|
479
479
|
/** Display model name. */
|
|
480
480
|
model: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared pagination / truncation helpers for agent session readers.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
4
|
+
* The six readers (claude/codex/opencode/pi/kimi/dsh) all implement the same
|
|
5
5
|
* offset-clamp + slice + total pattern for `listSessions`, and the same
|
|
6
6
|
* maxEvents tail-cap for `readSessionContent`. These were duplicated (and had
|
|
7
7
|
* drifted on the default limit). Centralized here so the contract is defined
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared pagination / truncation helpers for agent session readers.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
4
|
+
* The six readers (claude/codex/opencode/pi/kimi/dsh) all implement the same
|
|
5
5
|
* offset-clamp + slice + total pattern for `listSessions`, and the same
|
|
6
6
|
* maxEvents tail-cap for `readSessionContent`. These were duplicated (and had
|
|
7
7
|
* drifted on the default limit). Centralized here so the contract is defined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-remote",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|