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
@@ -8,6 +8,24 @@
8
8
  function mkNonce() {
9
9
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
10
10
  }
11
+ /** reason div(3 处共用);反引号中和防 lark_md 解析错误(review P2-2)。 */
12
+ function reasonDiv(approval) {
13
+ if (!approval.reason)
14
+ return null;
15
+ const neutralized = approval.reason.replace(/`/g, '·');
16
+ return {
17
+ tag: 'div',
18
+ text: { tag: 'lark_md', content: `💡 ${neutralized}` },
19
+ };
20
+ }
21
+ /**
22
+ * Append a possibly-null element to the array (convenience for optional divs).
23
+ * If the element is null, nothing is appended.
24
+ */
25
+ function pushIf(arr, el) {
26
+ if (el !== null)
27
+ arr.push(el);
28
+ }
11
29
  /**
12
30
  * Render the approval area for a run card.
13
31
  * Returns an array of CardKit 2.0 elements to append to the card body.
@@ -24,12 +42,18 @@ export function renderApprovalArea(approval, opts) {
24
42
  if (approval.kind === 'command') {
25
43
  elements.push(...renderCommandApproval(approval, opts?.expired, opts?.runId));
26
44
  }
45
+ else if (approval.kind === 'tool') {
46
+ elements.push(...renderToolApproval(approval, opts?.expired, opts?.runId));
47
+ }
27
48
  else if (approval.kind === 'file') {
28
49
  elements.push(...renderFileApproval(approval, opts?.expired, opts?.runId));
29
50
  }
30
51
  else if (approval.kind === 'permissions') {
31
52
  elements.push(...renderPermissionsApproval(approval, opts?.expired, opts?.runId));
32
53
  }
54
+ else if (approval.kind === 'question') {
55
+ elements.push(...renderQuestionApproval(approval, opts?.expired, opts?.runId));
56
+ }
33
57
  return elements;
34
58
  }
35
59
  /**
@@ -51,11 +75,18 @@ function renderDecisionButtons(approval, expired, runId) {
51
75
  accept: { label: '✅ 允许', type: 'primary' },
52
76
  acceptForSession: { label: '✅ 允许本次会话', type: 'primary' },
53
77
  acceptWithExecpolicyAmendment: { label: '🔒 允许并记住命令', type: 'primary' },
78
+ acceptAll: { label: '✅ 允许所有', type: 'primary' },
54
79
  decline: { label: '❌ 拒绝', type: 'danger' },
55
80
  };
56
81
  // Fixed presentation order; only offer decisions the protocol listed
57
- // (accept/decline are always available).
58
- const offered = ['accept', 'acceptForSession', 'acceptWithExecpolicyAmendment', 'decline'].filter((d) => d === 'accept' || d === 'decline' || approval.availableDecisions.includes(d));
82
+ // (accept/decline are always available; acceptAll is Claude 专属「允许所有」)。
83
+ const offered = [
84
+ 'accept',
85
+ 'acceptForSession',
86
+ 'acceptWithExecpolicyAmendment',
87
+ 'acceptAll',
88
+ 'decline',
89
+ ].filter((d) => d === 'accept' || d === 'decline' || approval.availableDecisions.includes(d));
59
90
  // 决策按钮逐个作为 body 直接元素返回(CardKit body 纵向堆叠 = 上下排列)。
60
91
  // 不能用 column_set width:auto 横排:手机窄屏下多个按钮挤在一行,按钮文字
61
92
  // 被截断成省略号,用户无法辨认审批动作。
@@ -78,12 +109,53 @@ function renderDecisionButtons(approval, expired, runId) {
78
109
  }));
79
110
  }
80
111
  // =============================================================================
112
+ // Tool Approval (non-command tools, e.g. ExitPlanMode)
113
+ // =============================================================================
114
+ /**
115
+ * 通用工具权限请求(kind === 'tool'):展示工具名 + 用途说明(reason)+ 原始
116
+ * input(若有)。ExitPlanMode 的 input 为空对象,工具名本身即审批内容,
117
+ * 不再落入 command 槽位显示无意义 `{}`。
118
+ */
119
+ function renderToolApproval(approval, expired, runId) {
120
+ const elements = [];
121
+ // ExitPlanMode 是「退出计划模式」:标题用计划审批语义;其他工具用通用标题。
122
+ const isPlanExit = approval.toolName === 'ExitPlanMode';
123
+ elements.push({
124
+ tag: 'div',
125
+ text: {
126
+ tag: 'lark_md',
127
+ content: isPlanExit ? '**📋 计划审批**' : '**🔧 工具请求**',
128
+ },
129
+ });
130
+ const toolLabel = approval.toolName ?? '(unknown tool)';
131
+ elements.push({
132
+ tag: 'div',
133
+ text: { tag: 'lark_md', content: `🔧 \`${toolLabel}\`` },
134
+ });
135
+ pushIf(elements, reasonDiv(approval));
136
+ // 原始 input(非空时展示,截断保护卡片预算;ExitPlanMode 的 `{}` 不展示)。
137
+ if (approval.toolInput && approval.toolInput !== '{}') {
138
+ const neutralized = approval.toolInput.replace(/`/g, '·');
139
+ elements.push({
140
+ tag: 'div',
141
+ text: { tag: 'lark_md', content: `\`${neutralized.slice(0, 500)}\`` },
142
+ });
143
+ }
144
+ elements.push(...renderDecisionButtons(approval, expired, runId));
145
+ return elements;
146
+ }
147
+ // =============================================================================
81
148
  // Command Approval
82
149
  // =============================================================================
83
150
  function renderCommandApproval(approval, expired, runId) {
84
151
  const elements = [];
85
152
  // Command display
86
- const cmdContent = approval.command ? `\`${approval.command.slice(0, 500)}\`` : '(no command)';
153
+ // review P2-2:命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
154
+ // 反引号极易触发 11311 解析错误导致整卡失败(同 renderFileDiff 的中和策略)。
155
+ const neutralizedCommand = (approval.command ?? '').replace(/`/g, '·');
156
+ const cmdContent = neutralizedCommand
157
+ ? `\`${neutralizedCommand.slice(0, 500)}\``
158
+ : '(no command)';
87
159
  elements.push({
88
160
  tag: 'div',
89
161
  text: { tag: 'lark_md', content: `**⚡ 命令审批**\n${cmdContent}` },
@@ -94,23 +166,11 @@ function renderCommandApproval(approval, expired, runId) {
94
166
  text: { tag: 'lark_md', content: `📂 \`${approval.commandCwd}\`` },
95
167
  });
96
168
  }
97
- if (approval.reason) {
98
- elements.push({
99
- tag: 'div',
100
- text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
101
- });
102
- }
169
+ pushIf(elements, reasonDiv(approval));
103
170
  // Action buttons — derived from the real protocol decision list (see
104
171
  // renderDecisionButtons): accept/decline always, acceptForSession and
105
172
  // acceptWithExecpolicyAmendment only when the server offers them.
106
173
  elements.push(...renderDecisionButtons(approval, expired, runId));
107
- // Pending count
108
- if (approval.pendingTotal > 1) {
109
- elements.push({
110
- tag: 'div',
111
- text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
112
- });
113
- }
114
174
  return elements;
115
175
  }
116
176
  // =============================================================================
@@ -122,12 +182,7 @@ function renderFileApproval(approval, expired, runId) {
122
182
  tag: 'div',
123
183
  text: { tag: 'lark_md', content: '**📄 文件变更审批**' },
124
184
  });
125
- if (approval.reason) {
126
- elements.push({
127
- tag: 'div',
128
- text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
129
- });
130
- }
185
+ pushIf(elements, reasonDiv(approval));
131
186
  // File changes list
132
187
  if (approval.fileChanges && approval.fileChanges.length > 0) {
133
188
  // 审批区 diff 总预算:估算函数不计审批区,多文件大 diff 可能顶爆 28KB 卡
@@ -164,12 +219,6 @@ function renderFileApproval(approval, expired, runId) {
164
219
  // Action buttons
165
220
  // Derived from the real protocol decision list (see renderDecisionButtons).
166
221
  elements.push(...renderDecisionButtons(approval, expired, runId));
167
- if (approval.pendingTotal > 1) {
168
- elements.push({
169
- tag: 'div',
170
- text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
171
- });
172
- }
173
222
  return elements;
174
223
  }
175
224
  /**
@@ -195,12 +244,7 @@ function renderPermissionsApproval(approval, expired, runId) {
195
244
  tag: 'div',
196
245
  text: { tag: 'lark_md', content: '**🔒 权限审批**' },
197
246
  });
198
- if (approval.reason) {
199
- elements.push({
200
- tag: 'div',
201
- text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
202
- });
203
- }
247
+ pushIf(elements, reasonDiv(approval));
204
248
  // Permission items with toggle buttons
205
249
  if (approval.permissions?.items) {
206
250
  for (const item of approval.permissions.items) {
@@ -236,12 +280,226 @@ function renderPermissionsApproval(approval, expired, runId) {
236
280
  // Action buttons
237
281
  // Derived from the real protocol decision list (see renderDecisionButtons).
238
282
  elements.push(...renderDecisionButtons(approval, expired, runId));
239
- if (approval.pendingTotal > 1) {
283
+ return elements;
284
+ }
285
+ // =============================================================================
286
+ // AskUserQuestion Approval
287
+ // =============================================================================
288
+ /**
289
+ * 渲染 Claude AskUserQuestion 选项卡(审批区复用)。
290
+ *
291
+ * - 单选问题:点击选项即选中(协调器在全部问题答完后自动提交)。
292
+ * - 多选问题:点击切换勾选,勾选后出现「提交答案」按钮。
293
+ * - 已选状态经 approval_view_updated 回流(view.questions[].selected)。
294
+ */
295
+ function renderQuestionApproval(approval, expired, runId) {
296
+ const elements = [];
297
+ elements.push({
298
+ tag: 'div',
299
+ text: { tag: 'lark_md', content: '**❓ 需要你回答**' },
300
+ });
301
+ // Kimi elicitation form:完整题干只在 form message 里合并出现,概要行展示
302
+ // 于提问卡顶部(数据驱动,Kimi runner 翻译时填充 approval.intro)。
303
+ if (approval.intro) {
304
+ elements.push({
305
+ tag: 'div',
306
+ text: { tag: 'lark_md', content: `📝 ${approval.intro}` },
307
+ });
308
+ }
309
+ if (expired) {
310
+ elements.push({
311
+ tag: 'div',
312
+ text: { tag: 'lark_md', content: '⏰ **审批已过期**' },
313
+ });
314
+ return elements;
315
+ }
316
+ const questions = approval.questions ?? [];
317
+ for (let i = 0; i < questions.length; i++) {
318
+ const q = questions[i];
319
+ const selected = q.selected ?? [];
320
+ const header = q.header || `问题 ${i + 1}`;
321
+ const freeText = (q.options?.length ?? 0) === 0;
240
322
  elements.push({
241
323
  tag: 'div',
242
- text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
324
+ text: {
325
+ tag: 'lark_md',
326
+ content: `**${header}**${q.multiSelect ? '(可多选)' : ''}\n${q.question}`,
327
+ },
328
+ });
329
+ if (freeText) {
330
+ // 自由文本题(Codex options:null / Pi extension input):无选项行,
331
+ // 直接渲染题面 + 输入框,提交走 answerCustom(coordinator 不要求有选项)。
332
+ elements.push(renderAnswerInput(approval.requestId, i, q.placeholder ?? '输入回答…', runId));
333
+ // 已答回显:input_value 不跨卡片更新保留,多题场景下用户提交后若还有
334
+ // 其他问题未答,卡片必须展示已答文本(与自定义答案选中态同模式)。
335
+ const answered = q.selected?.find((s) => s.length > 0);
336
+ if (answered) {
337
+ elements.push({
338
+ tag: 'div',
339
+ text: { tag: 'lark_md', content: `✍️ 已答:${answered}` },
340
+ });
341
+ }
342
+ continue;
343
+ }
344
+ for (const option of q.options ?? []) {
345
+ const isSelected = selected.includes(option.label);
346
+ // 单选/多选图标区分:单选 ⚪/🔵(radio 隐喻),多选 ⬜/☑️(checkbox 隐喻)。
347
+ const icon = q.multiSelect ? (isSelected ? '☑️' : '⬜') : isSelected ? '🔵' : '⚪';
348
+ elements.push(renderOptionRow(approval.requestId, i, option, isSelected, icon, runId));
349
+ }
350
+ // 自定义答案(Other)的选中态展示:选项按钮无法表示自由文本,单独显示
351
+ // 已选文本,避免「点了没反应」的困惑(review P3)。
352
+ const customSelected = q.selected?.find((s) => !q.options.some((o) => o.label === s));
353
+ if (customSelected) {
354
+ elements.push({
355
+ tag: 'div',
356
+ text: { tag: 'lark_md', content: `✍️ 自定义答案:${customSelected}` },
357
+ });
358
+ }
359
+ // review P3-4:单选问题提供自定义答案(Other)输入——输入文本后点击
360
+ // 输入框右侧 ✓ 提交图标,input_value 经 connector raw 事件回传。
361
+ // 显隐按 isOther !== false:Kimi form 会丢弃非声明选项值,翻译时置
362
+ // isOther=false 隐藏输入;Claude 未设置该字段默认显示(行为不变)。
363
+ if (!q.multiSelect && q.isOther !== false) {
364
+ elements.push(renderAnswerInput(approval.requestId, i, '自定义答案(Other)…', runId, 'approval.answerCustom'));
365
+ }
366
+ // Codex user_note:选项之外的补充说明(allowNote 数据驱动门控)。
367
+ // 提交走 answerNote(coordinator 记录 note,随答案一起提交),已填回显。
368
+ if (q.allowNote === true) {
369
+ elements.push(renderAnswerInput(approval.requestId, i, '补充说明(可选)…', runId, 'approval.answerNote'));
370
+ if (q.note) {
371
+ elements.push({
372
+ tag: 'div',
373
+ text: { tag: 'lark_md', content: `📝 ${q.note}` },
374
+ });
375
+ }
376
+ }
377
+ // 多选问题:有勾选才给提交按钮(单选在协调器内即时提交)。
378
+ if (q.multiSelect && selected.length > 0) {
379
+ elements.push({
380
+ tag: 'button',
381
+ text: { tag: 'plain_text', content: `✅ 提交答案(已选 ${selected.length} 项)` },
382
+ type: 'primary',
383
+ behaviors: [
384
+ {
385
+ type: 'callback',
386
+ value: {
387
+ cmd: 'approval.answerSubmit',
388
+ requestId: approval.requestId,
389
+ questionIndex: i,
390
+ runId,
391
+ nonce: mkNonce(),
392
+ },
393
+ },
394
+ ],
395
+ });
396
+ }
397
+ }
398
+ // 提问卡底部统一「跳过回答」:走现有 approval.respond + decision=decline
399
+ // (coordinator 对 question 允许 decline 作为安全兜底),runner 按协议映射
400
+ // 跳过语义(Claude deny / Codex 空 answers / Kimi action decline / Pi cancelled)。
401
+ if (!expired) {
402
+ elements.push({
403
+ tag: 'button',
404
+ text: { tag: 'plain_text', content: '⏭️ 跳过回答' },
405
+ type: 'default',
406
+ behaviors: [
407
+ {
408
+ type: 'callback',
409
+ value: {
410
+ cmd: 'approval.respond',
411
+ decision: 'decline',
412
+ requestId: approval.requestId,
413
+ runId,
414
+ nonce: mkNonce(),
415
+ },
416
+ },
417
+ ],
243
418
  });
244
419
  }
245
420
  return elements;
246
421
  }
422
+ /**
423
+ * 选项行:左按钮右描述(固定 100px 按钮列保证对齐),单选/多选图标区分。
424
+ * 与 renderAnswerInput 分开以便自由文本题不渲染选项行。
425
+ */
426
+ function renderOptionRow(requestId, questionIndex, option, isSelected, icon, runId) {
427
+ return {
428
+ tag: 'column_set',
429
+ flex_mode: 'none',
430
+ columns: [
431
+ {
432
+ tag: 'column',
433
+ width: 'auto',
434
+ vertical_align: 'center',
435
+ elements: [
436
+ {
437
+ tag: 'button',
438
+ text: { tag: 'plain_text', content: isSelected ? '取消选择' : '选择' },
439
+ type: isSelected ? 'default' : 'primary',
440
+ size: 'small',
441
+ // 固定宽度让所有选项行的按钮左对齐(描述长短不影响按钮列)。
442
+ width: '100px',
443
+ behaviors: [
444
+ {
445
+ type: 'callback',
446
+ value: {
447
+ cmd: 'approval.answer',
448
+ requestId,
449
+ questionIndex,
450
+ option: option.label,
451
+ runId,
452
+ nonce: mkNonce(),
453
+ },
454
+ },
455
+ ],
456
+ },
457
+ ],
458
+ },
459
+ {
460
+ tag: 'column',
461
+ width: 'weighted',
462
+ weight: 1,
463
+ vertical_align: 'center',
464
+ elements: [
465
+ {
466
+ tag: 'div',
467
+ text: {
468
+ tag: 'lark_md',
469
+ content: `${icon} ${option.label}${option.description ? `\n_${option.description}_` : ''}`,
470
+ },
471
+ },
472
+ ],
473
+ },
474
+ ],
475
+ };
476
+ }
477
+ /**
478
+ * 文本输入行(Other 输入与自由文本题共用):输入后点击右侧 ✓ 提交图标,
479
+ * input_value 经 connector raw 事件回传(CardKit input 的 input_value 会被
480
+ * SDK normalizer 丢弃,必须走 includeRawEvent 路径)。
481
+ */
482
+ function renderAnswerInput(requestId, questionIndex, placeholder, runId, cmd = 'approval.answerCustom') {
483
+ // name 必须含 cmd,保证同一题同时渲染多个输入框(如单选选项题的
484
+ // Other 输入 + Note 输入)时元素 name 唯一——飞书对同卡内重复 name
485
+ // 报 ErrCode 11310 拒绝整卡(2026-08-19 线上:name(answer-custom-0-0) duplicate)。
486
+ const kind = cmd === 'approval.answerNote' ? 'note' : 'custom';
487
+ return {
488
+ tag: 'input',
489
+ name: `answer-${kind}-${requestId}-${questionIndex}`,
490
+ placeholder: { tag: 'plain_text', content: placeholder },
491
+ behaviors: [
492
+ {
493
+ type: 'callback',
494
+ value: {
495
+ cmd,
496
+ requestId,
497
+ questionIndex,
498
+ runId,
499
+ nonce: mkNonce(),
500
+ },
501
+ },
502
+ ],
503
+ };
504
+ }
247
505
  //# sourceMappingURL=approval-render.js.map
@@ -1,4 +1,4 @@
1
- import { CardSession, type CardChannel, type StreamSettleResult } from './card-session.js';
1
+ import { CardSession, type CardChannel } from './card-session.js';
2
2
  import { type BashState, type BashRenderOptions } from './bash-renderer.js';
3
3
  /**
4
4
  * Store-time 输出上限(字符,P1-3 层①)。§P1-3 建议「store-time 截断,
@@ -23,18 +23,6 @@ export declare function capBashOutput(value: string): string;
23
23
  * independent cards.
24
24
  */
25
25
  export declare class BashCardSession extends CardSession<BashState, BashRenderOptions> {
26
- /**
27
- * P1-3 层③:push 合批窗口(ms),与 RunCardSession 同语义。连续 update 各触发
28
- * 一次全卡 render + patch 是 PATCH 风暴主源;窗口内多个事件复用同一次延迟 flush。
29
- * 窗口大小可通过 constructor 覆盖(测试用小窗口/0 禁用)。
30
- */
31
- private readonly coalesceMs;
32
- private flushTimer;
33
- private flushInFlight;
34
- /** in-flight flush 期间到达的 update 标记:flush 完成后需重新调度(防 stale 帧)。 */
35
- private pendingReschedule;
36
- /** 当前 in-flight flush 的 promise:finish 在终态 patch 前 await 它保证顺序。 */
37
- private flushP;
38
26
  constructor(opts: {
39
27
  connector: CardChannel;
40
28
  chatId: string;
@@ -51,6 +39,7 @@ export declare class BashCardSession extends CardSession<BashState, BashRenderOp
51
39
  protected get errorPrefix(): string;
52
40
  protected renderCard(state: BashState, options: BashRenderOptions): object;
53
41
  protected hasOwnBudgetProtection(): boolean;
42
+ protected isFlushableState(): boolean;
54
43
  /** Patch the card with updated bash state (e.g. new stdout/stderr). */
55
44
  update(patch: Partial<BashState>): Promise<void>;
56
45
  /** Transition to a terminal state and patch the card. */
@@ -59,17 +48,6 @@ export declare class BashCardSession extends CardSession<BashState, BashRenderOp
59
48
  stderr?: string;
60
49
  output?: string;
61
50
  }): Promise<void>;
62
- /**
63
- * 调度一次延迟 flush:已有 pending 调度则复用(合批核心);flushInFlight 期间
64
- * 不再起 timer(防重入叠加),但置 pendingReschedule 让 in-flight 完成后补渲染。
65
- */
66
- private scheduleFlush;
67
- /** 执行一次 render + updateCard(合批窗口到期或 finish 即时 flush 时调用)。 */
68
- private flush;
69
- /** 取消待 flush 的合批调度(finish/settle 前清理,避免延迟 update 与终态竞争)。 */
70
- private cancelPendingFlush;
71
- /** settle 前兜底取消合批调度,避免 release 后 dangling timer 更新已释放的 stream。 */
72
- settle(): Promise<StreamSettleResult>;
73
51
  /**
74
52
  * 合并 patch 并对 output/stderr 做 store-time 截断(P1-3 层①)。
75
53
  * 截断发生在入口(store 时),渲染层无需再为大 state 付代价。
@@ -1,5 +1,6 @@
1
1
  import { CardSession } from './card-session.js';
2
2
  import { renderBashCard } from './bash-renderer.js';
3
+ import { keepTail } from '../common/truncate.js';
3
4
  /**
4
5
  * Store-time 输出上限(字符,P1-3 层①)。§P1-3 建议「store-time 截断,
5
6
  * 保留尾部,上限如 24~64KB」——取区间下界 24_000 字符(字符上限 ×4 字节最坏
@@ -8,10 +9,6 @@ import { renderBashCard } from './bash-renderer.js';
8
9
  * state 与 bridge 局部变量。
9
10
  */
10
11
  export const BASH_OUTPUT_STORE_CAP = 24_000;
11
- /** 字符级 tail 保留(与 run-state.keepLatest 的 slice(-maxChars) 语义一致)。 */
12
- function keepTail(value, maxChars) {
13
- return value.length <= maxChars ? value : value.slice(-maxChars);
14
- }
15
12
  /**
16
13
  * Store-time 截断入口(P1-3 层①/层④共用)。bridge 本地 output/stderr 与 session
17
14
  * state 都走它,保证任意路径驻留的字符串 ≤ BASH_OUTPUT_STORE_CAP 字符。
@@ -29,18 +26,6 @@ export function capBashOutput(value) {
29
26
  * independent cards.
30
27
  */
31
28
  export class BashCardSession extends CardSession {
32
- /**
33
- * P1-3 层③:push 合批窗口(ms),与 RunCardSession 同语义。连续 update 各触发
34
- * 一次全卡 render + patch 是 PATCH 风暴主源;窗口内多个事件复用同一次延迟 flush。
35
- * 窗口大小可通过 constructor 覆盖(测试用小窗口/0 禁用)。
36
- */
37
- coalesceMs;
38
- flushTimer;
39
- flushInFlight = false;
40
- /** in-flight flush 期间到达的 update 标记:flush 完成后需重新调度(防 stale 帧)。 */
41
- pendingReschedule = false;
42
- /** 当前 in-flight flush 的 promise:finish 在终态 patch 前 await 它保证顺序。 */
43
- flushP;
44
29
  constructor(opts) {
45
30
  super({
46
31
  runId: opts.runId,
@@ -56,8 +41,8 @@ export class BashCardSession extends CardSession {
56
41
  renderOptions: opts.renderOptions,
57
42
  startTimeoutMs: opts.startTimeoutMs,
58
43
  settleTimeoutMs: opts.settleTimeoutMs,
44
+ coalesceMs: opts.coalesceMs,
59
45
  });
60
- this.coalesceMs = opts.coalesceMs ?? 100;
61
46
  }
62
47
  get logPrefix() {
63
48
  return '[bash-card]';
@@ -76,6 +61,10 @@ export class BashCardSession extends CardSession {
76
61
  // 卡应跳过它,靠自身 renderer 保护。
77
62
  return true;
78
63
  }
64
+ isFlushableState() {
65
+ // finish() 已把 state 转终态后,in-flight/延迟 flush 不再用旧 state patch。
66
+ return this.state.terminal === 'running';
67
+ }
79
68
  /** Patch the card with updated bash state (e.g. new stdout/stderr). */
80
69
  async update(patch) {
81
70
  this.state = this.applyCap(patch);
@@ -102,62 +91,6 @@ export class BashCardSession extends CardSession {
102
91
  await this.updateCard();
103
92
  this.release();
104
93
  }
105
- /**
106
- * 调度一次延迟 flush:已有 pending 调度则复用(合批核心);flushInFlight 期间
107
- * 不再起 timer(防重入叠加),但置 pendingReschedule 让 in-flight 完成后补渲染。
108
- */
109
- scheduleFlush() {
110
- if (this.flushTimer)
111
- return;
112
- if (this.flushInFlight) {
113
- this.pendingReschedule = true;
114
- return;
115
- }
116
- this.flushTimer = setTimeout(() => {
117
- this.flushTimer = undefined;
118
- void this.flush();
119
- }, this.coalesceMs);
120
- }
121
- /** 执行一次 render + updateCard(合批窗口到期或 finish 即时 flush 时调用)。 */
122
- async flush() {
123
- if (this.flushInFlight)
124
- return;
125
- // 终态守卫:finish 已把 state 转终态后,in-flight/延迟 flush 不再用旧 state patch
126
- if (this.state.terminal !== 'running')
127
- return;
128
- this.flushInFlight = true;
129
- const flushPromise = (async () => {
130
- try {
131
- await this.updateCard();
132
- }
133
- finally {
134
- this.flushInFlight = false;
135
- this.flushP = undefined;
136
- // in-flight 期间若有 update 到达(pendingReschedule),重新调度一次渲染
137
- if (this.pendingReschedule) {
138
- this.pendingReschedule = false;
139
- this.scheduleFlush();
140
- }
141
- }
142
- })();
143
- this.flushP = flushPromise;
144
- return flushPromise;
145
- }
146
- /** 取消待 flush 的合批调度(finish/settle 前清理,避免延迟 update 与终态竞争)。 */
147
- cancelPendingFlush() {
148
- this.pendingReschedule = false;
149
- if (this.flushTimer) {
150
- clearTimeout(this.flushTimer);
151
- this.flushTimer = undefined;
152
- return true;
153
- }
154
- return false;
155
- }
156
- /** settle 前兜底取消合批调度,避免 release 后 dangling timer 更新已释放的 stream。 */
157
- async settle() {
158
- this.cancelPendingFlush();
159
- return super.settle();
160
- }
161
94
  /**
162
95
  * 合并 patch 并对 output/stderr 做 store-time 截断(P1-3 层①)。
163
96
  * 截断发生在入口(store 时),渲染层无需再为大 state 付代价。
@@ -1,11 +1,10 @@
1
1
  import { terminalToColor, terminalToLabel, stopButton } from './card-shared.js';
2
2
  import { markdownDiv } from './collapsible.js';
3
- import { truncateUtf8 } from './text-truncate.js';
3
+ import { truncateUtf8, CARD_BUDGET_BYTES } from './text-truncate.js';
4
4
  /**
5
5
  * 飞书卡片大小限制(安全阈值,与 run-renderer 一致)。
6
- * 本地定义,不从 card-budget.ts 导入(避免流式/静态耦合)。
6
+ * 从共享的 text-truncate.ts 导入,避免三处各自定义后数值漂移。
7
7
  */
8
- const CARD_BUDGET_BYTES = 28_000;
9
8
  /**
10
9
  * 单个输出字段的默认最大字节数(保守值,用于正常路径)。
11
10
  * 飞书卡片限制约 28KB,留余量给卡片其他部分(header、status row、footer)