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.
- package/CHANGELOG.md +51 -1
- package/README.en.md +9 -1
- package/README.md +14 -2
- package/dist/bridge/approval-coordinator.d.ts +71 -34
- package/dist/bridge/approval-coordinator.js +225 -41
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +97 -13
- package/dist/bridge/index.js +227 -87
- package/dist/bridge/queue-manager.d.ts +2 -1
- package/dist/bridge/queue-manager.js +1 -0
- package/dist/card/approval-render.js +294 -36
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +7 -16
- package/dist/card/card-shared.js +7 -30
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.d.ts +7 -0
- package/dist/card/run-renderer.js +116 -238
- package/dist/card/run-state.d.ts +13 -0
- package/dist/card/run-state.js +25 -16
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/index.d.ts +112 -24
- package/dist/config/index.js +139 -37
- package/dist/connector/index.d.ts +69 -2
- package/dist/connector/index.js +107 -1
- package/dist/index.js +150 -118
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-action-payload.d.ts +30 -0
- package/dist/router/card-action-payload.js +39 -0
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +30 -1
- package/dist/router/config/codex.js +29 -50
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +5 -4
- package/dist/router/config/dsh.d.ts +51 -0
- package/dist/router/config/dsh.js +179 -0
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +28 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +17 -2
- package/dist/router/index.d.ts +76 -22
- package/dist/router/index.js +479 -548
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +16 -2
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +34 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +768 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
- package/dist/runner/codex/app-server/protocol-types.js +2 -13
- package/dist/runner/codex/app-server/runner.d.ts +52 -59
- package/dist/runner/codex/app-server/runner.js +216 -338
- package/dist/runner/codex/app-server/translator.d.ts +8 -21
- package/dist/runner/codex/app-server/translator.js +45 -18
- package/dist/runner/codex/index.d.ts +0 -2
- package/dist/runner/codex/index.js +0 -2
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +271 -0
- package/dist/runner/common/acp/protocol-types.js +77 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- package/dist/runner/dsh/client.d.ts +95 -0
- package/dist/runner/dsh/client.js +418 -0
- package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
- package/dist/runner/dsh/fake-dsh-server.js +156 -0
- package/dist/runner/dsh/index.d.ts +3 -0
- package/dist/runner/dsh/index.js +4 -0
- package/dist/runner/dsh/runner.d.ts +57 -0
- package/dist/runner/dsh/runner.js +191 -0
- package/dist/runner/dsh/translator.d.ts +28 -0
- package/dist/runner/dsh/translator.js +98 -0
- package/dist/runner/dsh/types.d.ts +155 -0
- package/dist/runner/dsh/types.js +14 -0
- package/dist/runner/index.d.ts +5 -4
- package/dist/runner/index.js +5 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +516 -0
- package/dist/runner/kimi/acp/translator.d.ts +138 -0
- package/dist/runner/kimi/acp/translator.js +441 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +74 -0
- package/dist/runner/pi/rpc/runner.js +190 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- package/dist/runner/probe.js +6 -1
- package/dist/runner/question-common.d.ts +26 -0
- package/dist/runner/question-common.js +42 -0
- package/dist/runner/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +97 -14
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +43 -28
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +14 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/dsh/index.d.ts +1 -0
- package/dist/session/dsh/index.js +2 -0
- package/dist/session/dsh/sessions.d.ts +39 -0
- package/dist/session/dsh/sessions.js +237 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/dist/workspace/index.d.ts +4 -0
- package/dist/workspace/index.js +8 -1
- package/package.json +2 -1
- package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/factory.d.ts +0 -29
- package/dist/runner/codex/factory.js +0 -39
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -66
- package/dist/runner/codex/runner.js +0 -129
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- 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-
|
|
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
|
|
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
|
+

|
|
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
|
|
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
|
+

|
|
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
|
|
9
|
+
* - onTurnEnded: mark all as expired
|
|
10
10
|
*/
|
|
11
11
|
import type { ApprovalRequestedEvent, ApprovalView, AgentEvent } from '../runner/types.js';
|
|
12
|
-
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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':
|