lark-remote 0.5.6 → 0.5.7
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 +30 -6
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/bridge/index.d.ts +48 -1
- package/dist/bridge/index.js +86 -14
- package/dist/card/card-budget.d.ts +37 -0
- package/dist/card/card-budget.js +128 -5
- package/dist/card/run-renderer.d.ts +24 -0
- package/dist/card/run-renderer.js +256 -60
- package/dist/card/run-state.d.ts +26 -0
- package/dist/card/run-state.js +45 -0
- package/dist/card/time.d.ts +14 -0
- package/dist/card/time.js +30 -0
- package/dist/card/tool-render.d.ts +15 -2
- package/dist/card/tool-render.js +40 -66
- package/dist/common/text-width.d.ts +24 -0
- package/dist/common/text-width.js +93 -0
- package/dist/common/tool-summary.d.ts +11 -0
- package/dist/common/tool-summary.js +153 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/index.js +61 -6
- package/dist/platform/command.js +1 -1
- package/dist/platform/shell.d.ts +2 -2
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/router/card-helpers.d.ts +18 -16
- package/dist/router/card-helpers.js +65 -4
- package/dist/router/index.d.ts +9 -0
- package/dist/router/index.js +57 -7
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +3 -3
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +3 -3
- package/dist/runner/common/spawning-runner.d.ts +1 -1
- package/dist/runner/common/spawning-runner.js +2 -2
- package/dist/runner/dsh/auth.js +2 -1
- package/dist/runner/dsh/client.d.ts +100 -3
- package/dist/runner/dsh/client.js +289 -4
- package/dist/runner/dsh/exit-cancel.d.ts +32 -0
- package/dist/runner/dsh/exit-cancel.js +72 -0
- package/dist/runner/dsh/inflight.d.ts +62 -0
- package/dist/runner/dsh/inflight.js +140 -0
- package/dist/runner/dsh/runner.d.ts +90 -4
- package/dist/runner/dsh/runner.js +436 -18
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +103 -1
- package/dist/runner/dsh/types.d.ts +60 -0
- package/dist/runner/types.d.ts +76 -2
- package/dist/session/claude/sessions.js +2 -2
- package/dist/session/dsh/sessions.js +100 -15
- package/dist/update/install.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ 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.5.7] - 2026-10-20
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- **DSH 运行的工具调用与运行状态进卡**:此前 DSH 直连的运行卡片只有正文、看不到任何工具调用。现在工具块以原生折叠面板呈现(与思考块同款交互,点标题展开、不经过服务端往返),运行中或失败的默认展开,正文按卡片内工具数量分配截断预算;摘要识别 DSH 的工具名与参数(`glob`、`web_search`、`todo_write`、`subagent`、`job_*`、`terminal_*` 等),标题显示不下时按**显示宽度**做中间省略——保住工具名与路径结尾,而不是从尾部切掉文件名。卡片还会投影 DSH 的 `session/control` 状态流(步数与上下文占用),并在首帧列出仍在后台运行的任务(终端里 `run_in_background` 起的进程活过了本轮),终态卡片可直接停止它们
|
|
13
|
+
- **恢复卡片能看出思路**:DSH 的历史块分 `text` / `reasoning` / `tool-call` 三类,此前只认 `text`,于是一个真实会话里 371 段思考、12 段正文的恢复卡只剩一串工具调用。现在思考映射成 thinking、正文映射成回复,工具调用与结果合成同一个面板(标题带工具名与命令,失败标 `❌`,正文是输出)
|
|
14
|
+
- **`/resume` 卡保住「我上一句 + 你最后一句」**:卡片只显示最后 N 个块,用户最关心的「我刚说了什么、它最后答了什么」常被中间的工具往返挤出窗口。现在这两句有独立名额
|
|
15
|
+
|
|
16
|
+
### 修复
|
|
17
|
+
|
|
18
|
+
- **手机端窄屏排版**:状态行原先用并排短列布局,窄屏上会被挤成竖排(「运行中」三个字竖着排),且列宽由客户端决定、服务端控制不了。改为单行单块 + ` · ` 分隔,并补上本轮耗时
|
|
19
|
+
- **卡片元素总数上限按实测收敛**:飞书拒绝的判据是递归 `elements` 成员数量,此前记录有误。现在运行卡片同时受字节预算与元素预算约束,超限时从最旧的步骤开始省略,不再整卡发送失败
|
|
20
|
+
- **DSH 的停止不再假报已停止**:回合跑在共享的 `dsh web` 宿主里,退出本地进程带不走它。停止现在分两步——prompt 还没被回合取走时从宿主收件箱撤回,已被取走才发取消并用 `session/list.running` 轮询确认;无法确认时明确告知「可能仍在后台运行」,而不是报一句已停止。取消确认落地前,下一条消息由门禁拦住,避免会话级取消打到新回合;每次提交的在途身份另存一份,下次启动对账:清掉残留的待执行 prompt,对仍开着的回合只提示不代杀
|
|
21
|
+
- **排队与压缩的回合归属**:排队中的 prompt 此前可能在上一个回合结束时被收尾,现在绑定到自己的回合;`/compact` 执行前先探服务端是否还有在途回合;进程退出时取消尚未收尾的回合,不留无人订阅的孤儿回合
|
|
22
|
+
- **空闲判定改用会话状态**:此前靠事件流静默推断,工具往返密集时容易误判。现在读 `session/list.running`,事件探测降为兜底
|
|
23
|
+
- **回顾卡不再被系统注入内容灌满**:DSH 的 `user/message` 里混着 agent 指令、技能清单、运行期上下文、压缩检查点等注入项,此前照单全收,回顾卡上看不见用户真正说过的话。现在按类型过滤,系统通知单独标 `🔔`
|
|
24
|
+
- **Windows 与 CI 稳定性**:退出路径的取消与凭据检查统一走平台判定;若干用例的凭据路径断言跟随 `path.join`,不再写死 POSIX 字面量
|
|
25
|
+
|
|
26
|
+
### 变更
|
|
27
|
+
|
|
28
|
+
- **英文文档补齐 DSH 部分**:`docs/en/architecture/design.md` 补上停止与取消确认的叙述,`docs/en/architecture/streaming-card.md` 更正工具折叠的口径(此前写的「连续工具超过阈值折叠」已被实现取代),两侧不再有互相矛盾的说法
|
|
29
|
+
- **用户文档纠正过期说法**:`/order` 别名的命令形态与存储位置(英文侧仍写已移除的 `/order alias <name> <text>` 与 `aliases.json`,实际是 `alias add|rm <orderId|序号>`、随指令存在 `orders.json`)、`/ws save` 示例多带一个实际不生效的路径参数、贡献指南要求 `bun run test` 而入门文档教的是 `bun test`、README 里真机用例的缺失目录前缀
|
|
30
|
+
- **注释与文档整理**:入站消息两篇架构文档改为描述当前实现,测试文件头不再点名已不存在的文件。无行为变化
|
|
31
|
+
|
|
8
32
|
## [0.5.6] - 2026-10-18
|
|
9
33
|
|
|
10
34
|
### 新增
|
|
@@ -20,7 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
20
44
|
### 变更
|
|
21
45
|
|
|
22
46
|
- **英文文档补齐 DSH 部分**:`docs/en/architecture/design.md` 补上直连与凭据签发、审批与提问转写、压缩能力声明;`docs/en/usage.md` 补 `manageProcess` / `dshHome` / `dshCommand` / `startTimeoutMs` / `permissionPreset` 五项配置说明,并纠正一处过期说法——原先写着 `token` 可以在 `/config` 卡片里编辑,实际该字段早已移出卡片
|
|
23
|
-
-
|
|
47
|
+
- **注释与发布条目整理**。无行为变化
|
|
24
48
|
|
|
25
49
|
## [0.5.5] - 2026-10-18
|
|
26
50
|
|
|
@@ -68,7 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
68
92
|
|
|
69
93
|
### 变更
|
|
70
94
|
|
|
71
|
-
-
|
|
95
|
+
- **注释里的运维数据与仓库外引用改为占位描述**。无行为变化
|
|
72
96
|
|
|
73
97
|
## [0.5.2] - 2026-10-11
|
|
74
98
|
|
|
@@ -96,7 +120,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
96
120
|
|
|
97
121
|
- **消除两处宿主环境相关的测试假红**:宿主带上本机没有的 locale(macOS 常见的 `LC_ALL=C.UTF-8`)时,bash 每次启动都往 stderr 打 setlocale 警告,让输出断言随机变红,测试 setup 阶段现在剔掉该变量(保留 UTF-8 解析所需项);JSON-RPC 退出计时用例的计时起点从进程启动改到收到 `initialize` 应答,慢速 Windows runner 上不再因启动耗时超过阈值而误红
|
|
98
122
|
- **英文架构文档补齐**:`docs/en/architecture/design.md` 增补与中文侧对应的五个章节,英中两侧不再不对称
|
|
99
|
-
-
|
|
123
|
+
- **注释与用例名整理**:让公开仓库里的引用都能落地。无行为变化
|
|
100
124
|
|
|
101
125
|
## [0.5.0] - 2026-10-07
|
|
102
126
|
|
|
@@ -114,7 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
114
138
|
|
|
115
139
|
### 变更
|
|
116
140
|
|
|
117
|
-
-
|
|
141
|
+
- **注释与测试命名整理**:让公开仓库里的引用都能落地。无行为变化
|
|
118
142
|
|
|
119
143
|
## [0.4.5] - 2026-10-07
|
|
120
144
|
|
|
@@ -468,13 +492,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
468
492
|
### 测试
|
|
469
493
|
|
|
470
494
|
- 合并 stub 工厂、净化 fixtures、移除冗余测试
|
|
471
|
-
- 新增
|
|
495
|
+
- 新增 configContainer 锚点测试(含 codex 负向断言)
|
|
472
496
|
|
|
473
497
|
## [0.1.2] - 2026-08-10
|
|
474
498
|
|
|
475
499
|
### 修复
|
|
476
500
|
|
|
477
|
-
- claude / opencode / kimi 的 Agent 工厂改为从 `configContainer`
|
|
501
|
+
- claude / opencode / kimi 的 Agent 工厂改为从 `configContainer` 读取配置,避免使用过期闭包导致配置不生效
|
|
478
502
|
- codex / kimi 的 `readSessionContent` 增加 cwd 校验,防止越界读取会话
|
|
479
503
|
- `listClaudeSessions` / `isClaudeSessionActive` 支持跨目录回退,处理会话迁移后的定位
|
|
480
504
|
- 修复 `handleQueueInput` 竞态:替换任务在 await 之前注册
|
package/README.en.md
CHANGED
|
@@ -137,8 +137,8 @@ bun run lint # eslint
|
|
|
137
137
|
|
|
138
138
|
# Live Feishu API integration tests (skipped by default; need valid
|
|
139
139
|
# credentials under ~/.lark-remote-test)
|
|
140
|
-
FEISHU_LIVE_TEST=1 bun run test tests/feishu-card-form-error.test.ts
|
|
141
|
-
FEISHU_LIVE_TEST=1 bun run test tests/feishu-reaction-emoji-live.test.ts
|
|
140
|
+
FEISHU_LIVE_TEST=1 bun run test tests/feishu-live/feishu-card-form-error.test.ts
|
|
141
|
+
FEISHU_LIVE_TEST=1 bun run test tests/feishu-live/feishu-reaction-emoji-live.test.ts
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
## Documentation
|
package/README.md
CHANGED
|
@@ -141,8 +141,8 @@ bun run typecheck # tsc --noEmit
|
|
|
141
141
|
bun run lint # eslint
|
|
142
142
|
|
|
143
143
|
# 真实飞书 API 集成测试(默认跳过,需要 ~/.lark-remote-test 下的有效凭据)
|
|
144
|
-
FEISHU_LIVE_TEST=1 bun run test tests/feishu-card-form-error.test.ts
|
|
145
|
-
FEISHU_LIVE_TEST=1 bun run test tests/feishu-reaction-emoji-live.test.ts
|
|
144
|
+
FEISHU_LIVE_TEST=1 bun run test tests/feishu-live/feishu-card-form-error.test.ts
|
|
145
|
+
FEISHU_LIVE_TEST=1 bun run test tests/feishu-live/feishu-reaction-emoji-live.test.ts
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
## 文档
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { SessionReaderRegistry, SessionStore } from '../session/index.js';
|
|
|
4
4
|
import type { AppConfig } from '../config/index.js';
|
|
5
5
|
import { type RunCardChannel } from '../card/run-card-session.js';
|
|
6
6
|
import type { RunTerminal } from '../card/run-state.js';
|
|
7
|
-
import { type AgentKind } from '../runner/types.js';
|
|
7
|
+
import { type AgentKind, type StopOutcome } from '../runner/types.js';
|
|
8
8
|
import { QueueManager, type QueuedTask, type EnqueueOptions, type AgentBinding, type ExecutingInfo } from './queue-manager.js';
|
|
9
9
|
import type { InboundMediaPayload } from '../connector/index.js';
|
|
10
10
|
import type { MediaOutcome } from '../inbound/turn.js';
|
|
@@ -46,6 +46,13 @@ export interface CommandResult {
|
|
|
46
46
|
markdown?: string;
|
|
47
47
|
card?: object;
|
|
48
48
|
}
|
|
49
|
+
/** Detailed result of one interrupt, for callers that must not over-promise. */
|
|
50
|
+
export interface InterruptOutcome {
|
|
51
|
+
stopped: boolean;
|
|
52
|
+
agentKind?: AgentKind;
|
|
53
|
+
/** Present for DSH: what the remote stop actually achieved. */
|
|
54
|
+
stop?: StopOutcome;
|
|
55
|
+
}
|
|
49
56
|
export interface ActiveRunSnapshot {
|
|
50
57
|
runId: string;
|
|
51
58
|
userId: string;
|
|
@@ -151,6 +158,16 @@ export declare class Bridge {
|
|
|
151
158
|
* 与 session 是否还在、能否压缩无关(2026-10-08 线上缺陷)。
|
|
152
159
|
*/
|
|
153
160
|
private compactableRuns;
|
|
161
|
+
/**
|
|
162
|
+
* 终态卡片的渲染快照(runId → state + options),供「详情」在 run 结束后
|
|
163
|
+
* 仍能二次渲染。
|
|
164
|
+
*
|
|
165
|
+
* run 结束时 activeRuns 被清空,而卡片上的「详情」按钮还活着;没有快照,
|
|
166
|
+
* 点终态卡上的「详情」只能得到一句「已失效」。只保留最近若干条:单条快照
|
|
167
|
+
* 受 MAX_BLOCKS 约束(几十 KB 以内),用户只会点刚收到的卡。
|
|
168
|
+
*/
|
|
169
|
+
private runCardSnapshots;
|
|
170
|
+
private static readonly RUN_CARD_SNAPSHOT_MAX;
|
|
154
171
|
/**
|
|
155
172
|
* Active `!` bash runs, keyed by runId (NOT cwd). Bash runs bypass the serial
|
|
156
173
|
* queue and may run in parallel with claude runs / other bash runs in the same
|
|
@@ -318,6 +335,20 @@ export declare class Bridge {
|
|
|
318
335
|
* omitted, all workspaces are candidates (used by /stop, /t, and card stop button which don't know which cwd to target). */
|
|
319
336
|
cwd?: string;
|
|
320
337
|
}): Promise<boolean>;
|
|
338
|
+
/**
|
|
339
|
+
* Same as {@link interruptCurrentRun}, plus what the runner's stop actually
|
|
340
|
+
* achieved. DSH returns a {@link StopOutcome} (withdrawn / cancelled /
|
|
341
|
+
* unconfirmed); process runners and bash return none.
|
|
342
|
+
*
|
|
343
|
+
* Callers use `stop.status === 'unconfirmed'` to avoid promising "stopped" or
|
|
344
|
+
* "executing immediately" when a turn may still be running in the host.
|
|
345
|
+
*/
|
|
346
|
+
interruptCurrentRunDetailed(input: {
|
|
347
|
+
userId: string;
|
|
348
|
+
chatId: string;
|
|
349
|
+
runId?: string;
|
|
350
|
+
cwd?: string;
|
|
351
|
+
}): Promise<InterruptOutcome>;
|
|
321
352
|
/** Send a result message to the user (§7). Returns true on success, false on failure. */
|
|
322
353
|
sendResult(result: CommandResult, ctx: CommandContext): Promise<boolean>;
|
|
323
354
|
/**
|
|
@@ -345,6 +376,22 @@ export declare class Bridge {
|
|
|
345
376
|
* card id from their set on `false`.
|
|
346
377
|
*/
|
|
347
378
|
patchCard(messageId: string, card: object): Promise<boolean>;
|
|
379
|
+
/**
|
|
380
|
+
* 终态快照的写入点:finalize 时把当前 state + 渲染选项留一份。
|
|
381
|
+
*
|
|
382
|
+
* 不放在 finish() 里:bridge 才知道 hasRunCompact 等渲染选项,且 eviction
|
|
383
|
+
* 与 activeRuns 生命周期在同一个地方收口。
|
|
384
|
+
*/
|
|
385
|
+
private rememberRunCardSnapshot;
|
|
386
|
+
/**
|
|
387
|
+
* 停止终态卡上的一个后台任务。
|
|
388
|
+
*
|
|
389
|
+
* 后台任务的生命周期跨 run(这就是它需要单独入口的原因),所以解析路径是
|
|
390
|
+
* 终态快照(runId → sessionId/cwd/agentKind)+ 该工作区的 runner 实例,
|
|
391
|
+
* 而不是 activeRuns。成功后同步摘掉 state 里的任务并就地重渲染,否则按钮
|
|
392
|
+
* 会留在卡片上而任务早已结束。
|
|
393
|
+
*/
|
|
394
|
+
killBackgroundJob(runId: string, jobId: string, ctx: CommandContext): Promise<boolean>;
|
|
348
395
|
/** Send a file to the user via Feishu. */
|
|
349
396
|
sendFile(filePath: string, ctx: CommandContext): Promise<void>;
|
|
350
397
|
/**
|
package/dist/bridge/index.js
CHANGED
|
@@ -150,6 +150,16 @@ export class Bridge {
|
|
|
150
150
|
* 与 session 是否还在、能否压缩无关(2026-10-08 线上缺陷)。
|
|
151
151
|
*/
|
|
152
152
|
compactableRuns = new Map();
|
|
153
|
+
/**
|
|
154
|
+
* 终态卡片的渲染快照(runId → state + options),供「详情」在 run 结束后
|
|
155
|
+
* 仍能二次渲染。
|
|
156
|
+
*
|
|
157
|
+
* run 结束时 activeRuns 被清空,而卡片上的「详情」按钮还活着;没有快照,
|
|
158
|
+
* 点终态卡上的「详情」只能得到一句「已失效」。只保留最近若干条:单条快照
|
|
159
|
+
* 受 MAX_BLOCKS 约束(几十 KB 以内),用户只会点刚收到的卡。
|
|
160
|
+
*/
|
|
161
|
+
runCardSnapshots = new Map();
|
|
162
|
+
static RUN_CARD_SNAPSHOT_MAX = 8;
|
|
153
163
|
/**
|
|
154
164
|
* Active `!` bash runs, keyed by runId (NOT cwd). Bash runs bypass the serial
|
|
155
165
|
* queue and may run in parallel with claude runs / other bash runs in the same
|
|
@@ -575,6 +585,17 @@ export class Bridge {
|
|
|
575
585
|
return result;
|
|
576
586
|
}
|
|
577
587
|
async interruptCurrentRun(input) {
|
|
588
|
+
return (await this.interruptCurrentRunDetailed(input)).stopped;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Same as {@link interruptCurrentRun}, plus what the runner's stop actually
|
|
592
|
+
* achieved. DSH returns a {@link StopOutcome} (withdrawn / cancelled /
|
|
593
|
+
* unconfirmed); process runners and bash return none.
|
|
594
|
+
*
|
|
595
|
+
* Callers use `stop.status === 'unconfirmed'` to avoid promising "stopped" or
|
|
596
|
+
* "executing immediately" when a turn may still be running in the host.
|
|
597
|
+
*/
|
|
598
|
+
async interruptCurrentRunDetailed(input) {
|
|
578
599
|
// Find the active claude run matching this user/chat
|
|
579
600
|
for (const [cwd, active] of this.activeRuns) {
|
|
580
601
|
if (input.cwd !== undefined && cwd !== input.cwd)
|
|
@@ -595,13 +616,18 @@ export class Bridge {
|
|
|
595
616
|
// unconditional reset would then zero the successor's count and mark
|
|
596
617
|
// its slot interrupted, hiding it from queue cards.
|
|
597
618
|
const stoppedSlot = this.queueManager.getExecutingSlot(cwd);
|
|
598
|
-
await Promise.allSettled([
|
|
619
|
+
const [, stopResult] = await Promise.allSettled([
|
|
599
620
|
// 用户主动中断(/stop、stop 卡按钮、queue.immediate)显式打标
|
|
600
621
|
// interruptedReason,与审批超时自动取消(approval_timeout)、审批
|
|
601
622
|
// 卡取消(approval_cancelled)在终态数据上可区分(方案 A)。
|
|
602
623
|
active.session.finish('interrupted', { interruptedReason: 'user_stop' }),
|
|
603
624
|
runner.stop({ immediate: true }),
|
|
604
625
|
]);
|
|
626
|
+
const stop = stopResult.status === 'fulfilled' &&
|
|
627
|
+
stopResult.value !== null &&
|
|
628
|
+
typeof stopResult.value === 'object'
|
|
629
|
+
? stopResult.value
|
|
630
|
+
: undefined;
|
|
605
631
|
// The chain may have advanced while the stop was in flight: a new run
|
|
606
632
|
// (from a task that began during the stop window) may now occupy this
|
|
607
633
|
// cwd. Only delete the entry we actually stopped.
|
|
@@ -641,7 +667,7 @@ export class Bridge {
|
|
|
641
667
|
this.queueManager.resetExecutingCount(cwd, stoppedSlot);
|
|
642
668
|
}
|
|
643
669
|
getLogger().info(`[lark-remote] interrupt stop done runId=${active.runId}`);
|
|
644
|
-
return true;
|
|
670
|
+
return { stopped: true, agentKind: active.agentKind, ...(stop ? { stop } : {}) };
|
|
645
671
|
}
|
|
646
672
|
}
|
|
647
673
|
// Then check active bash runs (! commands). These bypass the serial queue
|
|
@@ -658,11 +684,11 @@ export class Bridge {
|
|
|
658
684
|
await b.cardSession.finish('interrupted', { exitCode: -1 });
|
|
659
685
|
this.activeBashRuns.delete(runId);
|
|
660
686
|
getLogger().info(`[lark-remote] interrupt bash stop done runId=${runId}`);
|
|
661
|
-
return true;
|
|
687
|
+
return { stopped: true };
|
|
662
688
|
}
|
|
663
689
|
}
|
|
664
690
|
getLogger().debug(`[lark-remote] interrupt miss userId=${input.userId}`);
|
|
665
|
-
return false;
|
|
691
|
+
return { stopped: false };
|
|
666
692
|
}
|
|
667
693
|
/** Send a result message to the user (§7). Returns true on success, false on failure. */
|
|
668
694
|
async sendResult(result, ctx) {
|
|
@@ -756,6 +782,47 @@ export class Bridge {
|
|
|
756
782
|
return false;
|
|
757
783
|
}
|
|
758
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* 终态快照的写入点:finalize 时把当前 state + 渲染选项留一份。
|
|
787
|
+
*
|
|
788
|
+
* 不放在 finish() 里:bridge 才知道 hasRunCompact 等渲染选项,且 eviction
|
|
789
|
+
* 与 activeRuns 生命周期在同一个地方收口。
|
|
790
|
+
*/
|
|
791
|
+
rememberRunCardSnapshot(runId, state, options, cwd, agentKind) {
|
|
792
|
+
this.runCardSnapshots.set(runId, { state, options, cwd, agentKind });
|
|
793
|
+
while (this.runCardSnapshots.size > Bridge.RUN_CARD_SNAPSHOT_MAX) {
|
|
794
|
+
const oldest = this.runCardSnapshots.keys().next().value;
|
|
795
|
+
if (oldest === undefined)
|
|
796
|
+
break;
|
|
797
|
+
this.runCardSnapshots.delete(oldest);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* 停止终态卡上的一个后台任务。
|
|
802
|
+
*
|
|
803
|
+
* 后台任务的生命周期跨 run(这就是它需要单独入口的原因),所以解析路径是
|
|
804
|
+
* 终态快照(runId → sessionId/cwd/agentKind)+ 该工作区的 runner 实例,
|
|
805
|
+
* 而不是 activeRuns。成功后同步摘掉 state 里的任务并就地重渲染,否则按钮
|
|
806
|
+
* 会留在卡片上而任务早已结束。
|
|
807
|
+
*/
|
|
808
|
+
async killBackgroundJob(runId, jobId, ctx) {
|
|
809
|
+
const snapshot = this.runCardSnapshots.get(runId);
|
|
810
|
+
const sessionId = snapshot?.state.sessionId;
|
|
811
|
+
if (!snapshot || !sessionId)
|
|
812
|
+
return false;
|
|
813
|
+
const runner = this.getRunner(snapshot.cwd, snapshot.agentKind);
|
|
814
|
+
if (typeof runner.killJob !== 'function')
|
|
815
|
+
return false;
|
|
816
|
+
const killed = await runner.killJob(sessionId, jobId);
|
|
817
|
+
if (!killed)
|
|
818
|
+
return false;
|
|
819
|
+
snapshot.state = {
|
|
820
|
+
...snapshot.state,
|
|
821
|
+
backgroundJobs: (snapshot.state.backgroundJobs ?? []).filter((job) => job.id !== jobId),
|
|
822
|
+
};
|
|
823
|
+
await this.updateCardInPlace(renderRunCard(snapshot.state, snapshot.options), ctx);
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
759
826
|
/** Send a file to the user via Feishu. */
|
|
760
827
|
async sendFile(filePath, ctx) {
|
|
761
828
|
try {
|
|
@@ -1222,7 +1289,10 @@ export class Bridge {
|
|
|
1222
1289
|
await r.respondApproval(requestId, response);
|
|
1223
1290
|
}
|
|
1224
1291
|
},
|
|
1225
|
-
interruptTurn: () =>
|
|
1292
|
+
interruptTurn: async () => {
|
|
1293
|
+
// DSH may return a StopOutcome; the coordinator only awaits.
|
|
1294
|
+
await runner.stop();
|
|
1295
|
+
},
|
|
1226
1296
|
pushToCard: async (events) => {
|
|
1227
1297
|
for (const ev of events)
|
|
1228
1298
|
await cardSession.push(ev);
|
|
@@ -1409,17 +1479,19 @@ export class Bridge {
|
|
|
1409
1479
|
try {
|
|
1410
1480
|
getLogger().debug(`[lark-remote] finally settle runId=${runId} state.terminal=${cardSession.currentState.terminal}`);
|
|
1411
1481
|
const settleResult = await cardSession.settle();
|
|
1482
|
+
// 卡片收尾后仍要能点「详情」:把终态 state 与当时的渲染选项留一份。
|
|
1483
|
+
// 与 fallback 卡共用同一份 options(能力门控一致,展开后按钮语义不变)。
|
|
1484
|
+
const finalRenderOptions = {
|
|
1485
|
+
agentKind: activeRun.agentKind,
|
|
1486
|
+
// 与 createRunSession 的 renderOptions 一致:fallback 卡也要按
|
|
1487
|
+
// runner 能力门控 Compact 按钮(缺省 undefined = 显示按钮,
|
|
1488
|
+
// DSH 等无 runCompact 的 agent 会渲染出点了报错的死按钮)。
|
|
1489
|
+
compactSupported: this.hasRunCompact(cwd, activeRun.agentKind),
|
|
1490
|
+
};
|
|
1491
|
+
this.rememberRunCardSnapshot(runId, cardSession.currentState, finalRenderOptions, cwd, activeRun.agentKind);
|
|
1412
1492
|
let runCardSent = false;
|
|
1413
1493
|
if (settleResult === 'unsent') {
|
|
1414
|
-
const sent = await this.sendResult({
|
|
1415
|
-
card: renderRunCard(cardSession.currentState, {
|
|
1416
|
-
agentKind: activeRun.agentKind,
|
|
1417
|
-
// 与 createRunSession 的 renderOptions 一致:fallback 卡也要按
|
|
1418
|
-
// runner 能力门控 Compact 按钮(缺省 undefined = 显示按钮,
|
|
1419
|
-
// DSH 等无 runCompact 的 agent 会渲染出点了报错的死按钮)。
|
|
1420
|
-
compactSupported: this.hasRunCompact(cwd, activeRun.agentKind),
|
|
1421
|
-
}),
|
|
1422
|
-
}, ctx);
|
|
1494
|
+
const sent = await this.sendResult({ card: renderRunCard(cardSession.currentState, finalRenderOptions) }, ctx);
|
|
1423
1495
|
runCardSent = sent;
|
|
1424
1496
|
}
|
|
1425
1497
|
else {
|
|
@@ -6,6 +6,43 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Docs: https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukz/feishu-cards/card-components/containers/collapsible-panel
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* 飞书单卡「元素」总量上限(实测)。
|
|
11
|
+
*
|
|
12
|
+
* 2026-10-19 用真实 API 在测试会话探测(`tests/feishu-live/feishu-card-element-total-limit.test.ts`
|
|
13
|
+
* 与 `feishu-workspace-card-limit` / `feishu-order-active-card-limit`):
|
|
14
|
+
*
|
|
15
|
+
* | 形状 | 结果 | elements 计数 |
|
|
16
|
+
* | --- | --- | --- |
|
|
17
|
+
* | 顶层平铺 99 个 div | ✅ | 99 |
|
|
18
|
+
* | 顶层平铺 100 个 div | ❌ 11310 | 100 |
|
|
19
|
+
* | 单列 column_set 内嵌 98 个 div | ✅ | 99 |
|
|
20
|
+
* | 单列 column_set 内嵌 99 个 div | ❌ | 100 |
|
|
21
|
+
* | `/order` 旧结构 20 行 | ❌ | 103 |
|
|
22
|
+
* | `/ws` 旧结构 20 行 | ❌ | 101 |
|
|
23
|
+
* | `/ws` 分页 15 行(整卡 227 个 JSON 对象) | ✅ | 80 |
|
|
24
|
+
*
|
|
25
|
+
* 判据是**递归统计 `elements` 数组成员**(不含 `columns`)**≤ 99**:所有实测的
|
|
26
|
+
* 成功/失败点都被它分开,而"整卡 JSON 对象数"分不开(`/ws` 分页卡 227 个对象
|
|
27
|
+
* 能发出去,平铺 100 个 div 只有 205 个对象却失败)。
|
|
28
|
+
*
|
|
29
|
+
* 另一个独立维度:单个 `column_set` 的列数(实测 66 列通过、67 列失败)。本仓库
|
|
30
|
+
* 所有卡片的列数都在 3 以内,用不到这个上限,仅在此记录以免再次误判。
|
|
31
|
+
*
|
|
32
|
+
* **旧结论勘误**:仓库曾把 `230025` 记为「elements 过多」。本次探测中它一次都
|
|
33
|
+
* 没有出现;元素超限统一是 `230099 / ErrCode: 11310 element exceeds the limit`。
|
|
34
|
+
*
|
|
35
|
+
* `MAX_CARD_ELEMENTS` 取 90(相对实测 99 留 10% 余量),用于渲染前的保守裁剪。
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_CARD_ELEMENTS = 90;
|
|
38
|
+
/**
|
|
39
|
+
* 卡片递归的 `elements` 数组成员数(`columns` 不计数)——与飞书实测边界对齐。
|
|
40
|
+
*
|
|
41
|
+
* 注意与"JSON 对象总数"不同:`column_set` 的 `column` 对象不计入,button 的
|
|
42
|
+
* `behaviors`/`value` 也不计入。用它做预算才既不误伤能发出去的卡片,也不放过
|
|
43
|
+
* 真正超限的卡片。
|
|
44
|
+
*/
|
|
45
|
+
export declare function countCardElements(card: unknown): number;
|
|
9
46
|
interface CardBudgetOptions {
|
|
10
47
|
/** 最大事件面板数量(保留最新的) */
|
|
11
48
|
maxEventPanels?: number;
|
package/dist/card/card-budget.js
CHANGED
|
@@ -1,4 +1,77 @@
|
|
|
1
1
|
import { truncateUtf8, dropOldestMarkdownTables, countMarkdownTables, FEISHU_MAX_TABLES, CARD_BUDGET_BYTES, } from './text-truncate.js';
|
|
2
|
+
/**
|
|
3
|
+
* 静态卡片体积保护
|
|
4
|
+
*
|
|
5
|
+
* 用于:auto-resume, /active, completion notification 等静态卡片
|
|
6
|
+
* 不适用于:run-renderer 的流式卡片(已有独立保护)
|
|
7
|
+
*
|
|
8
|
+
* Docs: https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukz/feishu-cards/card-components/containers/collapsible-panel
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* 飞书单卡「元素」总量上限(实测)。
|
|
12
|
+
*
|
|
13
|
+
* 2026-10-19 用真实 API 在测试会话探测(`tests/feishu-live/feishu-card-element-total-limit.test.ts`
|
|
14
|
+
* 与 `feishu-workspace-card-limit` / `feishu-order-active-card-limit`):
|
|
15
|
+
*
|
|
16
|
+
* | 形状 | 结果 | elements 计数 |
|
|
17
|
+
* | --- | --- | --- |
|
|
18
|
+
* | 顶层平铺 99 个 div | ✅ | 99 |
|
|
19
|
+
* | 顶层平铺 100 个 div | ❌ 11310 | 100 |
|
|
20
|
+
* | 单列 column_set 内嵌 98 个 div | ✅ | 99 |
|
|
21
|
+
* | 单列 column_set 内嵌 99 个 div | ❌ | 100 |
|
|
22
|
+
* | `/order` 旧结构 20 行 | ❌ | 103 |
|
|
23
|
+
* | `/ws` 旧结构 20 行 | ❌ | 101 |
|
|
24
|
+
* | `/ws` 分页 15 行(整卡 227 个 JSON 对象) | ✅ | 80 |
|
|
25
|
+
*
|
|
26
|
+
* 判据是**递归统计 `elements` 数组成员**(不含 `columns`)**≤ 99**:所有实测的
|
|
27
|
+
* 成功/失败点都被它分开,而"整卡 JSON 对象数"分不开(`/ws` 分页卡 227 个对象
|
|
28
|
+
* 能发出去,平铺 100 个 div 只有 205 个对象却失败)。
|
|
29
|
+
*
|
|
30
|
+
* 另一个独立维度:单个 `column_set` 的列数(实测 66 列通过、67 列失败)。本仓库
|
|
31
|
+
* 所有卡片的列数都在 3 以内,用不到这个上限,仅在此记录以免再次误判。
|
|
32
|
+
*
|
|
33
|
+
* **旧结论勘误**:仓库曾把 `230025` 记为「elements 过多」。本次探测中它一次都
|
|
34
|
+
* 没有出现;元素超限统一是 `230099 / ErrCode: 11310 element exceeds the limit`。
|
|
35
|
+
*
|
|
36
|
+
* `MAX_CARD_ELEMENTS` 取 90(相对实测 99 留 10% 余量),用于渲染前的保守裁剪。
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_CARD_ELEMENTS = 90;
|
|
39
|
+
/**
|
|
40
|
+
* 卡片递归的 `elements` 数组成员数(`columns` 不计数)——与飞书实测边界对齐。
|
|
41
|
+
*
|
|
42
|
+
* 注意与"JSON 对象总数"不同:`column_set` 的 `column` 对象不计入,button 的
|
|
43
|
+
* `behaviors`/`value` 也不计入。用它做预算才既不误伤能发出去的卡片,也不放过
|
|
44
|
+
* 真正超限的卡片。
|
|
45
|
+
*/
|
|
46
|
+
export function countCardElements(card) {
|
|
47
|
+
let total = 0;
|
|
48
|
+
const walk = (value) => {
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
for (const item of value)
|
|
51
|
+
walk(item);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!value || typeof value !== 'object')
|
|
55
|
+
return;
|
|
56
|
+
const rec = value;
|
|
57
|
+
if (Array.isArray(rec.elements)) {
|
|
58
|
+
total += rec.elements.length;
|
|
59
|
+
walk(rec.elements);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(rec.columns)) {
|
|
62
|
+
walk(rec.columns);
|
|
63
|
+
}
|
|
64
|
+
// 继续下潜其余字段(`body`、`header` 等):卡片根对象本身没有 elements,
|
|
65
|
+
// 不遍历字段就永远数到 0(曾因此让整条元素预算静默失效)。
|
|
66
|
+
for (const [key, child] of Object.entries(value)) {
|
|
67
|
+
if (key === 'elements' || key === 'columns')
|
|
68
|
+
continue;
|
|
69
|
+
walk(child);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
walk(card);
|
|
73
|
+
return total;
|
|
74
|
+
}
|
|
2
75
|
/**
|
|
3
76
|
* 静态卡片体积保护
|
|
4
77
|
*
|
|
@@ -13,11 +86,12 @@ export function enforceCardBudget(card, opts = {}) {
|
|
|
13
86
|
const cardStr = JSON.stringify(card);
|
|
14
87
|
const bytesBefore = Buffer.byteLength(cardStr, 'utf8');
|
|
15
88
|
const parsed = JSON.parse(cardStr);
|
|
89
|
+
const elementsBefore = countCardElements(parsed);
|
|
16
90
|
const droppedTables = capCardMarkdownTables(parsed, FEISHU_MAX_TABLES);
|
|
17
91
|
if (droppedTables > 0) {
|
|
18
92
|
const fixedStr = JSON.stringify(parsed);
|
|
19
|
-
// table
|
|
20
|
-
if (Buffer.byteLength(fixedStr, 'utf8')
|
|
93
|
+
// table 截断后可能仍超预算(字节或元素总数),继续走后续阶段
|
|
94
|
+
if (fitsBudget(Buffer.byteLength(fixedStr, 'utf8'), countCardElements(parsed))) {
|
|
21
95
|
return {
|
|
22
96
|
card: parsed,
|
|
23
97
|
wasTruncated: true,
|
|
@@ -26,15 +100,64 @@ export function enforceCardBudget(card, opts = {}) {
|
|
|
26
100
|
bytesAfter: Buffer.byteLength(fixedStr, 'utf8'),
|
|
27
101
|
};
|
|
28
102
|
}
|
|
29
|
-
|
|
30
|
-
return enforceByteBudget(parsed, opts, bytesBefore);
|
|
103
|
+
return enforceElementBudget(parsed, opts, bytesBefore);
|
|
31
104
|
}
|
|
32
|
-
if (bytesBefore
|
|
105
|
+
if (fitsBudget(bytesBefore, elementsBefore)) {
|
|
33
106
|
return { card, wasTruncated: false, bytesBefore, bytesAfter: bytesBefore };
|
|
34
107
|
}
|
|
108
|
+
// 元素总数超限:table 与字节可能都正常,必须先按元素数裁剪。
|
|
109
|
+
if (bytesBefore <= CARD_BUDGET_BYTES && elementsBefore > MAX_CARD_ELEMENTS) {
|
|
110
|
+
return enforceElementBudget(parsed, opts, bytesBefore);
|
|
111
|
+
}
|
|
35
112
|
// 字节超限但 table 不超限
|
|
36
113
|
return enforceByteBudget(JSON.parse(cardStr), opts, bytesBefore);
|
|
37
114
|
}
|
|
115
|
+
/** 同时满足字节与元素总数两个预算。 */
|
|
116
|
+
function fitsBudget(bytes, elements) {
|
|
117
|
+
return bytes <= CARD_BUDGET_BYTES && elements <= MAX_CARD_ELEMENTS;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 元素总数超限时的裁剪:先丢最老的面板,再交给字节路径继续降级。
|
|
121
|
+
*
|
|
122
|
+
* 与字节路径分开是因为成因正交:字节超限来自内容长度(截断内容即可),元素
|
|
123
|
+
* 超限来自元素个数(只能删元素)。两条路径共用 `getPanelIndices`。
|
|
124
|
+
*/
|
|
125
|
+
function enforceElementBudget(card, opts = {}, bytesBefore) {
|
|
126
|
+
const reasons = [];
|
|
127
|
+
const elements = card.body?.elements ?? [];
|
|
128
|
+
const size = () => Buffer.byteLength(JSON.stringify(card), 'utf8');
|
|
129
|
+
const done = (wasTruncated) => ({
|
|
130
|
+
card,
|
|
131
|
+
wasTruncated,
|
|
132
|
+
reason: reasons.join('+') || undefined,
|
|
133
|
+
bytesBefore: bytesBefore ?? size(),
|
|
134
|
+
bytesAfter: size(),
|
|
135
|
+
});
|
|
136
|
+
const dropPanels = (count) => {
|
|
137
|
+
const indices = getPanelIndices(elements)
|
|
138
|
+
.slice(0, count)
|
|
139
|
+
.sort((a, b) => b - a);
|
|
140
|
+
for (const idx of indices)
|
|
141
|
+
elements.splice(idx, 1);
|
|
142
|
+
return indices.length;
|
|
143
|
+
};
|
|
144
|
+
// 阶段1:面板按数量砍(面板是节点大户:一个面板外壳 + 内容)
|
|
145
|
+
const panelCount = getPanelIndices(elements).length;
|
|
146
|
+
if (panelCount > 0) {
|
|
147
|
+
let dropped = 0;
|
|
148
|
+
while (countCardElements(card) > MAX_CARD_ELEMENTS && getPanelIndices(elements).length > 0) {
|
|
149
|
+
dropped += dropPanels(1);
|
|
150
|
+
}
|
|
151
|
+
if (dropped > 0)
|
|
152
|
+
reasons.push('element_budget_panels_dropped');
|
|
153
|
+
}
|
|
154
|
+
if (countCardElements(card) <= MAX_CARD_ELEMENTS && size() <= CARD_BUDGET_BYTES) {
|
|
155
|
+
return done(true);
|
|
156
|
+
}
|
|
157
|
+
// 阶段2:仍超限(既不是面板、或字节同时超)→ 交给字节路径继续降级,
|
|
158
|
+
// 它最终会落到最小卡(节点数固定很小)。
|
|
159
|
+
return enforceByteBudget(card, opts, bytesBefore);
|
|
160
|
+
}
|
|
38
161
|
/**
|
|
39
162
|
* Byte-budget enforcement, extracted so table-limit fix can delegate to it
|
|
40
163
|
* when table truncation alone doesn't bring the card under the byte limit.
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { RunState, ToolEntry } from './run-state.js';
|
|
2
|
+
/**
|
|
3
|
+
* 带「详情」按钮的工具行数上限(从最新往回数)。
|
|
4
|
+
*
|
|
5
|
+
* 按钮行是节点大户:`column_set` + 2 个 `column` + 标题 div/text + button/text/
|
|
6
|
+
* behaviors/value ≈ 10 个 JSON 节点;纯标题行只有 2 个。24 行全带按钮 ≈ 240 节点,
|
|
7
|
+
* 直接撞飞书的元素总数上限(实测 203 通过 / 204 失败,见 card-budget.ts)。
|
|
8
|
+
* 最近 8 行保留按钮(用户最可能展开的就是刚跑完的),更早的行退化成一行标题。
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_TOOL_ROWS_WITH_BUTTON = 8;
|
|
2
11
|
export interface RunCardRenderOptions {
|
|
3
12
|
/**
|
|
4
13
|
* Agent kind for the run card header title (e.g. "Claude · 思考中").
|
|
@@ -39,6 +48,21 @@ type BlockGroup = {
|
|
|
39
48
|
};
|
|
40
49
|
/** 每个块的截断后内容(thinking/plan/text),测量与渲染共用(建议项)。 */
|
|
41
50
|
type GroupContentPrepared = Map<BlockGroup, string>;
|
|
51
|
+
/**
|
|
52
|
+
* 状态行内容:**一行、一个 div、窄屏不换行**。
|
|
53
|
+
*
|
|
54
|
+
* 设计约束(2026-10-19 手机端实测):旧版把「状态标签」和「耗时/指标」放进
|
|
55
|
+
* `column_set` 的 auto + grow 两列,窄屏上 auto 列被挤到只剩两个字宽,
|
|
56
|
+
* "运行中" 被拆成 "运行 / 中"。列式布局的可用宽度取决于对端渲染,服务端无法
|
|
57
|
+
* 保证;所以这里改成单 div,段间用 " · " 分隔——换行只会发生在分隔符处,
|
|
58
|
+
* 而且整行控制在 ~26 个半角宽度内,320px 宽的手机也能放下。
|
|
59
|
+
*
|
|
60
|
+
* 状态标签(运行中/已完成…)不再重复:header 已经写了(`🔧 dsh · 调用工具`
|
|
61
|
+
* / `✅ dsh · 已完成`),终态 summary 还有一次。
|
|
62
|
+
*
|
|
63
|
+
* 导出供窄屏守卫测试断言分段宽度(与 `estimateCardBytes` 同一惯例)。
|
|
64
|
+
*/
|
|
65
|
+
export declare function statusLine(state: RunState): string;
|
|
42
66
|
/** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
|
|
43
67
|
export declare function estimateCardBytes(state: RunState, options?: RunCardRenderOptions, shared?: {
|
|
44
68
|
groups?: BlockGroup[];
|