lark-remote 0.1.9 → 0.1.10
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 +20 -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/dsh/runner.d.ts +7 -0
- package/dist/runner/dsh/runner.js +34 -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,26 @@ 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.10] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- `/order` 卡片指令文本编辑:卡片「编辑」按钮弹出输入框原地修改(保留别名/使用统计/创建时间),CLI `/order edit <orderId|序号> <新文本>` 备选入口;长度上限 200 字符、空白提交报错、文本未变化短路写盘
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
|
|
16
|
+
- cardAction 直返白名单补齐:`order.textInput` 与 `approval.planFeedback` 不再被 fire-and-forget 的 `enqueueImmediate` 吞掉返回值(编辑卡停留编辑界面 / 计划审批意见 toast 消失)
|
|
17
|
+
- DSH `selectModel` 写放大:`session.selectModel`(会写服务端全局默认)改为每 session 首次 run 对齐一次,不再每次 run 重复写;启动期 `/stop` 会取消刚创建的 session 并产出 `interrupted` 终态而非静默返回
|
|
18
|
+
- order 末页删空守卫:删除末页唯一指令后 `offset` 回退到非空页(不再出现空白卡)
|
|
19
|
+
- 计划审批意见门控:仅 `ExitPlanMode` 工具审批接受修改意见,其他审批类型拒绝
|
|
20
|
+
- 计划文件跨 run 重置:`doStartProcess` 清空会话内追踪的计划文件,避免旧计划被 `resolvePlanContent` 误读
|
|
21
|
+
- finalizeRun fallback 卡按 `compactSupported` 门控 Compact 按钮(无 `runCompact` 能力的 agent 不再渲染死按钮)
|
|
22
|
+
|
|
23
|
+
### 变更
|
|
24
|
+
|
|
25
|
+
- ESLint 告警清零(0 errors 0 warnings)
|
|
26
|
+
- 注释与文档同步 DSH 接入("5 个 agent"→"6 个 agent")
|
|
27
|
+
|
|
8
28
|
## [0.1.9] - 2026-08-20
|
|
9
29
|
|
|
10
30
|
### 新增
|
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);
|
|
@@ -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}`);
|
|
@@ -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.10",
|
|
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",
|