lark-remote 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +71 -34
  5. package/dist/bridge/approval-coordinator.js +225 -41
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +97 -13
  9. package/dist/bridge/index.js +227 -87
  10. package/dist/bridge/queue-manager.d.ts +2 -1
  11. package/dist/bridge/queue-manager.js +1 -0
  12. package/dist/card/approval-render.js +294 -36
  13. package/dist/card/bash-card-session.d.ts +2 -24
  14. package/dist/card/bash-card-session.js +6 -73
  15. package/dist/card/bash-renderer.js +2 -3
  16. package/dist/card/card-budget.js +29 -75
  17. package/dist/card/card-session.d.ts +48 -0
  18. package/dist/card/card-session.js +86 -0
  19. package/dist/card/card-shared.d.ts +7 -16
  20. package/dist/card/card-shared.js +7 -30
  21. package/dist/card/run-card-session.d.ts +2 -55
  22. package/dist/card/run-card-session.js +6 -104
  23. package/dist/card/run-renderer.d.ts +7 -0
  24. package/dist/card/run-renderer.js +116 -238
  25. package/dist/card/run-state.d.ts +13 -0
  26. package/dist/card/run-state.js +25 -16
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/index.d.ts +112 -24
  40. package/dist/config/index.js +139 -37
  41. package/dist/connector/index.d.ts +69 -2
  42. package/dist/connector/index.js +107 -1
  43. package/dist/index.js +150 -118
  44. package/dist/instance-lock.js +4 -12
  45. package/dist/order/alias-resolve.d.ts +13 -0
  46. package/dist/order/alias-resolve.js +22 -0
  47. package/dist/order/alias-store.d.ts +38 -0
  48. package/dist/order/alias-store.js +106 -0
  49. package/dist/persistence/atomic-write.d.ts +8 -2
  50. package/dist/persistence/atomic-write.js +23 -7
  51. package/dist/restart.js +2 -1
  52. package/dist/router/card-action-payload.d.ts +30 -0
  53. package/dist/router/card-action-payload.js +39 -0
  54. package/dist/router/card-helpers.d.ts +63 -0
  55. package/dist/router/card-helpers.js +146 -0
  56. package/dist/router/config/claude.js +30 -1
  57. package/dist/router/config/codex.js +29 -50
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +5 -4
  61. package/dist/router/config/dsh.d.ts +51 -0
  62. package/dist/router/config/dsh.js +179 -0
  63. package/dist/router/config/index.d.ts +12 -0
  64. package/dist/router/config/index.js +28 -1
  65. package/dist/router/config/kimi.d.ts +4 -1
  66. package/dist/router/config/kimi.js +27 -0
  67. package/dist/router/config/opencode.js +18 -5
  68. package/dist/router/config/pi.js +4 -5
  69. package/dist/router/config/types.d.ts +17 -2
  70. package/dist/router/index.d.ts +76 -22
  71. package/dist/router/index.js +479 -548
  72. package/dist/router/utils.d.ts +2 -0
  73. package/dist/router/utils.js +16 -2
  74. package/dist/runner/agent-choices-common.d.ts +16 -0
  75. package/dist/runner/agent-choices-common.js +34 -0
  76. package/dist/runner/claude/runner.d.ts +47 -10
  77. package/dist/runner/claude/runner.js +117 -46
  78. package/dist/runner/claude/session.d.ts +166 -0
  79. package/dist/runner/claude/session.js +768 -0
  80. package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
  81. package/dist/runner/codex/app-server/protocol-types.js +2 -13
  82. package/dist/runner/codex/app-server/runner.d.ts +52 -59
  83. package/dist/runner/codex/app-server/runner.js +216 -338
  84. package/dist/runner/codex/app-server/translator.d.ts +8 -21
  85. package/dist/runner/codex/app-server/translator.js +45 -18
  86. package/dist/runner/codex/index.d.ts +0 -2
  87. package/dist/runner/codex/index.js +0 -2
  88. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  89. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  90. package/dist/runner/common/acp/protocol-types.d.ts +271 -0
  91. package/dist/runner/common/acp/protocol-types.js +77 -0
  92. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  93. package/dist/runner/common/connection-based-runner.js +261 -0
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  95. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  96. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  97. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  98. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  99. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  100. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  101. package/dist/runner/common/jsonrpc/types.js +9 -0
  102. package/dist/runner/common/process-stopper.d.ts +2 -1
  103. package/dist/runner/common/process-stopper.js +1 -0
  104. package/dist/runner/common/runner-utils.d.ts +6 -13
  105. package/dist/runner/common/runner-utils.js +6 -47
  106. package/dist/runner/common/spawning-runner.d.ts +24 -70
  107. package/dist/runner/common/spawning-runner.js +98 -152
  108. package/dist/runner/dsh/client.d.ts +95 -0
  109. package/dist/runner/dsh/client.js +418 -0
  110. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  111. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  112. package/dist/runner/dsh/index.d.ts +3 -0
  113. package/dist/runner/dsh/index.js +4 -0
  114. package/dist/runner/dsh/runner.d.ts +57 -0
  115. package/dist/runner/dsh/runner.js +191 -0
  116. package/dist/runner/dsh/translator.d.ts +28 -0
  117. package/dist/runner/dsh/translator.js +98 -0
  118. package/dist/runner/dsh/types.d.ts +155 -0
  119. package/dist/runner/dsh/types.js +14 -0
  120. package/dist/runner/index.d.ts +5 -4
  121. package/dist/runner/index.js +5 -4
  122. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  123. package/dist/runner/kimi/acp/runner.js +516 -0
  124. package/dist/runner/kimi/acp/translator.d.ts +138 -0
  125. package/dist/runner/kimi/acp/translator.js +441 -0
  126. package/dist/runner/kimi/index.d.ts +1 -1
  127. package/dist/runner/kimi/index.js +1 -1
  128. package/dist/runner/opencode/acp/index.d.ts +1 -0
  129. package/dist/runner/opencode/acp/index.js +2 -0
  130. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  131. package/dist/runner/opencode/acp/runner.js +429 -0
  132. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  133. package/dist/runner/opencode/acp/translator.js +345 -0
  134. package/dist/runner/opencode/index.d.ts +1 -1
  135. package/dist/runner/opencode/index.js +1 -1
  136. package/dist/runner/pi/index.d.ts +1 -1
  137. package/dist/runner/pi/index.js +1 -1
  138. package/dist/runner/pi/rpc/client.d.ts +52 -0
  139. package/dist/runner/pi/rpc/client.js +114 -0
  140. package/dist/runner/pi/rpc/index.d.ts +4 -0
  141. package/dist/runner/pi/rpc/index.js +4 -0
  142. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  143. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  144. package/dist/runner/pi/rpc/runner.d.ts +74 -0
  145. package/dist/runner/pi/rpc/runner.js +190 -0
  146. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  147. package/dist/runner/pi/rpc/translator.js +258 -0
  148. package/dist/runner/probe.js +6 -1
  149. package/dist/runner/question-common.d.ts +26 -0
  150. package/dist/runner/question-common.js +42 -0
  151. package/dist/runner/registry.d.ts +7 -8
  152. package/dist/runner/registry.js +3 -4
  153. package/dist/runner/resolve-agent-choices.js +18 -59
  154. package/dist/runner/sync-agent-choices.js +16 -67
  155. package/dist/runner/types.d.ts +97 -14
  156. package/dist/session/claude/session-index.js +2 -3
  157. package/dist/session/claude/session-reader.js +3 -6
  158. package/dist/session/claude/sessions.js +43 -28
  159. package/dist/session/codex/rollout-reader.d.ts +2 -2
  160. package/dist/session/codex/rollout-reader.js +14 -17
  161. package/dist/session/codex/sessions.d.ts +1 -1
  162. package/dist/session/codex/sessions.js +2 -2
  163. package/dist/session/common/content-blocks.d.ts +7 -0
  164. package/dist/session/common/content-blocks.js +13 -9
  165. package/dist/session/common/jsonl.d.ts +8 -25
  166. package/dist/session/common/jsonl.js +126 -261
  167. package/dist/session/common/pagination.d.ts +28 -0
  168. package/dist/session/common/pagination.js +33 -0
  169. package/dist/session/dsh/index.d.ts +1 -0
  170. package/dist/session/dsh/index.js +2 -0
  171. package/dist/session/dsh/sessions.d.ts +39 -0
  172. package/dist/session/dsh/sessions.js +237 -0
  173. package/dist/session/index.d.ts +1 -0
  174. package/dist/session/index.js +1 -0
  175. package/dist/session/kimi/sessions.d.ts +34 -1
  176. package/dist/session/kimi/sessions.js +208 -98
  177. package/dist/session/opencode/sessions.d.ts +17 -3
  178. package/dist/session/opencode/sessions.js +29 -29
  179. package/dist/session/pi/sessions.d.ts +5 -1
  180. package/dist/session/pi/sessions.js +12 -16
  181. package/dist/session/session-store.d.ts +11 -1
  182. package/dist/session/session-store.js +75 -178
  183. package/dist/update/index.d.ts +2 -2
  184. package/dist/update/index.js +2 -2
  185. package/dist/update/install.d.ts +20 -2
  186. package/dist/update/install.js +51 -7
  187. package/dist/workspace/index.d.ts +4 -0
  188. package/dist/workspace/index.js +8 -1
  189. package/package.json +2 -1
  190. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  191. package/dist/runner/codex/argv.d.ts +0 -33
  192. package/dist/runner/codex/argv.js +0 -40
  193. package/dist/runner/codex/factory.d.ts +0 -29
  194. package/dist/runner/codex/factory.js +0 -39
  195. package/dist/runner/codex/jsonl.d.ts +0 -37
  196. package/dist/runner/codex/jsonl.js +0 -221
  197. package/dist/runner/codex/runner.d.ts +0 -66
  198. package/dist/runner/codex/runner.js +0 -129
  199. package/dist/runner/common/exec-translator.d.ts +0 -73
  200. package/dist/runner/common/exec-translator.js +0 -105
  201. package/dist/runner/kimi/jsonl.d.ts +0 -28
  202. package/dist/runner/kimi/jsonl.js +0 -143
  203. package/dist/runner/kimi/runner.d.ts +0 -49
  204. package/dist/runner/kimi/runner.js +0 -105
  205. package/dist/runner/opencode/argv.d.ts +0 -32
  206. package/dist/runner/opencode/argv.js +0 -35
  207. package/dist/runner/opencode/jsonl.d.ts +0 -68
  208. package/dist/runner/opencode/jsonl.js +0 -295
  209. package/dist/runner/opencode/runner.d.ts +0 -61
  210. package/dist/runner/opencode/runner.js +0 -93
  211. package/dist/runner/pi/jsonl.d.ts +0 -128
  212. package/dist/runner/pi/jsonl.js +0 -249
  213. package/dist/runner/pi/runner.d.ts +0 -36
  214. package/dist/runner/pi/runner.js +0 -76
  215. package/dist/session/common/catch-up.d.ts +0 -12
  216. package/dist/session/common/catch-up.js +0 -31
package/CHANGELOG.md CHANGED
@@ -5,6 +5,54 @@ 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.8] - 2026-08-19
9
+
10
+ ### 新增
11
+
12
+ - DSH(DeepSeek Harness)Agent 接入:纯 HTTP 连接 DSH Web Host(无本地子进程),支持会话创建/续跑、`selectModel` 模型对齐、SSE mux 事件订阅、会话历史读取
13
+ - `/config` DSH 配置卡:Host 地址 / 会话 preset / 模型 / 推理强度,模型与预设目录预取动态加载(失败回退固定兜底清单)
14
+ - AskUserQuestion 公共契约(`question-common`):标准提问审批事件工厂 + 按序答案映射,claude / codex / kimi / pi 统一接入
15
+ - Codex AskUserQuestion:支持自由文本题、补充说明(user_note)、`autoResolutionMs` 透传为单请求审批超时
16
+ - Kimi elicitation 表单回编 + request_permission 兜底桥
17
+ - 工具权限审批卡(`kind === 'tool'`):ExitPlanMode 等非命令工具展示工具名 + 用途说明,计划审批语义
18
+ - 提问卡单选/多选图标区分(⚪/🔵 vs ⬜/☑️)、自由文本题输入框、自定义答案回显
19
+
20
+ ### 变更
21
+
22
+ - claude session usage 扫描对齐 codex 语义:非累计字段为末轮(本 run)scope,累计走 `cumulative*` 字段
23
+ - Compact 按钮能力门控放开到所有 `runCompact` 能力 runner(claude / codex / kimi / opencode / pi)
24
+ - claude `/config` 权限模式下拉排除 `manual`(`default` 的别名,避免等价重复项)
25
+ - workspace 保存即触达:`save` 内部调用 `touch`,新保存的工作区排在列表最前
26
+ - DSH 可用性探测跳过二进制检查(HTTP-only Agent)
27
+
28
+ ### 修复
29
+
30
+ - claude session usage 扫描:修复第三方网关(如 DeepSeek)将零 usage 写在占位行、真实 usage 只在末行导致的 per-run 统计全 0(改为按 message id 逐字段 max 聚合)
31
+ - 审批答案重复投递(重复 nonce 二次点击)改为中性提示,不再误报"提交失败"
32
+ - usage 统计一致性护栏:本 run 超出累计时显式标记 `⚠️ 累计异常`(只标记不修正,便于排查数据源问题)
33
+
34
+ ## [0.1.7] - 2026-08-18
35
+
36
+ ### 新增
37
+
38
+ - pi RPC 模式 runner:通过持久连接复用共享 `ConnectionManager`,支持审批与上下文压缩
39
+ - kimi / opencode ACP 模式 runner:纯 ACP 持久连接,支持审批 + Compact
40
+ - 共享 ACP / JSON-RPC 连接层(`runner/common/acp`、`runner/common/jsonrpc`):统一 codex / kimi / opencode / pi 的连接管理
41
+ - 入站图片/文件自动落盘:往飞书私聊发图片/文件自动保存到 `.lark-remote-temp/`,可直接让 agent 处理
42
+ - `/order alias`:注册快捷别名,输入 `$name` 即展开
43
+ - 会话分页/截断共享辅助模块(`session/common/pagination`)
44
+ - Codex 命令审批卡片示例图
45
+
46
+ ### 变更
47
+
48
+ - codex runner 统一走 app-server 审批模式;移除 codex/kimi/opencode/pi 的 exec/jsonl 旧模式与死代码
49
+ - `codex-bundled-test-helpers` 迁移至 `tests/lib/`
50
+ - 用户文档:新增入站文件、`/order alias`、kimi acp 配置说明
51
+
52
+ ### 修复
53
+
54
+ - 修复文档引用指向被排除的本地文档导致的破链
55
+
8
56
  ## [0.1.6] - 2026-08-14
9
57
 
10
58
  ### 新增
@@ -14,7 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
62
  - Codex runner 工厂:自动选择直连模式或 App Server 模式
15
63
  - `rollout-reader`:Codex App Server rollout 开关读取
16
64
  - Run card 审批区域渲染 + context limit 百分比显示
17
- - 架构文档:`codex-app-server-integration.md`
65
+ - 架构文档:`docs/zh/guides/codex-config.md`
18
66
 
19
67
  ### 修复
20
68
 
@@ -153,3 +201,5 @@ Initial release.
153
201
  [0.1.4]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.4
154
202
  [0.1.5]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.5
155
203
  [0.1.6]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.6
204
+ [0.1.7]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.7
205
+ [0.1.8]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.8
package/README.en.md CHANGED
@@ -16,13 +16,17 @@ Designed for a single-user, peer-to-peer private chat. The agent's system prompt
16
16
 
17
17
  ## ⚠️ Security Warning (read first)
18
18
 
19
- This tool turns Feishu messages directly into local agent executions, and agents run with **full permissions** by default (Claude `bypassPermissions`, Codex `approval_policy=never` + full access) — effectively handing a shell on your machine to whoever can chat with the bot. You MUST:
19
+ This tool turns Feishu messages directly into local agent executions. Claude runs with **full permissions** by default (fixed `bypassPermissions`); Codex uses the **app-server approval mode** (default `approvalPolicy=on-request` — command execution requires your confirmation on the Feishu card, sandbox defaults to `workspace-write`), which is relatively safe; other agents run with their CLI defaults. Agents can still read and write the local directories you specify. You MUST:
20
20
 
21
21
  - **Use it only in your own private (p2p) chat.** Never add the bot to any group chat; never let anyone else talk to it.
22
22
  - Restrict the app's visibility to **yourself only** in the Feishu Open Platform.
23
23
  - Don't run it on a machine holding data you can't afford to lose; prefer a dedicated user / machine / container.
24
24
  - Keep credentials such as `appSecret` in your local `config.yaml` only — **never commit them anywhere**.
25
25
 
26
+ In Codex approval mode, an approval card arrives before any command runs — approve or reject it right in Feishu:
27
+
28
+ ![Codex command approval card example](docs/images/approval-card.png)
29
+
26
30
  ## Prerequisites
27
31
 
28
32
  - OS: macOS / Linux. **Windows is not supported yet** (relies on POSIX behaviors such as Unix signals, bash, and file locks)
@@ -153,6 +157,10 @@ Full docs under [`docs/`](docs/):
153
157
 
154
158
  > This package is a CLI tool only — no programmatic API is exposed.
155
159
 
160
+ ## Acknowledgments
161
+
162
+ This project was inspired by [lark-coding-agent-bridge](https://github.com/zarazhangrui/lark-coding-agent-bridge) — thanks for demonstrating the feasibility of bridging Feishu with local coding agents.
163
+
156
164
  ## License
157
165
 
158
166
  [MIT](LICENSE) © bungabungawoda
package/README.md CHANGED
@@ -16,13 +16,17 @@
16
16
 
17
17
  ## ⚠️ 安全警告(必读)
18
18
 
19
- 本工具会把飞书消息直接转为本地 agent 执行,agent 默认以**完全权限**运行(Claude `bypassPermissions`、Codex `approval_policy=never` + full access),等价于把一台机器的 shell 交给聊天对端。请务必:
19
+ 本工具会把飞书消息直接转为本地 agent 执行。Claude 默认以**完全权限**运行(固定 `bypassPermissions`);Codex 采用 **app-server 审批模式**(默认 `approvalPolicy=on-request`,执行命令需在飞书卡片上确认,沙箱默认 `workspace-write`),相对安全;其他 agent 以各自 CLI 的默认权限运行。agent 仍可读写你指定的本地目录,请务必:
20
20
 
21
21
  - **仅限你自己的私聊(p2p)使用**。不要把机器人拉进任何群聊,不要让任何其他人能与它对话。
22
22
  - 在飞书开放平台把应用可见范围限制到**只有你自己**。
23
23
  - 运行本工具的机器上不要存放与损失承受力不符的数据;建议在独立用户/机器/容器里运行。
24
24
  - `appSecret` 等凭据只在本地 `config.yaml`,**不要提交到任何仓库**。
25
25
 
26
+ Codex 审批模式下,执行命令前会收到审批卡片,可直接在飞书里允许或拒绝:
27
+
28
+ ![Codex 命令审批卡片示例](docs/images/approval-card.png)
29
+
26
30
  ## 前置条件
27
31
 
28
32
  - 操作系统:macOS / Linux。**暂不支持 Windows**(依赖 Unix 信号、bash、文件锁等 POSIX 行为)
@@ -120,12 +124,16 @@ bridge 启动后不在终端输出,运行日志写入 `~/.lark-remote/logs/YYY
120
124
  | `/reconnect` | - | 重连飞书 |
121
125
  | `/restart` | - | 原地自重启 bridge(新进程同 config 接管) |
122
126
  | `/config get\|set` | `/c` | 查改运行时配置(agent-aware 卡片) |
123
- | `/order save\|list` | `/o` | 保存或列出常用指令 |
127
+ | `/order save\|list\|alias` | `/o` | 收藏常用指令;`/order alias` 注册快捷别名(输入 `$name` 展开) |
124
128
  | `!<cmd>` | - | 执行 bash 命令并流式输出到卡片(绕过串行队列) |
125
129
  | `/exit` | `/e` | 退出 bridge |
126
130
 
127
131
  直接发非 `/` 开头的消息即转发给当前默认 agent。
128
132
 
133
+ 往飞书私聊发**图片/文件**会自动保存到当前目录的 `.lark-remote-temp/<YYYYMMDDHHmm>/`
134
+ (建议把 `.lark-remote-temp/` 加进项目 `.gitignore`),随后说「请处理刚才保存的文件」
135
+ 即可让 agent 直接读本地文件。更多用法见 [docs/zh/usage.md](docs/zh/usage.md)。
136
+
129
137
  ## 测试
130
138
 
131
139
  ```bash
@@ -152,6 +160,10 @@ FEISHU_LIVE_TEST=1 bun run test tests/feishu-reaction-emoji-live.test.ts
152
160
 
153
161
  > 本包为纯 CLI 工具,不提供编程 API。
154
162
 
163
+ ## 致谢
164
+
165
+ 本项目受到了 [lark-coding-agent-bridge](https://github.com/zarazhangrui/lark-coding-agent-bridge) 的启发,感谢该项目展示了飞书与本地 Coding Agent 桥接的可行性。
166
+
155
167
  ## License
156
168
 
157
169
  [MIT](LICENSE) © bungabungawoda
@@ -6,30 +6,25 @@
6
6
  * - onResolved: update state, clear timer
7
7
  * - submit: validate → atomic state transition → async respond
8
8
  * - togglePerm: flip permission item selection
9
- * - onTurnEnded / onConnectionLost: mark all as expired
9
+ * - onTurnEnded: mark all as expired
10
10
  */
11
11
  import type { ApprovalRequestedEvent, ApprovalView, AgentEvent } from '../runner/types.js';
12
- export type ApprovalState = 'pending' | 'submitting' | 'resolved' | 'failed' | 'expired';
12
+ import type { ApprovalAction } from '../runner/types.js';
13
+ type ApprovalState = 'pending' | 'submitting' | 'resolved' | 'failed' | 'expired';
13
14
  export interface TrackedApproval {
14
15
  requestId: number | string;
15
- kind: 'command' | 'file' | 'permissions';
16
+ kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
16
17
  state: ApprovalState;
17
18
  view: ApprovalView;
18
19
  createdAt: number;
19
20
  timeoutTimer: ReturnType<typeof setTimeout> | null;
21
+ /** AskUserQuestion 已答问题索引 → 答案 label[](kind === 'question')。 */
22
+ answers?: Map<number, string[]>;
23
+ /** AskUserQuestion 补充说明(Codex user_note):问题索引 → 文本。 */
24
+ notes?: Map<number, string>;
20
25
  }
21
- export type ApprovalResponder = (requestId: number | string, response: unknown) => Promise<void>;
22
- export type ApprovalAction = {
23
- action: 'accept';
24
- } | {
25
- action: 'accept_for_session';
26
- } | {
27
- action: 'accept_with_execpolicy_amendment';
28
- } | {
29
- action: 'decline';
30
- } | {
31
- action: 'cancel';
32
- };
26
+ type ApprovalResponder = (requestId: number | string, response: unknown) => Promise<void>;
27
+ export type { ApprovalAction } from '../runner/types.js';
33
28
  export interface ApprovalToggleAction {
34
29
  permId: string;
35
30
  selected: boolean;
@@ -38,19 +33,11 @@ export declare class ApprovalCoordinator {
38
33
  private approvals;
39
34
  /** 已提交过的 nonce(按 requestId):同一按钮连点/飞书重投递只生效一次。 */
40
35
  private submittedNonces;
41
- private readonly runId;
42
- private readonly userId;
43
- private readonly chatId;
44
- private readonly workspace;
45
36
  private readonly approvalTimeoutMs;
46
37
  private readonly responder;
47
38
  private readonly interruptTurn;
48
39
  private readonly pushToCard;
49
40
  constructor(opts: {
50
- runId: string;
51
- userId: string;
52
- chatId: string;
53
- workspace: string;
54
41
  approvalTimeoutMs: number;
55
42
  responder: ApprovalResponder;
56
43
  interruptTurn: () => Promise<void>;
@@ -68,24 +55,66 @@ export declare class ApprovalCoordinator {
68
55
  * Handle turn ended — mark all pending approvals as expired.
69
56
  */
70
57
  onTurnEnded(): void;
71
- /**
72
- * Handle connection lost — same as onTurnEnded.
73
- */
74
- onConnectionLost(): void;
75
58
  /**
76
59
  * Submit an approval decision.
77
- * Returns a promise that resolves when the response is sent.
78
60
  */
79
61
  submit(action: ApprovalAction, ctx: {
80
62
  requestId: number | string;
81
63
  nonce?: string;
82
- }): Promise<string>;
64
+ }): Promise<void>;
83
65
  /**
84
66
  * Toggle a permission item's selected state.
85
67
  */
86
68
  togglePerm(action: ApprovalToggleAction, ctx: {
87
69
  requestId: number | string;
88
- }): Promise<string>;
70
+ }): Promise<void>;
71
+ /**
72
+ * Claude AskUserQuestion 选项点击。
73
+ *
74
+ * - 单选:点击即选中该选项;全部问题答完时自动通过 responder 提交答案。
75
+ * - 多选:切换 selected 并推 approval_view_updated 重渲染,等待
76
+ * submitAnswers() 显式提交。
77
+ */
78
+ toggleAnswer(action: {
79
+ questionIndex: number;
80
+ option: string;
81
+ }, ctx: {
82
+ requestId: number | string;
83
+ nonce?: string;
84
+ }): Promise<void>;
85
+ /**
86
+ * Claude AskUserQuestion 多选问题的「提交答案」按钮。
87
+ * 提交该问题当前勾选的选项;全部问题答完时通过 responder 提交。
88
+ */
89
+ submitAnswers(action: {
90
+ questionIndex: number;
91
+ }, ctx: {
92
+ requestId: number | string;
93
+ nonce?: string;
94
+ }): Promise<void>;
95
+ /**
96
+ * AskUserQuestion 自定义答案(Other,review P3-4):自由文本直接作为该
97
+ * 单选问题的答案。与单选 toggle 同语义:全部问题答完自动提交。
98
+ */
99
+ answerCustom(action: {
100
+ questionIndex: number;
101
+ text: string;
102
+ }, ctx: {
103
+ requestId: number | string;
104
+ nonce?: string;
105
+ }): Promise<void>;
106
+ /**
107
+ * AskUserQuestion 补充说明(Codex user_note):选项之外的可选文本,随答案
108
+ * 提交;不单独构成答案(仍需选项/自定义答案才能完成该问题)。note 更新
109
+ * 后回流视图(卡片回显),同一请求可多次修改(新 nonce 来自新渲染)。
110
+ */
111
+ answerNote(action: {
112
+ questionIndex: number;
113
+ text: string;
114
+ }, ctx: {
115
+ requestId: number | string;
116
+ nonce?: string;
117
+ }): Promise<void>;
89
118
  /**
90
119
  * Update a pending approval's view (out-of-order item/started arrives late).
91
120
  * Only applies while the approval is still pending — resolved/expired
@@ -96,11 +125,19 @@ export declare class ApprovalCoordinator {
96
125
  * Get the number of pending approvals.
97
126
  */
98
127
  pendingCount(): number;
99
- /**
100
- * Get the head (oldest pending) approval, or undefined.
101
- */
102
- head(): TrackedApproval | undefined;
103
128
  private clearTimer;
129
+ /** nonce 去重:同一 nonce 只允许生效一次(防连点/飞书重投递)。 */
130
+ private assertFreshNonce;
131
+ /** 记录问题答案到 tracked.answers(懒初始化 Map)。 */
132
+ private recordAnswer;
133
+ private getPendingQuestion;
134
+ private allQuestionsAnswered;
135
+ /** 推送 question 卡片视图更新(选项勾选状态变化)。 */
136
+ private pushQuestionViewUpdate;
137
+ /** 全部问题已答:构建 answers 并提交(非空校验 + 原子状态迁移)。 */
138
+ private respondQuestionAnswers;
139
+ /** 推 approval_resolved 到卡片(移除审批区;幂等,失败仅记日志)。 */
140
+ private pushResolved;
104
141
  private expireApproval;
105
142
  private actionToDecision;
106
143
  }
@@ -6,7 +6,7 @@
6
6
  * - onResolved: update state, clear timer
7
7
  * - submit: validate → atomic state transition → async respond
8
8
  * - togglePerm: flip permission item selection
9
- * - onTurnEnded / onConnectionLost: mark all as expired
9
+ * - onTurnEnded: mark all as expired
10
10
  */
11
11
  import { getLogger } from '../logger/index.js';
12
12
  // =============================================================================
@@ -16,19 +16,11 @@ export class ApprovalCoordinator {
16
16
  approvals = new Map();
17
17
  /** 已提交过的 nonce(按 requestId):同一按钮连点/飞书重投递只生效一次。 */
18
18
  submittedNonces = new Map();
19
- runId;
20
- userId;
21
- chatId;
22
- workspace;
23
19
  approvalTimeoutMs;
24
20
  responder;
25
21
  interruptTurn;
26
22
  pushToCard;
27
23
  constructor(opts) {
28
- this.runId = opts.runId;
29
- this.userId = opts.userId;
30
- this.chatId = opts.chatId;
31
- this.workspace = opts.workspace;
32
24
  this.approvalTimeoutMs = opts.approvalTimeoutMs;
33
25
  this.responder = opts.responder;
34
26
  this.interruptTurn = opts.interruptTurn;
@@ -45,9 +37,12 @@ export class ApprovalCoordinator {
45
37
  existing.view = event.view;
46
38
  return;
47
39
  }
40
+ // 事件级 timeoutMs 优先(Codex autoResolutionMs / Pi extension timeout),
41
+ // 缺省回落 run 级默认超时。放在事件上而非桥按 agentKind 分支,公共层
42
+ // 保持无 agent 分支。
48
43
  const timer = setTimeout(() => {
49
44
  this.expireApproval(event.requestId);
50
- }, this.approvalTimeoutMs);
45
+ }, event.timeoutMs ?? this.approvalTimeoutMs);
51
46
  const tracked = {
52
47
  requestId: event.requestId,
53
48
  kind: event.kind,
@@ -79,15 +74,8 @@ export class ApprovalCoordinator {
79
74
  }
80
75
  }
81
76
  }
82
- /**
83
- * Handle connection lost — same as onTurnEnded.
84
- */
85
- onConnectionLost() {
86
- this.onTurnEnded();
87
- }
88
77
  /**
89
78
  * Submit an approval decision.
90
- * Returns a promise that resolves when the response is sent.
91
79
  */
92
80
  async submit(action, ctx) {
93
81
  const tracked = this.approvals.get(ctx.requestId);
@@ -97,14 +85,7 @@ export class ApprovalCoordinator {
97
85
  // nonce 去重:同一张卡片渲染出的按钮连续点击(或飞书重投递)携带相同
98
86
  // nonce,只允许生效一次;不同渲染的按钮 nonce 不同,状态机(pending →
99
87
  // submitting)仍然兜底防二次提交。
100
- if (ctx.nonce !== undefined) {
101
- const seen = this.submittedNonces.get(ctx.requestId) ?? new Set();
102
- if (seen.has(ctx.nonce)) {
103
- throw new Error(`Approval request ${ctx.requestId} already submitted (duplicate nonce)`);
104
- }
105
- seen.add(ctx.nonce);
106
- this.submittedNonces.set(ctx.requestId, seen);
107
- }
88
+ this.assertFreshNonce(ctx.requestId, ctx.nonce);
108
89
  if (tracked.state !== 'pending') {
109
90
  throw new Error(`Approval request ${ctx.requestId} is no longer pending (state=${tracked.state})`);
110
91
  }
@@ -123,11 +104,14 @@ export class ApprovalCoordinator {
123
104
  try {
124
105
  await this.responder(ctx.requestId, action);
125
106
  tracked.state = 'resolved';
107
+ // 提交成功后立即从卡片移除审批区(agent 无关):claude 控制协议没有
108
+ // resolved 回发,不推就会「已点允许按钮仍在」残留 UI;codex 的 server
109
+ // 稍后也会发 approval_resolved,reducer 按 requestId 过滤幂等。
110
+ this.pushResolved(ctx.requestId);
126
111
  }
127
112
  catch {
128
113
  tracked.state = 'failed';
129
114
  }
130
- return `Approval ${tracked.requestId} (${tracked.kind}): ${decision}`;
131
115
  }
132
116
  /**
133
117
  * Toggle a permission item's selected state.
@@ -152,7 +136,118 @@ export class ApprovalCoordinator {
152
136
  throw new Error(`Permission item ${action.permId} not found in request ${ctx.requestId}`);
153
137
  }
154
138
  item.selected = action.selected;
155
- return `Permission ${action.permId}: ${action.selected ? 'granted' : 'denied'}`;
139
+ }
140
+ /**
141
+ * Claude AskUserQuestion 选项点击。
142
+ *
143
+ * - 单选:点击即选中该选项;全部问题答完时自动通过 responder 提交答案。
144
+ * - 多选:切换 selected 并推 approval_view_updated 重渲染,等待
145
+ * submitAnswers() 显式提交。
146
+ */
147
+ async toggleAnswer(action, ctx) {
148
+ const tracked = this.getPendingQuestion(ctx.requestId);
149
+ const questions = tracked.view.questions;
150
+ const q = questions[action.questionIndex];
151
+ if (!q) {
152
+ throw new Error(`Question ${action.questionIndex} not found in request ${ctx.requestId}`);
153
+ }
154
+ const option = q.options.find((o) => o.label === action.option);
155
+ if (!option) {
156
+ throw new Error(`Option "${action.option}" not found in question ${action.questionIndex}`);
157
+ }
158
+ const selected = new Set(q.selected ?? []);
159
+ if (q.multiSelect) {
160
+ // review P2-1:多选切换按钮同一 nonce 重复投递(双击/飞书重投递)只应
161
+ // toggle 一次,否则勾选会被二次 toggle 抵消。单选用例的重复防护在
162
+ // respondQuestionAnswers(提交即幂等,选中是 set 语义)。
163
+ this.assertFreshNonce(ctx.requestId, ctx.nonce);
164
+ if (selected.has(action.option)) {
165
+ selected.delete(action.option);
166
+ }
167
+ else {
168
+ selected.add(action.option);
169
+ }
170
+ q.selected = [...selected];
171
+ this.pushQuestionViewUpdate(ctx.requestId, tracked);
172
+ return;
173
+ }
174
+ // 单选:替换选择;全部问题答完立即提交,否则等待后续问题。
175
+ q.selected = [action.option];
176
+ this.recordAnswer(tracked, action.questionIndex, [action.option]);
177
+ if (this.allQuestionsAnswered(tracked)) {
178
+ return this.respondQuestionAnswers(tracked, ctx);
179
+ }
180
+ this.pushQuestionViewUpdate(ctx.requestId, tracked);
181
+ }
182
+ /**
183
+ * Claude AskUserQuestion 多选问题的「提交答案」按钮。
184
+ * 提交该问题当前勾选的选项;全部问题答完时通过 responder 提交。
185
+ */
186
+ async submitAnswers(action, ctx) {
187
+ const tracked = this.getPendingQuestion(ctx.requestId);
188
+ const questions = tracked.view.questions;
189
+ const q = questions[action.questionIndex];
190
+ if (!q) {
191
+ throw new Error(`Question ${action.questionIndex} not found in request ${ctx.requestId}`);
192
+ }
193
+ if (!q.multiSelect) {
194
+ throw new Error('该问题为单选,请直接点击选项');
195
+ }
196
+ const selected = q.selected ?? [];
197
+ if (selected.length === 0) {
198
+ throw new Error('请先选择至少一个选项');
199
+ }
200
+ this.recordAnswer(tracked, action.questionIndex, selected);
201
+ if (this.allQuestionsAnswered(tracked)) {
202
+ return this.respondQuestionAnswers(tracked, ctx);
203
+ }
204
+ }
205
+ /**
206
+ * AskUserQuestion 自定义答案(Other,review P3-4):自由文本直接作为该
207
+ * 单选问题的答案。与单选 toggle 同语义:全部问题答完自动提交。
208
+ */
209
+ async answerCustom(action, ctx) {
210
+ const tracked = this.getPendingQuestion(ctx.requestId);
211
+ const questions = tracked.view.questions;
212
+ const q = questions[action.questionIndex];
213
+ if (!q) {
214
+ throw new Error(`Question ${action.questionIndex} not found in request ${ctx.requestId}`);
215
+ }
216
+ if (q.multiSelect) {
217
+ throw new Error('自定义答案仅支持单选');
218
+ }
219
+ const text = action.text.trim();
220
+ if (!text) {
221
+ throw new Error('请输入自定义答案');
222
+ }
223
+ q.selected = [text];
224
+ this.recordAnswer(tracked, action.questionIndex, [text]);
225
+ if (this.allQuestionsAnswered(tracked)) {
226
+ return this.respondQuestionAnswers(tracked, ctx);
227
+ }
228
+ this.pushQuestionViewUpdate(ctx.requestId, tracked);
229
+ }
230
+ /**
231
+ * AskUserQuestion 补充说明(Codex user_note):选项之外的可选文本,随答案
232
+ * 提交;不单独构成答案(仍需选项/自定义答案才能完成该问题)。note 更新
233
+ * 后回流视图(卡片回显),同一请求可多次修改(新 nonce 来自新渲染)。
234
+ */
235
+ async answerNote(action, ctx) {
236
+ const tracked = this.getPendingQuestion(ctx.requestId);
237
+ const questions = tracked.view.questions;
238
+ const q = questions[action.questionIndex];
239
+ if (!q) {
240
+ throw new Error(`Question ${action.questionIndex} not found in request ${ctx.requestId}`);
241
+ }
242
+ const text = action.text.trim();
243
+ if (!text) {
244
+ throw new Error('请输入补充说明');
245
+ }
246
+ this.assertFreshNonce(ctx.requestId, ctx.nonce);
247
+ tracked.notes ??= new Map();
248
+ tracked.notes.set(action.questionIndex, text);
249
+ q.note = text;
250
+ this.pushQuestionViewUpdate(ctx.requestId, tracked);
156
251
  }
157
252
  /**
158
253
  * Update a pending approval's view (out-of-order item/started arrives late).
@@ -176,20 +271,6 @@ export class ApprovalCoordinator {
176
271
  }
177
272
  return count;
178
273
  }
179
- /**
180
- * Get the head (oldest pending) approval, or undefined.
181
- */
182
- head() {
183
- let oldest;
184
- let oldestTime = Infinity;
185
- for (const [, tracked] of this.approvals) {
186
- if (tracked.state === 'pending' && tracked.createdAt < oldestTime) {
187
- oldest = tracked;
188
- oldestTime = tracked.createdAt;
189
- }
190
- }
191
- return oldest;
192
- }
193
274
  // =========================================================================
194
275
  // Internal
195
276
  // =========================================================================
@@ -199,6 +280,103 @@ export class ApprovalCoordinator {
199
280
  tracked.timeoutTimer = null;
200
281
  }
201
282
  }
283
+ /** nonce 去重:同一 nonce 只允许生效一次(防连点/飞书重投递)。 */
284
+ assertFreshNonce(requestId, nonce) {
285
+ if (nonce === undefined)
286
+ return;
287
+ const seen = this.submittedNonces.get(requestId) ?? new Set();
288
+ if (seen.has(nonce)) {
289
+ throw new Error(`Approval request ${requestId} already submitted (duplicate nonce)`);
290
+ }
291
+ seen.add(nonce);
292
+ this.submittedNonces.set(requestId, seen);
293
+ }
294
+ /** 记录问题答案到 tracked.answers(懒初始化 Map)。 */
295
+ recordAnswer(tracked, questionIndex, values) {
296
+ tracked.answers ??= new Map();
297
+ tracked.answers.set(questionIndex, values);
298
+ }
299
+ getPendingQuestion(requestId) {
300
+ const tracked = this.approvals.get(requestId);
301
+ if (!tracked) {
302
+ throw new Error(`Approval request ${requestId} not found`);
303
+ }
304
+ if (tracked.kind !== 'question') {
305
+ throw new Error(`Approval request ${requestId} is not a question request`);
306
+ }
307
+ if (tracked.state !== 'pending') {
308
+ throw new Error(`Approval request ${requestId} is no longer pending`);
309
+ }
310
+ return tracked;
311
+ }
312
+ allQuestionsAnswered(tracked) {
313
+ const questions = tracked.view.questions ?? [];
314
+ if (questions.length === 0)
315
+ return false;
316
+ for (let i = 0; i < questions.length; i++) {
317
+ const answered = (tracked.answers?.get(i)?.length ?? 0) > 0;
318
+ if (!answered)
319
+ return false;
320
+ }
321
+ return true;
322
+ }
323
+ /** 推送 question 卡片视图更新(选项勾选状态变化)。 */
324
+ pushQuestionViewUpdate(requestId, tracked) {
325
+ void this.pushToCard([
326
+ {
327
+ type: 'approval_view_updated',
328
+ requestId,
329
+ view: tracked.view,
330
+ timestamp: new Date().toISOString(),
331
+ },
332
+ ]).catch((err) => {
333
+ getLogger().warn(`[approval-coordinator] question view update failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
334
+ });
335
+ }
336
+ /** 全部问题已答:构建 answers 并提交(非空校验 + 原子状态迁移)。 */
337
+ async respondQuestionAnswers(tracked, ctx) {
338
+ this.assertFreshNonce(ctx.requestId, ctx.nonce);
339
+ if (tracked.state !== 'pending') {
340
+ throw new Error(`Approval request ${ctx.requestId} is no longer pending (state=${tracked.state})`);
341
+ }
342
+ const questions = tracked.view.questions ?? [];
343
+ const answers = {};
344
+ const notes = {};
345
+ for (let i = 0; i < questions.length; i++) {
346
+ const selected = tracked.answers?.get(i) ?? [];
347
+ answers[questions[i].question] = questions[i].multiSelect ? selected : selected[0];
348
+ const note = tracked.notes?.get(i);
349
+ if (note)
350
+ notes[questions[i].question] = note;
351
+ }
352
+ tracked.state = 'submitting';
353
+ this.clearTimer(tracked);
354
+ try {
355
+ await this.responder(ctx.requestId, {
356
+ action: 'answer',
357
+ answers,
358
+ ...(Object.keys(notes).length > 0 ? { notes } : {}),
359
+ });
360
+ tracked.state = 'resolved';
361
+ this.pushResolved(ctx.requestId);
362
+ }
363
+ catch {
364
+ tracked.state = 'failed';
365
+ }
366
+ }
367
+ /** 推 approval_resolved 到卡片(移除审批区;幂等,失败仅记日志)。 */
368
+ pushResolved(requestId) {
369
+ void this.pushToCard([
370
+ {
371
+ type: 'approval_resolved',
372
+ requestId,
373
+ outcome: 'resolved',
374
+ timestamp: new Date().toISOString(),
375
+ },
376
+ ]).catch((err) => {
377
+ getLogger().warn(`[approval-coordinator] resolved card event failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
378
+ });
379
+ }
202
380
  expireApproval(requestId) {
203
381
  const tracked = this.approvals.get(requestId);
204
382
  if (!tracked || tracked.state !== 'pending')
@@ -231,6 +409,10 @@ export class ApprovalCoordinator {
231
409
  return 'acceptForSession';
232
410
  case 'accept_with_execpolicy_amendment':
233
411
  return 'acceptWithExecpolicyAmendment';
412
+ case 'accept_all':
413
+ return 'acceptAll';
414
+ case 'answer':
415
+ return 'answer';
234
416
  case 'decline':
235
417
  return 'decline';
236
418
  case 'cancel':
@@ -247,6 +429,8 @@ export function decisionToApprovalAction(decision) {
247
429
  return { action: 'accept_for_session' };
248
430
  case 'acceptWithExecpolicyAmendment':
249
431
  return { action: 'accept_with_execpolicy_amendment' };
432
+ case 'acceptAll':
433
+ return { action: 'accept_all' };
250
434
  case 'decline':
251
435
  return { action: 'decline' };
252
436
  case 'cancel':