lark-remote 0.1.5 → 0.1.6
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 +17 -0
- package/README.en.md +0 -4
- package/README.md +0 -4
- package/dist/bridge/approval-coordinator.d.ts +108 -0
- package/dist/bridge/approval-coordinator.js +258 -0
- package/dist/bridge/index.d.ts +62 -0
- package/dist/bridge/index.js +402 -21
- package/dist/bridge/queue-manager.d.ts +9 -0
- package/dist/bridge/queue-manager.js +19 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +247 -0
- package/dist/card/card-shared.d.ts +14 -0
- package/dist/card/card-shared.js +28 -0
- package/dist/card/run-renderer.js +77 -6
- package/dist/card/run-state.d.ts +25 -3
- package/dist/card/run-state.js +203 -2
- package/dist/config/dir.js +1 -1
- package/dist/config/index.d.ts +42 -0
- package/dist/config/index.js +23 -1
- package/dist/connector/index.d.ts +5 -0
- package/dist/connector/index.js +12 -0
- package/dist/index.js +44 -22
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +49 -0
- package/dist/router/config/common/render.js +4 -0
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/index.d.ts +27 -2
- package/dist/router/index.js +109 -28
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +5 -2
- package/dist/runner/codex/app-server/client.d.ts +82 -0
- package/dist/runner/codex/app-server/client.js +234 -0
- package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
- package/dist/runner/codex/app-server/connection-manager.js +170 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
- package/dist/runner/codex/app-server/protocol-types.js +67 -0
- package/dist/runner/codex/app-server/runner.d.ts +121 -0
- package/dist/runner/codex/app-server/runner.js +554 -0
- package/dist/runner/codex/app-server/translator.d.ts +109 -0
- package/dist/runner/codex/app-server/translator.js +734 -0
- package/dist/runner/codex/app-server/transport.d.ts +49 -0
- package/dist/runner/codex/app-server/transport.js +231 -0
- package/dist/runner/codex/factory.d.ts +29 -0
- package/dist/runner/codex/factory.js +39 -0
- package/dist/runner/codex/index.d.ts +2 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/runner.d.ts +3 -0
- package/dist/runner/codex/runner.js +5 -0
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +1 -1
- package/dist/runner/types.d.ts +123 -2
- package/dist/session/codex/rollout-reader.js +43 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.6] - 2026-08-14
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- Codex App Server 集成:通过 stdio JSON-RPC 持久连接 Codex CLI,支持审批流(文件读写/命令执行)、Compact 上下文压缩、空闲超时自动断开
|
|
13
|
+
- Approval 审批系统:`approval-coordinator` 协调超时/取消/回复,`approval-render` 渲染审批按钮卡片
|
|
14
|
+
- Codex runner 工厂:自动选择直连模式或 App Server 模式
|
|
15
|
+
- `rollout-reader`:Codex App Server rollout 开关读取
|
|
16
|
+
- Run card 审批区域渲染 + context limit 百分比显示
|
|
17
|
+
- 架构文档:`codex-app-server-integration.md`
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
|
|
21
|
+
- `/update` 命令移除自动重启逻辑
|
|
22
|
+
- Codex 配置枚举回归官方标准值
|
|
23
|
+
|
|
8
24
|
## [0.1.5] - 2026-08-14
|
|
9
25
|
|
|
10
26
|
### 新增
|
|
@@ -136,3 +152,4 @@ Initial release.
|
|
|
136
152
|
[0.1.3]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.3
|
|
137
153
|
[0.1.4]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.4
|
|
138
154
|
[0.1.5]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.5
|
|
155
|
+
[0.1.6]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.6
|
package/README.en.md
CHANGED
|
@@ -153,10 +153,6 @@ Full docs under [`docs/`](docs/):
|
|
|
153
153
|
|
|
154
154
|
> This package is a CLI tool only — no programmatic API is exposed.
|
|
155
155
|
|
|
156
|
-
## Acknowledgments
|
|
157
|
-
|
|
158
|
-
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.
|
|
159
|
-
|
|
160
156
|
## License
|
|
161
157
|
|
|
162
158
|
[MIT](LICENSE) © bungabungawoda
|
package/README.md
CHANGED
|
@@ -152,10 +152,6 @@ FEISHU_LIVE_TEST=1 bun run test tests/feishu-reaction-emoji-live.test.ts
|
|
|
152
152
|
|
|
153
153
|
> 本包为纯 CLI 工具,不提供编程 API。
|
|
154
154
|
|
|
155
|
-
## 致谢
|
|
156
|
-
|
|
157
|
-
本项目受到了 [lark-coding-agent-bridge](https://github.com/zarazhangrui/lark-coding-agent-bridge) 的启发,感谢该项目展示了飞书与本地 Coding Agent 桥接的可行性。
|
|
158
|
-
|
|
159
155
|
## License
|
|
160
156
|
|
|
161
157
|
[MIT](LICENSE) © bungabungawoda
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApprovalCoordinator: tracks and manages approval requests for a single run.
|
|
3
|
+
*
|
|
4
|
+
* Handles the lifecycle of command/file/permissions approval requests:
|
|
5
|
+
* - onRequested: add to tracking, arm timeout timer
|
|
6
|
+
* - onResolved: update state, clear timer
|
|
7
|
+
* - submit: validate → atomic state transition → async respond
|
|
8
|
+
* - togglePerm: flip permission item selection
|
|
9
|
+
* - onTurnEnded / onConnectionLost: mark all as expired
|
|
10
|
+
*/
|
|
11
|
+
import type { ApprovalRequestedEvent, ApprovalView, AgentEvent } from '../runner/types.js';
|
|
12
|
+
export type ApprovalState = 'pending' | 'submitting' | 'resolved' | 'failed' | 'expired';
|
|
13
|
+
export interface TrackedApproval {
|
|
14
|
+
requestId: number | string;
|
|
15
|
+
kind: 'command' | 'file' | 'permissions';
|
|
16
|
+
state: ApprovalState;
|
|
17
|
+
view: ApprovalView;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
timeoutTimer: ReturnType<typeof setTimeout> | null;
|
|
20
|
+
}
|
|
21
|
+
export type ApprovalResponder = (requestId: number | string, response: unknown) => Promise<void>;
|
|
22
|
+
export type ApprovalAction = {
|
|
23
|
+
action: 'accept';
|
|
24
|
+
} | {
|
|
25
|
+
action: 'accept_for_session';
|
|
26
|
+
} | {
|
|
27
|
+
action: 'accept_with_execpolicy_amendment';
|
|
28
|
+
} | {
|
|
29
|
+
action: 'decline';
|
|
30
|
+
} | {
|
|
31
|
+
action: 'cancel';
|
|
32
|
+
};
|
|
33
|
+
export interface ApprovalToggleAction {
|
|
34
|
+
permId: string;
|
|
35
|
+
selected: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare class ApprovalCoordinator {
|
|
38
|
+
private approvals;
|
|
39
|
+
/** 已提交过的 nonce(按 requestId):同一按钮连点/飞书重投递只生效一次。 */
|
|
40
|
+
private submittedNonces;
|
|
41
|
+
private readonly runId;
|
|
42
|
+
private readonly userId;
|
|
43
|
+
private readonly chatId;
|
|
44
|
+
private readonly workspace;
|
|
45
|
+
private readonly approvalTimeoutMs;
|
|
46
|
+
private readonly responder;
|
|
47
|
+
private readonly interruptTurn;
|
|
48
|
+
private readonly pushToCard;
|
|
49
|
+
constructor(opts: {
|
|
50
|
+
runId: string;
|
|
51
|
+
userId: string;
|
|
52
|
+
chatId: string;
|
|
53
|
+
workspace: string;
|
|
54
|
+
approvalTimeoutMs: number;
|
|
55
|
+
responder: ApprovalResponder;
|
|
56
|
+
interruptTurn: () => Promise<void>;
|
|
57
|
+
pushToCard: (events: AgentEvent[]) => Promise<void>;
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Handle an approval_requested event.
|
|
61
|
+
*/
|
|
62
|
+
onRequested(event: ApprovalRequestedEvent): void;
|
|
63
|
+
/**
|
|
64
|
+
* Handle an approval_resolved event.
|
|
65
|
+
*/
|
|
66
|
+
onResolved(requestId: number | string): void;
|
|
67
|
+
/**
|
|
68
|
+
* Handle turn ended — mark all pending approvals as expired.
|
|
69
|
+
*/
|
|
70
|
+
onTurnEnded(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Handle connection lost — same as onTurnEnded.
|
|
73
|
+
*/
|
|
74
|
+
onConnectionLost(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Submit an approval decision.
|
|
77
|
+
* Returns a promise that resolves when the response is sent.
|
|
78
|
+
*/
|
|
79
|
+
submit(action: ApprovalAction, ctx: {
|
|
80
|
+
requestId: number | string;
|
|
81
|
+
nonce?: string;
|
|
82
|
+
}): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Toggle a permission item's selected state.
|
|
85
|
+
*/
|
|
86
|
+
togglePerm(action: ApprovalToggleAction, ctx: {
|
|
87
|
+
requestId: number | string;
|
|
88
|
+
}): Promise<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Update a pending approval's view (out-of-order item/started arrives late).
|
|
91
|
+
* Only applies while the approval is still pending — resolved/expired
|
|
92
|
+
* approvals must not be resurrected or rewritten.
|
|
93
|
+
*/
|
|
94
|
+
updateView(requestId: number | string, view: ApprovalView): void;
|
|
95
|
+
/**
|
|
96
|
+
* Get the number of pending approvals.
|
|
97
|
+
*/
|
|
98
|
+
pendingCount(): number;
|
|
99
|
+
/**
|
|
100
|
+
* Get the head (oldest pending) approval, or undefined.
|
|
101
|
+
*/
|
|
102
|
+
head(): TrackedApproval | undefined;
|
|
103
|
+
private clearTimer;
|
|
104
|
+
private expireApproval;
|
|
105
|
+
private actionToDecision;
|
|
106
|
+
}
|
|
107
|
+
/** Map a card decision string to the ApprovalAction used across the bridge. */
|
|
108
|
+
export declare function decisionToApprovalAction(decision: string): ApprovalAction;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApprovalCoordinator: tracks and manages approval requests for a single run.
|
|
3
|
+
*
|
|
4
|
+
* Handles the lifecycle of command/file/permissions approval requests:
|
|
5
|
+
* - onRequested: add to tracking, arm timeout timer
|
|
6
|
+
* - onResolved: update state, clear timer
|
|
7
|
+
* - submit: validate → atomic state transition → async respond
|
|
8
|
+
* - togglePerm: flip permission item selection
|
|
9
|
+
* - onTurnEnded / onConnectionLost: mark all as expired
|
|
10
|
+
*/
|
|
11
|
+
import { getLogger } from '../logger/index.js';
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Coordinator
|
|
14
|
+
// =============================================================================
|
|
15
|
+
export class ApprovalCoordinator {
|
|
16
|
+
approvals = new Map();
|
|
17
|
+
/** 已提交过的 nonce(按 requestId):同一按钮连点/飞书重投递只生效一次。 */
|
|
18
|
+
submittedNonces = new Map();
|
|
19
|
+
runId;
|
|
20
|
+
userId;
|
|
21
|
+
chatId;
|
|
22
|
+
workspace;
|
|
23
|
+
approvalTimeoutMs;
|
|
24
|
+
responder;
|
|
25
|
+
interruptTurn;
|
|
26
|
+
pushToCard;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
this.runId = opts.runId;
|
|
29
|
+
this.userId = opts.userId;
|
|
30
|
+
this.chatId = opts.chatId;
|
|
31
|
+
this.workspace = opts.workspace;
|
|
32
|
+
this.approvalTimeoutMs = opts.approvalTimeoutMs;
|
|
33
|
+
this.responder = opts.responder;
|
|
34
|
+
this.interruptTurn = opts.interruptTurn;
|
|
35
|
+
this.pushToCard = opts.pushToCard;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Handle an approval_requested event.
|
|
39
|
+
*/
|
|
40
|
+
onRequested(event) {
|
|
41
|
+
const existing = this.approvals.get(event.requestId);
|
|
42
|
+
if (existing) {
|
|
43
|
+
// Already tracked — update state
|
|
44
|
+
existing.state = 'pending';
|
|
45
|
+
existing.view = event.view;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
this.expireApproval(event.requestId);
|
|
50
|
+
}, this.approvalTimeoutMs);
|
|
51
|
+
const tracked = {
|
|
52
|
+
requestId: event.requestId,
|
|
53
|
+
kind: event.kind,
|
|
54
|
+
state: 'pending',
|
|
55
|
+
view: event.view,
|
|
56
|
+
createdAt: Date.now(),
|
|
57
|
+
timeoutTimer: timer,
|
|
58
|
+
};
|
|
59
|
+
this.approvals.set(event.requestId, tracked);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Handle an approval_resolved event.
|
|
63
|
+
*/
|
|
64
|
+
onResolved(requestId) {
|
|
65
|
+
const tracked = this.approvals.get(requestId);
|
|
66
|
+
if (!tracked)
|
|
67
|
+
return;
|
|
68
|
+
tracked.state = 'resolved';
|
|
69
|
+
this.clearTimer(tracked);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Handle turn ended — mark all pending approvals as expired.
|
|
73
|
+
*/
|
|
74
|
+
onTurnEnded() {
|
|
75
|
+
for (const [, tracked] of this.approvals) {
|
|
76
|
+
if (tracked.state === 'pending') {
|
|
77
|
+
tracked.state = 'expired';
|
|
78
|
+
this.clearTimer(tracked);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Handle connection lost — same as onTurnEnded.
|
|
84
|
+
*/
|
|
85
|
+
onConnectionLost() {
|
|
86
|
+
this.onTurnEnded();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Submit an approval decision.
|
|
90
|
+
* Returns a promise that resolves when the response is sent.
|
|
91
|
+
*/
|
|
92
|
+
async submit(action, ctx) {
|
|
93
|
+
const tracked = this.approvals.get(ctx.requestId);
|
|
94
|
+
if (!tracked) {
|
|
95
|
+
throw new Error(`Approval request ${ctx.requestId} not found`);
|
|
96
|
+
}
|
|
97
|
+
// nonce 去重:同一张卡片渲染出的按钮连续点击(或飞书重投递)携带相同
|
|
98
|
+
// nonce,只允许生效一次;不同渲染的按钮 nonce 不同,状态机(pending →
|
|
99
|
+
// submitting)仍然兜底防二次提交。
|
|
100
|
+
if (ctx.nonce !== undefined) {
|
|
101
|
+
const seen = this.submittedNonces.get(ctx.requestId) ?? new Set();
|
|
102
|
+
if (seen.has(ctx.nonce)) {
|
|
103
|
+
throw new Error(`Approval request ${ctx.requestId} already submitted (duplicate nonce)`);
|
|
104
|
+
}
|
|
105
|
+
seen.add(ctx.nonce);
|
|
106
|
+
this.submittedNonces.set(ctx.requestId, seen);
|
|
107
|
+
}
|
|
108
|
+
if (tracked.state !== 'pending') {
|
|
109
|
+
throw new Error(`Approval request ${ctx.requestId} is no longer pending (state=${tracked.state})`);
|
|
110
|
+
}
|
|
111
|
+
// Validate action against available decisions. decline/cancel are always
|
|
112
|
+
// allowed as a safety override: denying/interrupting is a universal human
|
|
113
|
+
// affordance even when the server's list omits them (observed in practice).
|
|
114
|
+
const decision = this.actionToDecision(action);
|
|
115
|
+
const safetyOverride = decision === 'decline' || decision === 'cancel';
|
|
116
|
+
if (!tracked.view.availableDecisions.includes(decision) && !safetyOverride) {
|
|
117
|
+
throw new Error(`Decision "${decision}" not in available decisions: ${tracked.view.availableDecisions.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
// Atomic state transition: pending → submitting(同步,无 await)
|
|
120
|
+
tracked.state = 'submitting';
|
|
121
|
+
this.clearTimer(tracked);
|
|
122
|
+
// Send response via the responder
|
|
123
|
+
try {
|
|
124
|
+
await this.responder(ctx.requestId, action);
|
|
125
|
+
tracked.state = 'resolved';
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
tracked.state = 'failed';
|
|
129
|
+
}
|
|
130
|
+
return `Approval ${tracked.requestId} (${tracked.kind}): ${decision}`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Toggle a permission item's selected state.
|
|
134
|
+
*/
|
|
135
|
+
async togglePerm(action, ctx) {
|
|
136
|
+
const tracked = this.approvals.get(ctx.requestId);
|
|
137
|
+
if (!tracked) {
|
|
138
|
+
throw new Error(`Approval request ${ctx.requestId} not found`);
|
|
139
|
+
}
|
|
140
|
+
if (tracked.kind !== 'permissions') {
|
|
141
|
+
throw new Error(`Approval request ${ctx.requestId} is not a permissions request`);
|
|
142
|
+
}
|
|
143
|
+
if (tracked.state !== 'pending') {
|
|
144
|
+
throw new Error(`Approval request ${ctx.requestId} is no longer pending`);
|
|
145
|
+
}
|
|
146
|
+
const items = tracked.view.permissions?.items;
|
|
147
|
+
if (!items) {
|
|
148
|
+
throw new Error(`Approval request ${ctx.requestId} has no permissions items`);
|
|
149
|
+
}
|
|
150
|
+
const item = items.find((i) => i.id === action.permId);
|
|
151
|
+
if (!item) {
|
|
152
|
+
throw new Error(`Permission item ${action.permId} not found in request ${ctx.requestId}`);
|
|
153
|
+
}
|
|
154
|
+
item.selected = action.selected;
|
|
155
|
+
return `Permission ${action.permId}: ${action.selected ? 'granted' : 'denied'}`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Update a pending approval's view (out-of-order item/started arrives late).
|
|
159
|
+
* Only applies while the approval is still pending — resolved/expired
|
|
160
|
+
* approvals must not be resurrected or rewritten.
|
|
161
|
+
*/
|
|
162
|
+
updateView(requestId, view) {
|
|
163
|
+
const tracked = this.approvals.get(requestId);
|
|
164
|
+
if (!tracked || tracked.state !== 'pending')
|
|
165
|
+
return;
|
|
166
|
+
tracked.view = view;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the number of pending approvals.
|
|
170
|
+
*/
|
|
171
|
+
pendingCount() {
|
|
172
|
+
let count = 0;
|
|
173
|
+
for (const [, tracked] of this.approvals) {
|
|
174
|
+
if (tracked.state === 'pending')
|
|
175
|
+
count++;
|
|
176
|
+
}
|
|
177
|
+
return count;
|
|
178
|
+
}
|
|
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
|
+
// =========================================================================
|
|
194
|
+
// Internal
|
|
195
|
+
// =========================================================================
|
|
196
|
+
clearTimer(tracked) {
|
|
197
|
+
if (tracked.timeoutTimer !== null) {
|
|
198
|
+
clearTimeout(tracked.timeoutTimer);
|
|
199
|
+
tracked.timeoutTimer = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
expireApproval(requestId) {
|
|
203
|
+
const tracked = this.approvals.get(requestId);
|
|
204
|
+
if (!tracked || tracked.state !== 'pending')
|
|
205
|
+
return;
|
|
206
|
+
tracked.state = 'expired';
|
|
207
|
+
this.clearTimer(tracked);
|
|
208
|
+
// 闭环:审批过期时向 server 发送 cancel(真实协议决策空间中的「停止等待」语义),
|
|
209
|
+
// 避免 server 无限等待审批响应。decline 仅是本桥 UI 安全兜底,不在真实协议内。
|
|
210
|
+
// 此前只改本地状态,server 只能等 runner 的 turn 超时兜底(用户「点了允许还超时」)。
|
|
211
|
+
void this.responder(requestId, { action: 'cancel' }).catch((err) => {
|
|
212
|
+
getLogger().warn(`[approval-coordinator] expiry cancel failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
213
|
+
// 响应失败不是「正常过期」:状态升级为 failed,避免误判为已正常通知
|
|
214
|
+
// server(spec R1:responder 抛错时状态置 failed)。
|
|
215
|
+
tracked.state = 'failed';
|
|
216
|
+
// 兜底:cancel 未能送达时主动中断 turn,避免 server 无限等待审批响应。
|
|
217
|
+
void this.interruptTurn().catch((interruptErr) => {
|
|
218
|
+
getLogger().warn(`[approval-coordinator] expiry interrupt failed requestId=${requestId}: ${interruptErr instanceof Error ? interruptErr.message : String(interruptErr)}`);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
// 通知卡片进入过期态(run-renderer 渲染「⏰ 审批已过期」并隐藏按钮)。
|
|
222
|
+
void this.pushToCard([{ type: 'approval_expired', requestId }]).catch((err) => {
|
|
223
|
+
getLogger().warn(`[approval-coordinator] expiry card event failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
actionToDecision(action) {
|
|
227
|
+
switch (action.action) {
|
|
228
|
+
case 'accept':
|
|
229
|
+
return 'accept';
|
|
230
|
+
case 'accept_for_session':
|
|
231
|
+
return 'acceptForSession';
|
|
232
|
+
case 'accept_with_execpolicy_amendment':
|
|
233
|
+
return 'acceptWithExecpolicyAmendment';
|
|
234
|
+
case 'decline':
|
|
235
|
+
return 'decline';
|
|
236
|
+
case 'cancel':
|
|
237
|
+
return 'cancel';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Map a card decision string to the ApprovalAction used across the bridge. */
|
|
242
|
+
export function decisionToApprovalAction(decision) {
|
|
243
|
+
switch (decision) {
|
|
244
|
+
case 'accept':
|
|
245
|
+
return { action: 'accept' };
|
|
246
|
+
case 'acceptForSession':
|
|
247
|
+
return { action: 'accept_for_session' };
|
|
248
|
+
case 'acceptWithExecpolicyAmendment':
|
|
249
|
+
return { action: 'accept_with_execpolicy_amendment' };
|
|
250
|
+
case 'decline':
|
|
251
|
+
return { action: 'decline' };
|
|
252
|
+
case 'cancel':
|
|
253
|
+
return { action: 'cancel' };
|
|
254
|
+
default:
|
|
255
|
+
return { action: 'decline' };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=approval-coordinator.js.map
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ interface BridgeChannel extends RunCardChannel {
|
|
|
25
25
|
reconnect(): Promise<void>;
|
|
26
26
|
sendFile(chatId: string, filePath: string): Promise<string>;
|
|
27
27
|
addReaction(messageId: string, emoji: string): Promise<void>;
|
|
28
|
+
removeReactionByEmoji(messageId: string, emoji: string): Promise<void>;
|
|
28
29
|
}
|
|
29
30
|
/** Caller context for a bridge operation. */
|
|
30
31
|
interface BridgeContext {
|
|
@@ -118,6 +119,13 @@ export declare class Bridge {
|
|
|
118
119
|
private queueManager;
|
|
119
120
|
/** Active runs per workspace (cwd). Multiple workspaces can run in parallel. */
|
|
120
121
|
private activeRuns;
|
|
122
|
+
/** Approval coordinators keyed by runId. */
|
|
123
|
+
private approvalCoordinators;
|
|
124
|
+
/**
|
|
125
|
+
* 最近一次已完成的 codex app-server run(按 cwd)。run 结束后 activeRuns
|
|
126
|
+
* 已清空,Compact 需要它来校验 runId 并提供 sessionId。
|
|
127
|
+
*/
|
|
128
|
+
private lastCompactableCodexRun;
|
|
121
129
|
/**
|
|
122
130
|
* Active `!` bash runs, keyed by runId (NOT cwd). Bash runs bypass the serial
|
|
123
131
|
* queue and may run in parallel with claude runs / other bash runs in the same
|
|
@@ -324,6 +332,60 @@ export declare class Bridge {
|
|
|
324
332
|
* find and return to this session without scrolling up.
|
|
325
333
|
*/
|
|
326
334
|
private sendCompletionNotificationCard;
|
|
335
|
+
/**
|
|
336
|
+
* Handle an approval response from a card button click.
|
|
337
|
+
* Routes to the ApprovalCoordinator for the given run.
|
|
338
|
+
*/
|
|
339
|
+
handleApprovalRespond(opts: {
|
|
340
|
+
runId: string;
|
|
341
|
+
requestId: number | string;
|
|
342
|
+
decision: string;
|
|
343
|
+
scope?: 'turn' | 'session';
|
|
344
|
+
nonce: string;
|
|
345
|
+
}): Promise<void>;
|
|
346
|
+
/**
|
|
347
|
+
* Handle a permission toggle from a card button click.
|
|
348
|
+
* Routes to the ApprovalCoordinator for the given run.
|
|
349
|
+
*/
|
|
350
|
+
handleApprovalToggle(opts: {
|
|
351
|
+
runId: string;
|
|
352
|
+
requestId: number | string;
|
|
353
|
+
permId: string;
|
|
354
|
+
nonce: string;
|
|
355
|
+
selected?: boolean;
|
|
356
|
+
}): Promise<void>;
|
|
357
|
+
/**
|
|
358
|
+
* Handle codex.compact card action — trigger a Codex compaction request.
|
|
359
|
+
* Validates the run exists and is in a terminal state, then calls
|
|
360
|
+
* the app-server runner's runCompact().
|
|
361
|
+
*/
|
|
362
|
+
handleCodexCompact(value: {
|
|
363
|
+
runId?: string;
|
|
364
|
+
}, ctx: BridgeContext): Promise<void>;
|
|
365
|
+
/**
|
|
366
|
+
* Stream a Codex compaction to a card: start a RunCardSession with
|
|
367
|
+
* operationKind='compaction' (no recursive Compact button), consume
|
|
368
|
+
* runner.runCompact() events, read authoritative jsonl usage, and finish
|
|
369
|
+
* the card. Shared by handleCodexCompact (run card button) and
|
|
370
|
+
* handleResumeCompact (resume cards) so both flows stay in sync.
|
|
371
|
+
*
|
|
372
|
+
* Precondition: callers have already validated that `runner` implements
|
|
373
|
+
* runCompact and that the session exists in `cwd`.
|
|
374
|
+
*/
|
|
375
|
+
private streamCodexCompact;
|
|
376
|
+
/**
|
|
377
|
+
* Handle resume.compact card action — compact a session directly from a
|
|
378
|
+
* resume card (auto-resume / `/resume <id>`), without a runId.
|
|
379
|
+
*
|
|
380
|
+
* Unlike handleCodexCompact (which validates against the last finished run's
|
|
381
|
+
* runId), the resume card carries the sessionId + agent it was rendered for.
|
|
382
|
+
* Validation: the session must exist in the current cwd (same rule as
|
|
383
|
+
* cmdResume / resume.use) and the runner must implement runCompact.
|
|
384
|
+
*/
|
|
385
|
+
handleResumeCompact(value: {
|
|
386
|
+
sessionId?: string;
|
|
387
|
+
agent?: string;
|
|
388
|
+
}, ctx: BridgeContext): Promise<void>;
|
|
327
389
|
/**
|
|
328
390
|
* Execute a bash command and stream output to a card.
|
|
329
391
|
* `!` commands bypass the serial queue entirely (see body comment + §9.6):
|