lark-remote 0.4.5 → 0.5.1
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 +34 -0
- package/dist/bridge/approval-coordinator.d.ts +1 -1
- package/dist/bridge/approval-coordinator.js +3 -3
- package/dist/bridge/inbound-media.js +10 -7
- package/dist/bridge/index.d.ts +55 -17
- package/dist/bridge/index.js +145 -74
- package/dist/bridge/queue-manager.d.ts +71 -20
- package/dist/bridge/queue-manager.js +139 -57
- package/dist/card/approval-render.d.ts +1 -1
- package/dist/card/approval-render.js +6 -6
- package/dist/card/bash-card-session.d.ts +3 -3
- package/dist/card/bash-card-session.js +3 -3
- package/dist/card/bash-renderer.js +1 -1
- package/dist/card/card-session.d.ts +4 -4
- package/dist/card/card-session.js +6 -6
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.d.ts +1 -1
- package/dist/card/run-renderer.js +14 -14
- package/dist/card/run-state.d.ts +2 -2
- package/dist/card/run-state.js +3 -3
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +6 -6
- package/dist/cli.js +2 -1
- package/dist/clone.d.ts +5 -3
- package/dist/clone.js +8 -4
- package/dist/common/truncate.d.ts +1 -1
- package/dist/common/truncate.js +2 -2
- package/dist/common/ttl-cache.d.ts +2 -2
- package/dist/common/ttl-cache.js +2 -2
- package/dist/config/codex-config.d.ts +9 -9
- package/dist/config/codex-config.js +18 -18
- package/dist/config/dir.js +5 -2
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +6 -6
- package/dist/config/opencode-config.js +3 -3
- package/dist/connector/index.d.ts +5 -5
- package/dist/connector/index.js +24 -11
- package/dist/inbound/event.d.ts +1 -1
- package/dist/inbound/event.js +1 -1
- package/dist/inbound/placeholder.d.ts +2 -2
- package/dist/inbound/placeholder.js +2 -2
- package/dist/index.js +23 -12
- package/dist/instance-lock.js +4 -3
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.js +4 -4
- package/dist/persistence/atomic-write.d.ts +1 -1
- package/dist/persistence/atomic-write.js +6 -2
- package/dist/platform/agent-stopper.d.ts +1 -1
- package/dist/platform/identity.d.ts +7 -1
- package/dist/platform/identity.js +2 -2
- package/dist/platform/path.d.ts +6 -0
- package/dist/platform/path.js +14 -8
- package/dist/platform/select.d.ts +1 -1
- package/dist/platform/select.js +1 -1
- package/dist/platform/shell.d.ts +2 -3
- package/dist/platform/spawn.d.ts +2 -2
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/terminator-posix.js +1 -1
- package/dist/platform/terminator-win32.js +2 -2
- package/dist/platform/terminator.d.ts +3 -3
- package/dist/platform/terminator.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/restart.d.ts +1 -1
- package/dist/restart.js +5 -3
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +2 -2
- package/dist/router/config/opencode.js +1 -1
- package/dist/router/index.d.ts +65 -16
- package/dist/router/index.js +141 -47
- package/dist/router/order-exec-dispatch.js +2 -2
- package/dist/runner/bash/runner.d.ts +2 -2
- package/dist/runner/bash/runner.js +6 -6
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/claude/session.d.ts +2 -2
- package/dist/runner/claude/session.js +14 -14
- package/dist/runner/codex/app-server/runner.d.ts +28 -0
- package/dist/runner/codex/app-server/runner.js +57 -4
- package/dist/runner/codex/app-server/translator.d.ts +2 -2
- package/dist/runner/codex/app-server/translator.js +5 -5
- package/dist/runner/common/acp/acp-approval.d.ts +2 -2
- package/dist/runner/common/acp/acp-approval.js +3 -3
- package/dist/runner/common/acp/base-acp-runner.js +1 -1
- package/dist/runner/common/acp/base-translator.d.ts +3 -3
- package/dist/runner/common/acp/base-translator.js +4 -4
- package/dist/runner/common/acp/protocol-types.js +1 -1
- package/dist/runner/common/connection-based-runner.d.ts +1 -1
- package/dist/runner/common/connection-based-runner.js +2 -2
- package/dist/runner/common/jsonl-stream.d.ts +2 -2
- package/dist/runner/common/jsonl-stream.js +12 -12
- package/dist/runner/common/jsonrpc/client.d.ts +1 -1
- package/dist/runner/common/jsonrpc/client.js +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +2 -2
- package/dist/runner/common/spawning-runner.d.ts +21 -8
- package/dist/runner/common/spawning-runner.js +41 -15
- package/dist/runner/dsh/runner.js +2 -2
- package/dist/runner/kimi/acp/runner.d.ts +3 -3
- package/dist/runner/kimi/acp/runner.js +9 -9
- package/dist/runner/kimi/acp/translator.d.ts +2 -2
- package/dist/runner/kimi/acp/translator.js +4 -4
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +3 -3
- package/dist/runner/opencode/acp/translator.js +1 -1
- package/dist/runner/pi/rpc/runner.js +2 -2
- package/dist/runner/pi/rpc/translator.d.ts +1 -1
- package/dist/runner/pi/rpc/translator.js +4 -4
- package/dist/runner/types.d.ts +11 -2
- package/dist/runner/types.js +12 -1
- package/dist/session/claude/session-index.d.ts +12 -0
- package/dist/session/claude/session-index.js +20 -5
- package/dist/session/claude/sessions.js +10 -10
- package/dist/session/codex/rollout-reader.d.ts +4 -4
- package/dist/session/codex/rollout-reader.js +12 -12
- package/dist/session/common/two-pass.js +2 -2
- package/dist/session/common/usage-accumulator.d.ts +2 -2
- package/dist/session/common/usage-accumulator.js +2 -2
- package/dist/session/dsh/sessions.js +2 -2
- package/dist/session/kimi/sessions.d.ts +2 -2
- package/dist/session/kimi/sessions.js +11 -11
- package/dist/session/opencode/sessions.d.ts +3 -3
- package/dist/session/opencode/sessions.js +12 -10
- package/dist/session/pi/sessions.js +13 -12
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AgentKind } from '../runner/types.js';
|
|
2
2
|
/**
|
|
3
3
|
* 入队时刻快照:当前 defaultAgent + 该 agent 的 sessionId(无 session 则 undefined)。
|
|
4
|
-
*
|
|
5
|
-
* 执行时刻,避免 /new、/config 在排队期间改写 live
|
|
4
|
+
* 唯一捕获点:排队消息在入队时刻把 agent+session 钉进 AgentBinding,随任务闭包带到执行时刻
|
|
5
|
+
* 执行时刻,避免 /new、/config 在排队期间改写 live 状态导致语义漂移。
|
|
6
6
|
*/
|
|
7
7
|
export interface AgentBinding {
|
|
8
8
|
agent: AgentKind;
|
|
@@ -51,9 +51,32 @@ export interface QueuedTask {
|
|
|
51
51
|
* `handleQueueImmediate`).
|
|
52
52
|
*/
|
|
53
53
|
editedMessage?: string;
|
|
54
|
-
/** 入队时刻捕获的 agent+session
|
|
54
|
+
/** 入队时刻捕获的 agent+session 绑定,随任务闭包带到执行时刻。 */
|
|
55
55
|
binding?: AgentBinding;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* lane 条目的展示类型。只影响排队卡如何描述「正在执行的是什么」:
|
|
59
|
+
* `message` 普通消息(缺省)、`compact` 压缩会话、`command` 其他卡片动作。
|
|
60
|
+
*/
|
|
61
|
+
export type LaneTaskKind = 'message' | 'compact' | 'command';
|
|
62
|
+
/**
|
|
63
|
+
* lane 当前占用者的展示信息。由 begin 路径登记、settle 路径清除,与执行
|
|
64
|
+
* 槽位同寿命;排队卡靠它回答「我在等谁」。
|
|
65
|
+
*/
|
|
66
|
+
export interface ExecutingInfo {
|
|
67
|
+
/**
|
|
68
|
+
* 归属标记:与执行槽位共用同一个 slot id。清除时校验它,防止一条迟到的
|
|
69
|
+
* settle 清掉后继任务的占用者(身份不用外部 id,同 §9.24 口径)。
|
|
70
|
+
*/
|
|
71
|
+
slotId: number;
|
|
72
|
+
kind: LaneTaskKind;
|
|
73
|
+
/** 展示标签,例如 '💬 消息' / '🗜 Compact' / '📋 config.save'。 */
|
|
74
|
+
label: string;
|
|
75
|
+
/** 该条目自己的排队预览(可选)。 */
|
|
76
|
+
preview?: string;
|
|
77
|
+
/** 登记时刻(epoch ms)。当前只供诊断,卡片不渲染实时时长。 */
|
|
78
|
+
startedAt: number;
|
|
79
|
+
}
|
|
57
80
|
/** Options for enqueue operation */
|
|
58
81
|
export interface EnqueueOptions {
|
|
59
82
|
taskMeta?: {
|
|
@@ -70,8 +93,14 @@ export interface EnqueueOptions {
|
|
|
70
93
|
feishuReplyTo?: string;
|
|
71
94
|
/** Whether the queued task is editable (queue card ✏️ 编辑 button). Defaults to true. */
|
|
72
95
|
editable?: boolean;
|
|
73
|
-
/** 入队时刻捕获的 agent+session
|
|
96
|
+
/** 入队时刻捕获的 agent+session 绑定。 */
|
|
74
97
|
binding?: AgentBinding;
|
|
98
|
+
/**
|
|
99
|
+
* lane 占用者的展示类型,缺省 'message'。
|
|
100
|
+
* 'compact' 时排队卡写明「正在执行: 🗜 Compact」,并把「立即执行」置灰
|
|
101
|
+
* ——该按钮会中断压缩(见 buildQueueActionButtons)。
|
|
102
|
+
*/
|
|
103
|
+
kind?: LaneTaskKind;
|
|
75
104
|
};
|
|
76
105
|
}
|
|
77
106
|
/** Queue info for a cwd */
|
|
@@ -90,7 +119,7 @@ export declare class QueueManager {
|
|
|
90
119
|
/** Per-workspace queued tasks for queue display cards. Key = cwd, value = task list. */
|
|
91
120
|
private queuedTasks;
|
|
92
121
|
/**
|
|
93
|
-
*
|
|
122
|
+
* per-workspace `messageId → QueuedTask` index mirroring `queuedTasks`.
|
|
94
123
|
* O(1) lookup for the task-start removal path (replacing the old double
|
|
95
124
|
* `find`+`findIndex` O(N) scan) and for `getQueuedTask`/`removeFromQueue`/
|
|
96
125
|
* `updateQueuedTaskMessage`. The ordered array remains the source of truth
|
|
@@ -115,18 +144,23 @@ export declare class QueueManager {
|
|
|
115
144
|
/**
|
|
116
145
|
* Monotonic counter minting a unique slot id per enqueued task. A slot
|
|
117
146
|
* identifies one task's execution period for the interrupt bookkeeping in
|
|
118
|
-
* `
|
|
147
|
+
* `executingInfo`/`interruptedSlots` — replacing the unowned skip-credit
|
|
119
148
|
* counter, which any settle could consume and which therefore leaked a
|
|
120
|
-
* re-armed count after repeated resets
|
|
149
|
+
* re-armed count after repeated resets.
|
|
121
150
|
*/
|
|
122
151
|
private slotCounter;
|
|
123
152
|
/**
|
|
124
|
-
* Per-workspace
|
|
125
|
-
* path right before the task runs; deleted
|
|
126
|
-
*
|
|
127
|
-
* the interrupted task's slot
|
|
153
|
+
* Per-workspace record of the task currently executing — the lane occupant.
|
|
154
|
+
* Set in the begin path right before the task runs; deleted by that task's
|
|
155
|
+
* settle. Carries both the slot id (`resetExecutingCount` binds the
|
|
156
|
+
* interrupt credit to exactly the interrupted task's slot) and the display
|
|
157
|
+
* info the queue card needs to name what a waiting message waits for.
|
|
158
|
+
*
|
|
159
|
+
* 2026-10-09 线上:一次 Compact 占着 lane,新消息的排队卡却写「位置: 第 1
|
|
160
|
+
* 位 / 前面还有: 0 条消息」——开卡判据看 lane(含正在执行者),位置文案只看
|
|
161
|
+
* 等待队列,两个口径的差集正是这条记录。
|
|
128
162
|
*/
|
|
129
|
-
private
|
|
163
|
+
private executingInfo;
|
|
130
164
|
/**
|
|
131
165
|
* Per-workspace set of slot ids whose task was interrupted by
|
|
132
166
|
* `resetExecutingCount`. When such a slot settles, its decrement is skipped
|
|
@@ -141,7 +175,7 @@ export declare class QueueManager {
|
|
|
141
175
|
/**
|
|
142
176
|
* MessageIds of tasks that have actually begun executing (cancellation check
|
|
143
177
|
* passed, task removed from the queue). Entries are sticky: they remain
|
|
144
|
-
* after the task settles
|
|
178
|
+
* after the task settles. `handleQueueImmediate` step 6 uses this to
|
|
145
179
|
* distinguish "target was cancelled (never began)" from "target began while
|
|
146
180
|
* the interrupt was in flight" so the final feedback can report the true
|
|
147
181
|
* state instead of telling a running task's user that nothing was
|
|
@@ -162,11 +196,11 @@ export declare class QueueManager {
|
|
|
162
196
|
constructor(isWorkspaceRunning: (cwd: string) => boolean, sendCard: (chatId: string, card: object, opts?: {
|
|
163
197
|
replyTo?: string;
|
|
164
198
|
}) => Promise<string>, updateCard: (messageId: string, card: object) => Promise<void>);
|
|
165
|
-
/**
|
|
199
|
+
/** register a queued task in the per-workspace `messageId → task` index. */
|
|
166
200
|
private indexAdd;
|
|
167
|
-
/**
|
|
201
|
+
/** drop a task from the per-workspace index by messageId. */
|
|
168
202
|
private indexRemove;
|
|
169
|
-
/**
|
|
203
|
+
/** O(1) lookup of a queued task by messageId via the index. */
|
|
170
204
|
private indexGet;
|
|
171
205
|
/**
|
|
172
206
|
* Enqueue a task into the workspace-level serial queue.
|
|
@@ -184,6 +218,12 @@ export declare class QueueManager {
|
|
|
184
218
|
* after it). Consumed when the task's slot begins; removed if cancelled.
|
|
185
219
|
*/
|
|
186
220
|
setTaskReplacement(cwd: string, messageId: string, task: () => Promise<void>): void;
|
|
221
|
+
/**
|
|
222
|
+
* 排队卡头部。只有前面确实还有等待中的消息时才说「排队」;前面为空时
|
|
223
|
+
* 用户等的是当前正在执行的任务(例如 Compact),用「等待当前任务结束」
|
|
224
|
+
* 表达才不会与「前面还有 0 条」自相矛盾(2026-10-09 线上实例)。
|
|
225
|
+
*/
|
|
226
|
+
private queueCardHeader;
|
|
187
227
|
/** Send a queue status card showing current queue position and actions. */
|
|
188
228
|
private sendQueueStatusCard;
|
|
189
229
|
/** Get queue info for a cwd. */
|
|
@@ -198,7 +238,11 @@ export declare class QueueManager {
|
|
|
198
238
|
* state — pending cards enable both, executing/cancelled cards disable
|
|
199
239
|
* both. Centralizing the pair here eliminates the 3-way duplication
|
|
200
240
|
* between `buildQueueStatusCardElements`, `updateQueueCardToExecuting`,
|
|
201
|
-
* and `updateQueueCardToCancelled` (Clean Code
|
|
241
|
+
* and `updateQueueCardToCancelled` (Clean Code).
|
|
242
|
+
*
|
|
243
|
+
* `opts.immediateLabel`(仅 pending 卡用)在 lane 被 Compact 占用时替换
|
|
244
|
+
* 「⚡ 立即执行」并强制置灰:点击它会中断正在跑的压缩。「❌ 撤销」不参与,
|
|
245
|
+
* 它只移除等待中的消息,不触碰 Compact。
|
|
202
246
|
*/
|
|
203
247
|
private buildQueueActionButtons;
|
|
204
248
|
/**
|
|
@@ -251,7 +295,7 @@ export declare class QueueManager {
|
|
|
251
295
|
* id minted at enqueue time. Three cases:
|
|
252
296
|
*
|
|
253
297
|
* 1. The slot is the workspace's current executing slot → its execution
|
|
254
|
-
* period ends (`
|
|
298
|
+
* period ends (`executingInfo` record removed).
|
|
255
299
|
* 2. The slot is in `interruptedSlots` (its task was interrupted by
|
|
256
300
|
* `resetExecutingCount`) → skip the decrement: the reset already zeroed
|
|
257
301
|
* the count, so a stale decrement could wrongly zero it again while a
|
|
@@ -279,16 +323,23 @@ export declare class QueueManager {
|
|
|
279
323
|
* `expectedSlot` binds the reset to the task that was actually interrupted:
|
|
280
324
|
* the caller captures the executing slot BEFORE stopping the runner, and
|
|
281
325
|
* the stop window may outlive the interrupted task's settle (the chain can
|
|
282
|
-
* advance to a NEW task, which then owns `
|
|
326
|
+
* advance to a NEW task, which then owns `executingInfo`). When the current
|
|
283
327
|
* slot no longer matches the interrupted task's slot, the interrupted task
|
|
284
328
|
* already decremented normally — resetting now would zero the count of the
|
|
285
329
|
* running successor and mark ITS slot interrupted, hiding it from the
|
|
286
|
-
* queue card
|
|
330
|
+
* queue card.
|
|
287
331
|
*/
|
|
288
332
|
resetExecutingCount(cwd: string, expectedSlot: number): void;
|
|
289
333
|
/**
|
|
290
334
|
* The slot id of the task currently executing in this cwd (if any).
|
|
291
335
|
*/
|
|
292
336
|
getExecutingSlot(cwd: string): number | undefined;
|
|
337
|
+
/**
|
|
338
|
+
* The lane occupant's display info for this cwd (if any). Queue cards read
|
|
339
|
+
* it to name what a waiting message is waiting for; `handleQueueImmediate`
|
|
340
|
+
* reads it to refuse interrupting a running Compact. Returns a copy so
|
|
341
|
+
* callers cannot mutate the live record.
|
|
342
|
+
*/
|
|
343
|
+
getExecutingInfo(cwd: string): ExecutingInfo | undefined;
|
|
293
344
|
}
|
|
294
345
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getLogger } from '../logger/index.js';
|
|
2
2
|
import { displayName } from '../platform/path.js';
|
|
3
3
|
/**
|
|
4
|
-
* Per-workspace cap on the number of tasks WAITING to execute
|
|
4
|
+
* Per-workspace cap on the number of tasks WAITING to execute.
|
|
5
5
|
* Without a bound, a message flood grows `queuedTasks` / the promise chain /
|
|
6
6
|
* each task's captured `msg.content` closure without limit, and every enqueued
|
|
7
7
|
* task fires a queue card (amplifying outbound API calls). When the waiting
|
|
@@ -11,6 +11,18 @@ import { displayName } from '../platform/path.js';
|
|
|
11
11
|
* backlog, not throughput. Owner is a single user, so 50 is ample headroom.
|
|
12
12
|
*/
|
|
13
13
|
const MAX_WAITING_QUEUE = 50;
|
|
14
|
+
/** 排队卡「正在执行」一行用的展示标签。 */
|
|
15
|
+
function laneOccupantLabel(kind, preview) {
|
|
16
|
+
switch (kind) {
|
|
17
|
+
case 'compact':
|
|
18
|
+
return '🗜 Compact';
|
|
19
|
+
case 'command':
|
|
20
|
+
// 卡片动作的预览形如 'card action: config.save',标签里去掉前缀更短。
|
|
21
|
+
return `📋 ${preview.replace(/^card action:\s*/, '')}`;
|
|
22
|
+
default:
|
|
23
|
+
return '💬 消息';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
14
26
|
/**
|
|
15
27
|
* QueueManager handles the per-workspace serial processing queue.
|
|
16
28
|
* Manages task queuing, cancellation, and queue status card rendering.
|
|
@@ -21,7 +33,7 @@ export class QueueManager {
|
|
|
21
33
|
/** Per-workspace queued tasks for queue display cards. Key = cwd, value = task list. */
|
|
22
34
|
queuedTasks = new Map();
|
|
23
35
|
/**
|
|
24
|
-
*
|
|
36
|
+
* per-workspace `messageId → QueuedTask` index mirroring `queuedTasks`.
|
|
25
37
|
* O(1) lookup for the task-start removal path (replacing the old double
|
|
26
38
|
* `find`+`findIndex` O(N) scan) and for `getQueuedTask`/`removeFromQueue`/
|
|
27
39
|
* `updateQueuedTaskMessage`. The ordered array remains the source of truth
|
|
@@ -46,18 +58,23 @@ export class QueueManager {
|
|
|
46
58
|
/**
|
|
47
59
|
* Monotonic counter minting a unique slot id per enqueued task. A slot
|
|
48
60
|
* identifies one task's execution period for the interrupt bookkeeping in
|
|
49
|
-
* `
|
|
61
|
+
* `executingInfo`/`interruptedSlots` — replacing the unowned skip-credit
|
|
50
62
|
* counter, which any settle could consume and which therefore leaked a
|
|
51
|
-
* re-armed count after repeated resets
|
|
63
|
+
* re-armed count after repeated resets.
|
|
52
64
|
*/
|
|
53
65
|
slotCounter = 0;
|
|
54
66
|
/**
|
|
55
|
-
* Per-workspace
|
|
56
|
-
* path right before the task runs; deleted
|
|
57
|
-
*
|
|
58
|
-
* the interrupted task's slot
|
|
67
|
+
* Per-workspace record of the task currently executing — the lane occupant.
|
|
68
|
+
* Set in the begin path right before the task runs; deleted by that task's
|
|
69
|
+
* settle. Carries both the slot id (`resetExecutingCount` binds the
|
|
70
|
+
* interrupt credit to exactly the interrupted task's slot) and the display
|
|
71
|
+
* info the queue card needs to name what a waiting message waits for.
|
|
72
|
+
*
|
|
73
|
+
* 2026-10-09 线上:一次 Compact 占着 lane,新消息的排队卡却写「位置: 第 1
|
|
74
|
+
* 位 / 前面还有: 0 条消息」——开卡判据看 lane(含正在执行者),位置文案只看
|
|
75
|
+
* 等待队列,两个口径的差集正是这条记录。
|
|
59
76
|
*/
|
|
60
|
-
|
|
77
|
+
executingInfo = new Map();
|
|
61
78
|
/**
|
|
62
79
|
* Per-workspace set of slot ids whose task was interrupted by
|
|
63
80
|
* `resetExecutingCount`. When such a slot settles, its decrement is skipped
|
|
@@ -72,7 +89,7 @@ export class QueueManager {
|
|
|
72
89
|
/**
|
|
73
90
|
* MessageIds of tasks that have actually begun executing (cancellation check
|
|
74
91
|
* passed, task removed from the queue). Entries are sticky: they remain
|
|
75
|
-
* after the task settles
|
|
92
|
+
* after the task settles. `handleQueueImmediate` step 6 uses this to
|
|
76
93
|
* distinguish "target was cancelled (never began)" from "target began while
|
|
77
94
|
* the interrupt was in flight" so the final feedback can report the true
|
|
78
95
|
* state instead of telling a running task's user that nothing was
|
|
@@ -95,7 +112,7 @@ export class QueueManager {
|
|
|
95
112
|
this.sendCard = sendCard;
|
|
96
113
|
this.updateCard = updateCard;
|
|
97
114
|
}
|
|
98
|
-
/**
|
|
115
|
+
/** register a queued task in the per-workspace `messageId → task` index. */
|
|
99
116
|
indexAdd(cwd, task) {
|
|
100
117
|
let idx = this.taskIndex.get(cwd);
|
|
101
118
|
if (!idx) {
|
|
@@ -104,11 +121,11 @@ export class QueueManager {
|
|
|
104
121
|
}
|
|
105
122
|
idx.set(task.messageId, task);
|
|
106
123
|
}
|
|
107
|
-
/**
|
|
124
|
+
/** drop a task from the per-workspace index by messageId. */
|
|
108
125
|
indexRemove(cwd, messageId) {
|
|
109
126
|
this.taskIndex.get(cwd)?.delete(messageId);
|
|
110
127
|
}
|
|
111
|
-
/**
|
|
128
|
+
/** O(1) lookup of a queued task by messageId via the index. */
|
|
112
129
|
indexGet(cwd, messageId) {
|
|
113
130
|
return this.taskIndex.get(cwd)?.get(messageId);
|
|
114
131
|
}
|
|
@@ -147,7 +164,7 @@ export class QueueManager {
|
|
|
147
164
|
const currentExecutingCount = this.pendingOrExecutingCount.get(cwd) ?? 0;
|
|
148
165
|
const currentQueueLength = this.queuedTasks.get(cwd)?.length ?? 0;
|
|
149
166
|
const hasWaitingTasks = currentExecutingCount > 0 || currentQueueLength > 0;
|
|
150
|
-
//
|
|
167
|
+
// bound the waiting backlog. When the queue is full, reject the
|
|
151
168
|
// task with a visible card instead of appending it (which would grow
|
|
152
169
|
// the promise chain + closures unboundedly under a message flood and
|
|
153
170
|
// fire another queue card). Do NOT increment pendingOrExecutingCount —
|
|
@@ -183,6 +200,14 @@ export class QueueManager {
|
|
|
183
200
|
taskList = [];
|
|
184
201
|
this.queuedTasks.set(cwd, taskList);
|
|
185
202
|
}
|
|
203
|
+
// 身份不变量 tripwire:同一 cwd 内 queue key 必须唯一(一次入队动作一个
|
|
204
|
+
// key,铸造点 router.mintQueueTaskKey)。撞 key 时数组(追加)与 taskIndex
|
|
205
|
+
// (覆盖)必然漂移,位置显示/撤销/立即执行/卡片更新会串到别的条目上——
|
|
206
|
+
// 2026-10-08 线上缺陷就是这么长出来的。这里不改行为,只留证据。
|
|
207
|
+
if (this.indexGet(cwd, queuedTask.messageId)) {
|
|
208
|
+
getLogger().warn(`[queue-manager] duplicate queue key cwd=${cwd} messageId=${queuedTask.messageId} — ` +
|
|
209
|
+
'queue identity must be minted per enqueue action (see router.mintQueueTaskKey)');
|
|
210
|
+
}
|
|
186
211
|
taskList.push(queuedTask);
|
|
187
212
|
this.indexAdd(cwd, queuedTask);
|
|
188
213
|
// Increment executing count SYNCHRONOUSLY so subsequent enqueues see it
|
|
@@ -217,7 +242,7 @@ export class QueueManager {
|
|
|
217
242
|
let livePreview = taskMeta?.messagePreview ?? '';
|
|
218
243
|
// Check if this task was cancelled before executing
|
|
219
244
|
if (messageId) {
|
|
220
|
-
//
|
|
245
|
+
// O(1) index lookup replaces the old `find` + `findIndex`
|
|
221
246
|
// double O(N) scan. The ordered array still drives removal (splice
|
|
222
247
|
// preserves queue order for position display); the index stays in
|
|
223
248
|
// sync via indexRemove.
|
|
@@ -281,16 +306,24 @@ export class QueueManager {
|
|
|
281
306
|
this.pendingOrExecutingCount.set(cwd, 1);
|
|
282
307
|
getLogger().debug(`[queue-manager] re-armed pendingOrExecutingCount cwd=${cwd} (interrupt resume)`);
|
|
283
308
|
}
|
|
284
|
-
// Mark this task
|
|
285
|
-
// it. `resetExecutingCount` reads
|
|
286
|
-
// credit to exactly this task; the settle removes the
|
|
287
|
-
|
|
309
|
+
// Mark this task as the lane occupant for its whole execution period
|
|
310
|
+
// before running it. `resetExecutingCount` reads the slot id to grant
|
|
311
|
+
// the interrupt credit to exactly this task; the settle removes the
|
|
312
|
+
// record. `kind`/`label` let a later message's queue card name what it
|
|
313
|
+
// is waiting for instead of reporting "前面还有 0 条".
|
|
314
|
+
this.executingInfo.set(cwd, {
|
|
315
|
+
slotId,
|
|
316
|
+
kind: taskMeta?.kind ?? 'message',
|
|
317
|
+
label: laneOccupantLabel(taskMeta?.kind ?? 'message', livePreview),
|
|
318
|
+
preview: livePreview,
|
|
319
|
+
startedAt: Date.now(),
|
|
320
|
+
});
|
|
288
321
|
return replacement ? replacement() : task();
|
|
289
322
|
})
|
|
290
323
|
.then(() => {
|
|
291
324
|
getLogger().debug(`[queue-manager] task end cwd=${cwd}`);
|
|
292
325
|
// beganMessageIds entry intentionally retained: sticky by design
|
|
293
|
-
//
|
|
326
|
+
// the marker records "has ever begun", not "is currently
|
|
294
327
|
// running", so a task that began and settled quickly still reports
|
|
295
328
|
// "已开始执行" to queue.immediate.
|
|
296
329
|
this.decrementExecutingCount(cwd, slotId);
|
|
@@ -298,7 +331,7 @@ export class QueueManager {
|
|
|
298
331
|
.catch((err) => {
|
|
299
332
|
getLogger().error('[queue-manager] queue task error:', err);
|
|
300
333
|
// Same sticky rationale as the success settle: the marker survives
|
|
301
|
-
// even when the task errors after beginning
|
|
334
|
+
// even when the task errors after beginning.
|
|
302
335
|
this.decrementExecutingCount(cwd, slotId);
|
|
303
336
|
});
|
|
304
337
|
this.queues.set(cwd, newQueue);
|
|
@@ -325,6 +358,20 @@ export class QueueManager {
|
|
|
325
358
|
workspaceReplacements.set(messageId, task);
|
|
326
359
|
getLogger().debug(`[queue-manager] set task replacement cwd=${cwd} messageId=${messageId}`);
|
|
327
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* 排队卡头部。只有前面确实还有等待中的消息时才说「排队」;前面为空时
|
|
363
|
+
* 用户等的是当前正在执行的任务(例如 Compact),用「等待当前任务结束」
|
|
364
|
+
* 表达才不会与「前面还有 0 条」自相矛盾(2026-10-09 线上实例)。
|
|
365
|
+
*/
|
|
366
|
+
queueCardHeader(tasksAhead) {
|
|
367
|
+
return {
|
|
368
|
+
template: 'orange',
|
|
369
|
+
title: {
|
|
370
|
+
tag: 'plain_text',
|
|
371
|
+
content: tasksAhead > 0 ? '⏳ 消息排队中' : '⏳ 等待当前任务结束',
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
328
375
|
/** Send a queue status card showing current queue position and actions. */
|
|
329
376
|
async sendQueueStatusCard(cwd, chatId, replyToMessageId, messagePreview,
|
|
330
377
|
/** Feishu reply target when `replyToMessageId` (the queue key) is an
|
|
@@ -338,10 +385,7 @@ export class QueueManager {
|
|
|
338
385
|
const card = {
|
|
339
386
|
schema: '2.0',
|
|
340
387
|
config: { wide_screen_mode: true },
|
|
341
|
-
header:
|
|
342
|
-
template: 'orange',
|
|
343
|
-
title: { tag: 'plain_text', content: '⏳ 消息排队中' },
|
|
344
|
-
},
|
|
388
|
+
header: this.queueCardHeader(tasksAhead),
|
|
345
389
|
body: {
|
|
346
390
|
elements: this.buildQueueStatusCardElements(cwd, actualPosition, tasksAhead, replyToMessageId, messagePreview),
|
|
347
391
|
},
|
|
@@ -379,25 +423,50 @@ export class QueueManager {
|
|
|
379
423
|
/** Build queue status card elements. */
|
|
380
424
|
buildQueueStatusCardElements(cwd, actualPosition, tasksAhead, messageId, messagePreview) {
|
|
381
425
|
const workspaceName = displayName(cwd);
|
|
382
|
-
const
|
|
426
|
+
const occupant = this.executingInfo.get(cwd);
|
|
383
427
|
const elements = [
|
|
384
428
|
{
|
|
429
|
+
tag: 'div',
|
|
430
|
+
text: { tag: 'lark_md', content: `**当前 Workspace:** \`${workspaceName}\`` },
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
// 「队列位置 / 前面还有」只在前面确实还有等待中的消息时渲染。前面为空时
|
|
434
|
+
// 这两个数是「第 1 位 / 0 条」,与「排队中」字面矛盾;此时挡住用户的只有
|
|
435
|
+
// 正在执行的任务,交给下一段如实说明。
|
|
436
|
+
if (tasksAhead > 0) {
|
|
437
|
+
elements.push({
|
|
385
438
|
tag: 'div',
|
|
386
439
|
text: {
|
|
387
440
|
tag: 'lark_md',
|
|
388
|
-
content:
|
|
441
|
+
content: `**队列位置:** 第 ${actualPosition} 位\n**前面还有:** ${tasksAhead} 条消息`,
|
|
389
442
|
},
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (occupant) {
|
|
446
|
+
elements.push({
|
|
447
|
+
tag: 'div',
|
|
448
|
+
text: { tag: 'lark_md', content: `**正在执行:** ${occupant.label}` },
|
|
449
|
+
});
|
|
450
|
+
elements.push({
|
|
393
451
|
tag: 'div',
|
|
394
452
|
text: {
|
|
395
453
|
tag: 'lark_md',
|
|
396
|
-
content:
|
|
454
|
+
content: occupant.kind === 'compact'
|
|
455
|
+
? '💡 压缩结束后将按顺序执行你的消息'
|
|
456
|
+
: '💡 消息将按顺序执行…',
|
|
397
457
|
},
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
// 防御分支:开卡判据是 lane 非空,所以「有排队卡但没有占用者」说明计数
|
|
462
|
+
// 与占用者记录漂移了。不改行为,只留证据(与 duplicate queue key 同口径)。
|
|
463
|
+
getLogger().warn(`[queue-manager] queue card without lane occupant cwd=${cwd} tasksAhead=${tasksAhead}`);
|
|
464
|
+
elements.push({
|
|
465
|
+
tag: 'div',
|
|
466
|
+
text: { tag: 'lark_md', content: '💡 消息将按顺序执行…' },
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
elements.push({ tag: 'hr' });
|
|
401
470
|
// Show message preview with edit button (only if not executing AND the
|
|
402
471
|
// task is editable). One-shot card actions like Compact enqueue with
|
|
403
472
|
// editable=false: their preview is not user-editable text, so the ✏️ 编辑
|
|
@@ -423,8 +492,10 @@ export class QueueManager {
|
|
|
423
492
|
}
|
|
424
493
|
elements.push({ tag: 'hr' });
|
|
425
494
|
}
|
|
426
|
-
// Action buttons
|
|
427
|
-
|
|
495
|
+
// Action buttons. Compact 占着 lane 时「⚡ 立即执行」会中断压缩
|
|
496
|
+
// (interruptCurrentRun 遍历 activeRuns,压缩也注册在里面),按钮置灰并
|
|
497
|
+
// 换文案说明;执行层 handleQueueImmediate 再做一次同样的守卫兜住旧卡片。
|
|
498
|
+
elements.push(...this.buildQueueActionButtons(cwd, messageId, false, occupant?.kind === 'compact' ? { immediateLabel: '压缩完成后按顺序执行' } : undefined));
|
|
428
499
|
return elements;
|
|
429
500
|
}
|
|
430
501
|
/**
|
|
@@ -435,9 +506,13 @@ export class QueueManager {
|
|
|
435
506
|
* state — pending cards enable both, executing/cancelled cards disable
|
|
436
507
|
* both. Centralizing the pair here eliminates the 3-way duplication
|
|
437
508
|
* between `buildQueueStatusCardElements`, `updateQueueCardToExecuting`,
|
|
438
|
-
* and `updateQueueCardToCancelled` (Clean Code
|
|
509
|
+
* and `updateQueueCardToCancelled` (Clean Code).
|
|
510
|
+
*
|
|
511
|
+
* `opts.immediateLabel`(仅 pending 卡用)在 lane 被 Compact 占用时替换
|
|
512
|
+
* 「⚡ 立即执行」并强制置灰:点击它会中断正在跑的压缩。「❌ 撤销」不参与,
|
|
513
|
+
* 它只移除等待中的消息,不触碰 Compact。
|
|
439
514
|
*/
|
|
440
|
-
buildQueueActionButtons(cwd, messageId, disabled) {
|
|
515
|
+
buildQueueActionButtons(cwd, messageId, disabled, opts) {
|
|
441
516
|
return [
|
|
442
517
|
{
|
|
443
518
|
tag: 'button',
|
|
@@ -448,9 +523,9 @@ export class QueueManager {
|
|
|
448
523
|
},
|
|
449
524
|
{
|
|
450
525
|
tag: 'button',
|
|
451
|
-
text: { tag: 'plain_text', content: '⚡ 立即执行' },
|
|
526
|
+
text: { tag: 'plain_text', content: opts?.immediateLabel ?? '⚡ 立即执行' },
|
|
452
527
|
type: 'primary',
|
|
453
|
-
disabled,
|
|
528
|
+
disabled: disabled || opts?.immediateLabel !== undefined,
|
|
454
529
|
behaviors: [{ type: 'callback', value: { cmd: 'queue.immediate', cwd, messageId } }],
|
|
455
530
|
},
|
|
456
531
|
];
|
|
@@ -476,8 +551,8 @@ export class QueueManager {
|
|
|
476
551
|
try {
|
|
477
552
|
// Prefer the live preview: an edited task's messagePreview is updated in
|
|
478
553
|
// place, and the passed-in snapshot may be stale if the card send was in
|
|
479
|
-
// flight while the user edited
|
|
480
|
-
// passes the live preview captured at begin
|
|
554
|
+
// flight while the user edited. The begin path (started=true) already
|
|
555
|
+
// passes the live preview captured at begin, so the fallback is only
|
|
481
556
|
// exercised when the task is no longer queued.
|
|
482
557
|
const liveTask = this.indexGet(cwd, messageId);
|
|
483
558
|
const stillQueued = liveTask !== undefined;
|
|
@@ -527,7 +602,7 @@ export class QueueManager {
|
|
|
527
602
|
const taskList = this.queuedTasks.get(cwd);
|
|
528
603
|
if (!taskList)
|
|
529
604
|
return false;
|
|
530
|
-
//
|
|
605
|
+
// index is the O(1) presence check; array splice keeps order.
|
|
531
606
|
if (!this.indexGet(cwd, messageId))
|
|
532
607
|
return false;
|
|
533
608
|
const index = taskList.findIndex((t) => t.messageId === messageId);
|
|
@@ -582,7 +657,7 @@ export class QueueManager {
|
|
|
582
657
|
}
|
|
583
658
|
/** Get task metadata from queue. */
|
|
584
659
|
getQueuedTask(cwd, messageId) {
|
|
585
|
-
//
|
|
660
|
+
// O(1) index lookup instead of array `find`.
|
|
586
661
|
return this.indexGet(cwd, messageId);
|
|
587
662
|
}
|
|
588
663
|
/**
|
|
@@ -600,7 +675,7 @@ export class QueueManager {
|
|
|
600
675
|
}
|
|
601
676
|
/** Update the messagePreview for a queued task. Returns true if found and updated. */
|
|
602
677
|
updateQueuedTaskMessage(cwd, messageId, newMessage) {
|
|
603
|
-
//
|
|
678
|
+
// O(1) index lookup instead of array `find`.
|
|
604
679
|
const task = this.indexGet(cwd, messageId);
|
|
605
680
|
if (!task)
|
|
606
681
|
return false;
|
|
@@ -633,10 +708,7 @@ export class QueueManager {
|
|
|
633
708
|
return {
|
|
634
709
|
schema: '2.0',
|
|
635
710
|
config: { wide_screen_mode: true },
|
|
636
|
-
header:
|
|
637
|
-
template: 'orange',
|
|
638
|
-
title: { tag: 'plain_text', content: '⏳ 消息排队中' },
|
|
639
|
-
},
|
|
711
|
+
header: this.queueCardHeader(tasksAhead),
|
|
640
712
|
body: {
|
|
641
713
|
elements: this.buildQueueStatusCardElements(cwd, actualPosition, tasksAhead, messageId, newMessagePreview),
|
|
642
714
|
},
|
|
@@ -650,7 +722,7 @@ export class QueueManager {
|
|
|
650
722
|
* id minted at enqueue time. Three cases:
|
|
651
723
|
*
|
|
652
724
|
* 1. The slot is the workspace's current executing slot → its execution
|
|
653
|
-
* period ends (`
|
|
725
|
+
* period ends (`executingInfo` record removed).
|
|
654
726
|
* 2. The slot is in `interruptedSlots` (its task was interrupted by
|
|
655
727
|
* `resetExecutingCount`) → skip the decrement: the reset already zeroed
|
|
656
728
|
* the count, so a stale decrement could wrongly zero it again while a
|
|
@@ -658,8 +730,8 @@ export class QueueManager {
|
|
|
658
730
|
* 3. Otherwise → normal decrement.
|
|
659
731
|
*/
|
|
660
732
|
decrementExecutingCount(cwd, slotId) {
|
|
661
|
-
if (this.
|
|
662
|
-
this.
|
|
733
|
+
if (this.executingInfo.get(cwd)?.slotId === slotId) {
|
|
734
|
+
this.executingInfo.delete(cwd);
|
|
663
735
|
}
|
|
664
736
|
const interrupted = this.interruptedSlots.get(cwd);
|
|
665
737
|
if (interrupted?.has(slotId)) {
|
|
@@ -693,19 +765,19 @@ export class QueueManager {
|
|
|
693
765
|
* `expectedSlot` binds the reset to the task that was actually interrupted:
|
|
694
766
|
* the caller captures the executing slot BEFORE stopping the runner, and
|
|
695
767
|
* the stop window may outlive the interrupted task's settle (the chain can
|
|
696
|
-
* advance to a NEW task, which then owns `
|
|
768
|
+
* advance to a NEW task, which then owns `executingInfo`). When the current
|
|
697
769
|
* slot no longer matches the interrupted task's slot, the interrupted task
|
|
698
770
|
* already decremented normally — resetting now would zero the count of the
|
|
699
771
|
* running successor and mark ITS slot interrupted, hiding it from the
|
|
700
|
-
* queue card
|
|
772
|
+
* queue card.
|
|
701
773
|
*/
|
|
702
774
|
resetExecutingCount(cwd, expectedSlot) {
|
|
703
|
-
const currentSlot = this.
|
|
775
|
+
const currentSlot = this.executingInfo.get(cwd)?.slotId;
|
|
704
776
|
if (currentSlot !== expectedSlot) {
|
|
705
777
|
getLogger().debug(`[queue-manager] resetExecutingCount skip (stopped task settled, slot advanced) cwd=${cwd} expectedSlot=${expectedSlot} currentSlot=${currentSlot ?? 'none'}`);
|
|
706
778
|
return;
|
|
707
779
|
}
|
|
708
|
-
//
|
|
780
|
+
// only reset and grant an interrupt slot when there is actually a
|
|
709
781
|
// pending/executing task.
|
|
710
782
|
const currentCount = this.pendingOrExecutingCount.get(cwd) ?? 0;
|
|
711
783
|
if (currentCount === 0) {
|
|
@@ -733,7 +805,17 @@ export class QueueManager {
|
|
|
733
805
|
* The slot id of the task currently executing in this cwd (if any).
|
|
734
806
|
*/
|
|
735
807
|
getExecutingSlot(cwd) {
|
|
736
|
-
return this.
|
|
808
|
+
return this.executingInfo.get(cwd)?.slotId;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* The lane occupant's display info for this cwd (if any). Queue cards read
|
|
812
|
+
* it to name what a waiting message is waiting for; `handleQueueImmediate`
|
|
813
|
+
* reads it to refuse interrupting a running Compact. Returns a copy so
|
|
814
|
+
* callers cannot mutate the live record.
|
|
815
|
+
*/
|
|
816
|
+
getExecutingInfo(cwd) {
|
|
817
|
+
const info = this.executingInfo.get(cwd);
|
|
818
|
+
return info ? { ...info } : undefined;
|
|
737
819
|
}
|
|
738
820
|
}
|
|
739
821
|
//# sourceMappingURL=queue-manager.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* renderApprovalArea: renders the approval request area in a run card.
|
|
3
3
|
*
|
|
4
|
-
* Renders command/file/permissions approval areas with button mappings
|
|
4
|
+
* Renders command/file/permissions approval areas with button mappings.
|
|
5
5
|
* Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
|
|
6
6
|
*/
|
|
7
7
|
import type { ApprovalView } from '../runner/types.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* renderApprovalArea: renders the approval request area in a run card.
|
|
3
3
|
*
|
|
4
|
-
* Renders command/file/permissions approval areas with button mappings
|
|
4
|
+
* Renders command/file/permissions approval areas with button mappings.
|
|
5
5
|
* Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
|
|
6
6
|
*/
|
|
7
7
|
import { collapsibleMarkdownPanel } from './collapsible.js';
|
|
@@ -13,7 +13,7 @@ function mkNonce() {
|
|
|
13
13
|
}
|
|
14
14
|
/** 计划面板正文字节预算(折叠展示 + 截断保护 28KB 卡片预算)。 */
|
|
15
15
|
const PLAN_PANEL_BYTES = 6000;
|
|
16
|
-
/** reason div(3 处共用);反引号中和防 lark_md
|
|
16
|
+
/** reason div(3 处共用);反引号中和防 lark_md 解析错误。 */
|
|
17
17
|
function reasonDiv(approval) {
|
|
18
18
|
if (!approval.reason)
|
|
19
19
|
return null;
|
|
@@ -39,7 +39,7 @@ export function renderApprovalArea(approval, opts) {
|
|
|
39
39
|
if (!approval)
|
|
40
40
|
return [];
|
|
41
41
|
// 终态(done/error/interrupted/idle_timeout)下 run 已结束:coordinator 已
|
|
42
|
-
// 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"
|
|
42
|
+
// 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"。
|
|
43
43
|
if (opts?.terminal && opts.terminal !== 'running' && opts.terminal !== 'finalizing') {
|
|
44
44
|
return [];
|
|
45
45
|
}
|
|
@@ -238,7 +238,7 @@ function renderPlanFeedbackInput(requestId, runId) {
|
|
|
238
238
|
function renderCommandApproval(approval, expired, runId) {
|
|
239
239
|
const elements = [];
|
|
240
240
|
// Command display
|
|
241
|
-
//
|
|
241
|
+
// 命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
|
|
242
242
|
// 反引号极易触发 11311 解析错误导致整卡失败(同 renderFileDiff 的中和策略)。
|
|
243
243
|
const neutralizedCommand = (approval.command ?? '').replace(/`/g, '·');
|
|
244
244
|
const cmdContent = neutralizedCommand
|
|
@@ -422,7 +422,7 @@ function renderQuestionApproval(approval, expired, runId) {
|
|
|
422
422
|
elements.push(renderOptionRow(approval.requestId, i, option, isSelected, icon, runId));
|
|
423
423
|
}
|
|
424
424
|
// 自定义答案(Other)的选中态展示:选项按钮无法表示自由文本,单独显示
|
|
425
|
-
//
|
|
425
|
+
// 已选文本,避免「点了没反应」的困惑。
|
|
426
426
|
const customSelected = q.selected?.find((s) => !q.options.some((o) => o.label === s));
|
|
427
427
|
if (customSelected) {
|
|
428
428
|
elements.push({
|
|
@@ -430,7 +430,7 @@ function renderQuestionApproval(approval, expired, runId) {
|
|
|
430
430
|
text: { tag: 'lark_md', content: `✍️ 自定义答案:${customSelected}` },
|
|
431
431
|
});
|
|
432
432
|
}
|
|
433
|
-
//
|
|
433
|
+
// 单选问题提供自定义答案(Other)输入——输入文本后提交
|
|
434
434
|
// (移动端键盘回车/完成键,桌面端输入框右侧提交图标),input_value 经
|
|
435
435
|
// connector raw 事件回传。
|
|
436
436
|
// 显隐按 isOther !== false:Kimi form 会丢弃非声明选项值,翻译时置
|