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/dist/card/run-state.js
CHANGED
|
@@ -32,8 +32,204 @@ export function reduceRunState(state, event) {
|
|
|
32
32
|
return reducePlanEvent(state, event);
|
|
33
33
|
if (event.type === 'file_change')
|
|
34
34
|
return reduceFileChangeEvent(state, event);
|
|
35
|
+
if (event.type === 'turn_started')
|
|
36
|
+
return reduceTurnStartedEvent(state, event);
|
|
37
|
+
if (event.type === 'turn_diff')
|
|
38
|
+
return reduceTurnDiffEvent(state, event);
|
|
39
|
+
if (event.type === 'approval_requested') {
|
|
40
|
+
const approvals = state.approvals ?? [];
|
|
41
|
+
const next = { view: event.view, expired: false };
|
|
42
|
+
const idx = approvals.findIndex((a) => a.view.requestId === event.requestId);
|
|
43
|
+
if (idx >= 0) {
|
|
44
|
+
// 同一 requestId 重复到达:原地替换,保持槽位顺序。
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
approvals: approvals.map((a, i) => (i === idx ? next : a)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { ...state, approvals: [...approvals, next] };
|
|
51
|
+
}
|
|
52
|
+
if (event.type === 'approval_resolved') {
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
approvals: (state.approvals ?? []).filter((a) => a.view.requestId !== event.requestId),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (event.type === 'approval_view_updated') {
|
|
59
|
+
return {
|
|
60
|
+
...state,
|
|
61
|
+
approvals: (state.approvals ?? []).map((a) => a.view.requestId === event.requestId ? { ...a, view: event.view } : a),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (event.type === 'approval_expired') {
|
|
65
|
+
return {
|
|
66
|
+
...state,
|
|
67
|
+
approvals: (state.approvals ?? []).map((a) => a.view.requestId === event.requestId ? { ...a, expired: true } : a),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
35
70
|
return state;
|
|
36
71
|
}
|
|
72
|
+
/** Handle app-server turn_started — records the operation kind (turn vs compact). */
|
|
73
|
+
function reduceTurnStartedEvent(state, event) {
|
|
74
|
+
if (event.type !== 'turn_started')
|
|
75
|
+
return state;
|
|
76
|
+
return {
|
|
77
|
+
...state,
|
|
78
|
+
operationKind: event.operationKind,
|
|
79
|
+
footer: state.footer ?? 'thinking',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Handle app-server turn_diff events. The translator emits FULL per-item
|
|
84
|
+
* snapshots (accumulated text/reasoning/tool output/plan, current file-change
|
|
85
|
+
* set). Each snapshot is scoped to a thread item (event.itemId); the block is
|
|
86
|
+
* replaced in place to avoid duplication, and a NEW block is appended on the
|
|
87
|
+
* item's first sight — so interleaved items keep their real chronology and
|
|
88
|
+
* the active streaming content sits at the bottom, like the exec path.
|
|
89
|
+
*/
|
|
90
|
+
function reduceTurnDiffEvent(state, event) {
|
|
91
|
+
if (event.type !== 'turn_diff')
|
|
92
|
+
return state;
|
|
93
|
+
let next = state;
|
|
94
|
+
const ts = event.timestamp;
|
|
95
|
+
const complete = event.complete === true;
|
|
96
|
+
if (event.text !== undefined) {
|
|
97
|
+
const blocks = markThinkingInactive(next.blocks);
|
|
98
|
+
next = {
|
|
99
|
+
...next,
|
|
100
|
+
blocks: keepLatestBlocks(upsertSnapshotBlock(blocks, (b) => b.kind === 'text' && b.itemId === event.itemId, () => ({
|
|
101
|
+
kind: 'text',
|
|
102
|
+
itemId: event.itemId,
|
|
103
|
+
content: keepLatest(event.text, MAX_TEXT_CHARS),
|
|
104
|
+
timestamp: ts,
|
|
105
|
+
...(complete ? { completedAt: ts } : {}),
|
|
106
|
+
}), (existing) => {
|
|
107
|
+
const textBlock = existing;
|
|
108
|
+
return {
|
|
109
|
+
...textBlock,
|
|
110
|
+
content: keepLatest(event.text, MAX_TEXT_CHARS),
|
|
111
|
+
...(complete ? { completedAt: ts } : {}),
|
|
112
|
+
};
|
|
113
|
+
}, !complete)),
|
|
114
|
+
footer: 'streaming',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (event.reasoning !== undefined) {
|
|
118
|
+
next = {
|
|
119
|
+
...next,
|
|
120
|
+
blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'thinking' && b.itemId === event.itemId, () => ({
|
|
121
|
+
kind: 'thinking',
|
|
122
|
+
itemId: event.itemId,
|
|
123
|
+
content: keepLatest(event.reasoning, MAX_REASONING_CHARS),
|
|
124
|
+
active: !complete,
|
|
125
|
+
timestamp: ts,
|
|
126
|
+
...(complete ? { completedAt: ts } : {}),
|
|
127
|
+
}), (existing) => {
|
|
128
|
+
const thinkingBlock = existing;
|
|
129
|
+
return {
|
|
130
|
+
...thinkingBlock,
|
|
131
|
+
content: keepLatest(event.reasoning, MAX_REASONING_CHARS),
|
|
132
|
+
active: !complete,
|
|
133
|
+
...(complete ? { completedAt: ts } : {}),
|
|
134
|
+
};
|
|
135
|
+
}, !complete)),
|
|
136
|
+
footer: 'thinking',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (event.toolOutput !== undefined) {
|
|
140
|
+
next = {
|
|
141
|
+
...next,
|
|
142
|
+
blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'tool' && b.tool.id === event.itemId, () => ({
|
|
143
|
+
kind: 'tool',
|
|
144
|
+
tool: {
|
|
145
|
+
id: event.itemId,
|
|
146
|
+
name: 'command',
|
|
147
|
+
input: '',
|
|
148
|
+
output: keepLatest(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
|
|
149
|
+
status: complete ? (event.toolStatus ?? 'ok') : 'running',
|
|
150
|
+
startedAt: ts,
|
|
151
|
+
...(complete ? { completedAt: ts } : {}),
|
|
152
|
+
},
|
|
153
|
+
}), (existing) => ({
|
|
154
|
+
kind: 'tool',
|
|
155
|
+
tool: {
|
|
156
|
+
...existing.tool,
|
|
157
|
+
output: keepLatest(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
|
|
158
|
+
status: complete
|
|
159
|
+
? (event.toolStatus ?? 'ok')
|
|
160
|
+
: existing.tool.status,
|
|
161
|
+
...(complete ? { completedAt: ts } : {}),
|
|
162
|
+
},
|
|
163
|
+
}), !complete)),
|
|
164
|
+
footer: 'tool_running',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (event.plan !== undefined) {
|
|
168
|
+
const newPlan = keepLatest(event.plan, MAX_REASONING_CHARS * 2);
|
|
169
|
+
next = {
|
|
170
|
+
...next,
|
|
171
|
+
plan: newPlan,
|
|
172
|
+
blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'plan' && b.itemId === event.itemId, () => ({
|
|
173
|
+
kind: 'plan',
|
|
174
|
+
itemId: event.itemId,
|
|
175
|
+
content: newPlan,
|
|
176
|
+
active: !complete,
|
|
177
|
+
timestamp: ts,
|
|
178
|
+
...(complete ? { completedAt: ts } : {}),
|
|
179
|
+
}), (existing) => {
|
|
180
|
+
const planBlock = existing;
|
|
181
|
+
return {
|
|
182
|
+
...planBlock,
|
|
183
|
+
content: newPlan,
|
|
184
|
+
active: !complete,
|
|
185
|
+
...(complete ? { completedAt: ts } : {}),
|
|
186
|
+
};
|
|
187
|
+
}, !complete)),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (event.fileChanges !== undefined) {
|
|
191
|
+
let blocks = next.blocks;
|
|
192
|
+
for (const fc of event.fileChanges) {
|
|
193
|
+
const fileBlock = {
|
|
194
|
+
kind: 'file_change',
|
|
195
|
+
itemId: event.itemId,
|
|
196
|
+
path: fc.path,
|
|
197
|
+
operation: fc.kind === 'add' ? 'create' : fc.kind === 'delete' ? 'delete' : 'edit',
|
|
198
|
+
diff: fc.diff,
|
|
199
|
+
timestamp: ts,
|
|
200
|
+
...(complete ? { completedAt: ts } : {}),
|
|
201
|
+
};
|
|
202
|
+
// 同一 item 的同一路径原地替换(权威 diff 校正),新路径/新 item 追加。
|
|
203
|
+
blocks = upsertSnapshotBlock(blocks, (b) => b.kind === 'file_change' && b.itemId === event.itemId && b.path === fc.path, () => fileBlock, () => fileBlock, !complete);
|
|
204
|
+
}
|
|
205
|
+
next = { ...next, blocks: keepLatestBlocks(blocks) };
|
|
206
|
+
}
|
|
207
|
+
return next;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Replace the last block matching `predicate` with `make`; if none matches,
|
|
211
|
+
* append a block built by `fallback`. Snapshot semantics for app-server
|
|
212
|
+
* turn_diff streams.
|
|
213
|
+
*
|
|
214
|
+
* `moveToEndWhenNotLast`: 当命中块不在数组末尾且为流式更新(complete=false)
|
|
215
|
+
* 时,把更新后的块移到末尾。正常协议下 item 串行、流式块总是最后一个,
|
|
216
|
+
* 此分支不触发;若真实 server 在同一个 item 上跨其他 item 续流 delta(违反
|
|
217
|
+
* item 生命周期),流式内容仍会出现在卡片底部,而不是钉在旧位置。
|
|
218
|
+
*/
|
|
219
|
+
function upsertSnapshotBlock(blocks, predicate, fallback, update, moveToEndWhenNotLast = false) {
|
|
220
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
221
|
+
if (predicate(blocks[i])) {
|
|
222
|
+
const copy = blocks.slice();
|
|
223
|
+
copy[i] = update ? update(blocks[i]) : fallback();
|
|
224
|
+
if (moveToEndWhenNotLast && i < copy.length - 1) {
|
|
225
|
+
const [moved] = copy.splice(i, 1);
|
|
226
|
+
copy.push(moved);
|
|
227
|
+
}
|
|
228
|
+
return copy;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return [...blocks, fallback()];
|
|
232
|
+
}
|
|
37
233
|
/** Handle system init and compact_boundary events. */
|
|
38
234
|
function reduceSystemEvent(state, event) {
|
|
39
235
|
if (event.type !== 'system')
|
|
@@ -71,8 +267,13 @@ function reduceResultEvent(state, event) {
|
|
|
71
267
|
const resultEvent = event;
|
|
72
268
|
const incomingSubtype = event.subtype;
|
|
73
269
|
const incomingErrorMsg = incomingSubtype === 'error' ? (resultEvent.errorMessage ?? 'Agent 返回错误结果') : undefined;
|
|
74
|
-
// 合并 subtype:error
|
|
75
|
-
|
|
270
|
+
// 合并 subtype:error 优先,其次 interrupted(任一为 error 即 error;
|
|
271
|
+
// 否则任一为 interrupted 即 interrupted;interrupted 不得并入 success)。
|
|
272
|
+
const mergedSubtype = state.resultSubtype === 'error' || incomingSubtype === 'error'
|
|
273
|
+
? 'error'
|
|
274
|
+
: state.resultSubtype === 'interrupted' || incomingSubtype === 'interrupted'
|
|
275
|
+
? 'interrupted'
|
|
276
|
+
: 'success';
|
|
76
277
|
// 合并 errorMsg:首个非 undefined 胜出(existing ?? incoming)
|
|
77
278
|
const mergedErrorMsg = state.errorMsg ?? incomingErrorMsg;
|
|
78
279
|
return {
|
package/dist/config/dir.js
CHANGED
|
@@ -79,7 +79,7 @@ export function printHelp() {
|
|
|
79
79
|
'Options:',
|
|
80
80
|
' --config-dir <path> 自定义配置目录(默认 ~/.lark-remote,可用于同机多实例)',
|
|
81
81
|
' --settings <path> 指定 Claude 配置文件路径',
|
|
82
|
-
' --dev 开发模式:标记从源码 bun src/index.ts
|
|
82
|
+
' --dev 开发模式:标记从源码 bun src/index.ts 启动(看门狗据此选择拉起方式)',
|
|
83
83
|
' --update 升级到最新版本后退出(用于 cron/脚本自动化升级)',
|
|
84
84
|
' -h, --help 显示本帮助信息',
|
|
85
85
|
' -v, --version 显示版本号',
|
package/dist/config/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export declare const PI_THINKING_LEVELS: readonly ["off", "minimal", "low", "med
|
|
|
10
10
|
* instead of re-literalizing `5000` (Clean Code P3-1, G25).
|
|
11
11
|
*/
|
|
12
12
|
export declare const DEFAULT_STOP_GRACE_MS: 5000;
|
|
13
|
+
/** Codex app-server turn idle timeout default (minutes). */
|
|
14
|
+
export declare const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES: 10;
|
|
13
15
|
/** 模型 ID → alias 映射。 */
|
|
14
16
|
export declare const MODEL_ID_TO_ALIAS: Record<string, string>;
|
|
15
17
|
/** alias → model ID 反向映射。 */
|
|
@@ -31,6 +33,26 @@ export declare const CodexConfigSchema: z.ZodObject<{
|
|
|
31
33
|
modelProvider: z.ZodOptional<z.ZodString>;
|
|
32
34
|
reasoningEffort: z.ZodOptional<z.ZodString>;
|
|
33
35
|
stopGraceMs: z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
serviceMode: z.ZodOptional<z.ZodEnum<{
|
|
37
|
+
exec: "exec";
|
|
38
|
+
"app-server": "app-server";
|
|
39
|
+
}>>;
|
|
40
|
+
approvalPolicy: z.ZodOptional<z.ZodEnum<{
|
|
41
|
+
untrusted: "untrusted";
|
|
42
|
+
"on-request": "on-request";
|
|
43
|
+
never: "never";
|
|
44
|
+
}>>;
|
|
45
|
+
sandbox: z.ZodOptional<z.ZodEnum<{
|
|
46
|
+
"danger-full-access": "danger-full-access";
|
|
47
|
+
"read-only": "read-only";
|
|
48
|
+
"workspace-write": "workspace-write";
|
|
49
|
+
}>>;
|
|
50
|
+
appServer: z.ZodOptional<z.ZodObject<{
|
|
51
|
+
binary: z.ZodDefault<z.ZodString>;
|
|
52
|
+
requestTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
53
|
+
idleTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
54
|
+
turnIdleTimeoutMinutes: z.ZodDefault<z.ZodNumber>;
|
|
55
|
+
}, z.core.$strip>>;
|
|
34
56
|
}, z.core.$strip>;
|
|
35
57
|
/** OpenCode-specific configuration (run mode: opencode run --format json --auto). */
|
|
36
58
|
declare const OpencodeConfigSchema: z.ZodObject<{
|
|
@@ -82,6 +104,26 @@ export declare const AppConfigSchema: z.ZodObject<{
|
|
|
82
104
|
modelProvider: z.ZodOptional<z.ZodString>;
|
|
83
105
|
reasoningEffort: z.ZodOptional<z.ZodString>;
|
|
84
106
|
stopGraceMs: z.ZodDefault<z.ZodNumber>;
|
|
107
|
+
serviceMode: z.ZodOptional<z.ZodEnum<{
|
|
108
|
+
exec: "exec";
|
|
109
|
+
"app-server": "app-server";
|
|
110
|
+
}>>;
|
|
111
|
+
approvalPolicy: z.ZodOptional<z.ZodEnum<{
|
|
112
|
+
untrusted: "untrusted";
|
|
113
|
+
"on-request": "on-request";
|
|
114
|
+
never: "never";
|
|
115
|
+
}>>;
|
|
116
|
+
sandbox: z.ZodOptional<z.ZodEnum<{
|
|
117
|
+
"danger-full-access": "danger-full-access";
|
|
118
|
+
"read-only": "read-only";
|
|
119
|
+
"workspace-write": "workspace-write";
|
|
120
|
+
}>>;
|
|
121
|
+
appServer: z.ZodOptional<z.ZodObject<{
|
|
122
|
+
binary: z.ZodDefault<z.ZodString>;
|
|
123
|
+
requestTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
124
|
+
idleTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
125
|
+
turnIdleTimeoutMinutes: z.ZodDefault<z.ZodNumber>;
|
|
126
|
+
}, z.core.$strip>>;
|
|
85
127
|
}, z.core.$strip>>;
|
|
86
128
|
opencode: z.ZodOptional<z.ZodObject<{
|
|
87
129
|
providerID: z.ZodDefault<z.ZodString>;
|
package/dist/config/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const DEFAULTS = {
|
|
|
16
16
|
CLAUDE_MODEL: 'claude-opus-4-8',
|
|
17
17
|
STOP_GRACE_MS: 5000,
|
|
18
18
|
IDLE_WATCHDOG_MINUTES: 15,
|
|
19
|
+
TURN_IDLE_TIMEOUT_MINUTES: 10,
|
|
19
20
|
};
|
|
20
21
|
/**
|
|
21
22
|
* Default SIGTERM→SIGKILL grace period (ms). Exported so runners (SpawningRunner)
|
|
@@ -23,6 +24,8 @@ const DEFAULTS = {
|
|
|
23
24
|
* instead of re-literalizing `5000` (Clean Code P3-1, G25).
|
|
24
25
|
*/
|
|
25
26
|
export const DEFAULT_STOP_GRACE_MS = DEFAULTS.STOP_GRACE_MS;
|
|
27
|
+
/** Codex app-server turn idle timeout default (minutes). */
|
|
28
|
+
export const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES = DEFAULTS.TURN_IDLE_TIMEOUT_MINUTES;
|
|
26
29
|
/** 模型 ID → alias 映射。 */
|
|
27
30
|
export const MODEL_ID_TO_ALIAS = {
|
|
28
31
|
'claude-opus-4-8': 'opus',
|
|
@@ -47,13 +50,24 @@ const ClaudeConfigSchema = z.object({
|
|
|
47
50
|
// permissionMode 硬编码为 bypassPermissions(runner 内部),不通过 config 配置
|
|
48
51
|
stopGraceMs: z.number().int().min(0).default(DEFAULTS.STOP_GRACE_MS),
|
|
49
52
|
});
|
|
53
|
+
/** Codex app-server connection configuration. */
|
|
54
|
+
const CodexAppServerConfigSchema = z.object({
|
|
55
|
+
/** Path to the codex binary for app-server mode. */
|
|
56
|
+
binary: z.string().default('codex'),
|
|
57
|
+
/** Request timeout in milliseconds. */
|
|
58
|
+
requestTimeoutMs: z.number().int().min(0).default(60000),
|
|
59
|
+
/** Idle TTL for connection pool in milliseconds. */
|
|
60
|
+
idleTtlMs: z.number().int().min(0).default(1800000),
|
|
61
|
+
/** Turn idle timeout in minutes: no app-server output for this long triggers
|
|
62
|
+
* turn/interrupt and fails the run. 0 disables the timeout. */
|
|
63
|
+
turnIdleTimeoutMinutes: z.number().int().min(0).default(DEFAULT_TURN_IDLE_TIMEOUT_MINUTES),
|
|
64
|
+
});
|
|
50
65
|
/** Codex-specific configuration. */
|
|
51
66
|
export const CodexConfigSchema = z.object({
|
|
52
67
|
/** model to use. Undefined → codex reads from its config.toml. */
|
|
53
68
|
model: z.string().optional(),
|
|
54
69
|
/** model provider, e.g. 'volcengine-coding-plan'. Undefined → codex reads from its config.toml. */
|
|
55
70
|
modelProvider: z.string().optional(),
|
|
56
|
-
// approvalPolicy 和 sandboxPolicy 在 runner 内硬编码(exec 模式使用 approval_policy=never)
|
|
57
71
|
/**
|
|
58
72
|
* Reasoning effort level。codex 标准档位 minimal/low/medium/high/xhigh/max/ultra
|
|
59
73
|
* 之外还接受 none 与模型自定义值(ReasoningEffort::Custom),目录声明什么就存什么
|
|
@@ -63,6 +77,14 @@ export const CodexConfigSchema = z.object({
|
|
|
63
77
|
reasoningEffort: z.string().min(1).optional(),
|
|
64
78
|
/** Stop grace period in milliseconds. */
|
|
65
79
|
stopGraceMs: z.number().int().min(0).default(5000),
|
|
80
|
+
/** Service mode: 'exec' (spawn-per-message) or 'app-server' (persistent connection). */
|
|
81
|
+
serviceMode: z.enum(['exec', 'app-server']).optional(),
|
|
82
|
+
/** Approval policy (Codex 官方 AskForApproval 标准值). 仅 app-server 模式有效(exec 固定 approval_policy="never")。 */
|
|
83
|
+
approvalPolicy: z.enum(['untrusted', 'on-request', 'never']).optional(),
|
|
84
|
+
/** Sandbox mode (Codex 官方 SandboxMode 标准值). 仅 app-server 模式有效(exec 固定 --sandbox danger-full-access)。 */
|
|
85
|
+
sandbox: z.enum(['read-only', 'workspace-write', 'danger-full-access']).optional(),
|
|
86
|
+
/** App-server connection configuration. */
|
|
87
|
+
appServer: CodexAppServerConfigSchema.optional(),
|
|
66
88
|
});
|
|
67
89
|
/** OpenCode-specific configuration (run mode: opencode run --format json --auto). */
|
|
68
90
|
const OpencodeConfigSchema = z.object({
|
|
@@ -68,6 +68,11 @@ export declare class FeishuConnector {
|
|
|
68
68
|
/** Format error for logging without causing circular serialization */
|
|
69
69
|
private formatError;
|
|
70
70
|
addReaction(messageId: string, emoji: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Remove a reaction by emoji (e.g. retract the Typing indicator).
|
|
73
|
+
* The SDK resolves the reaction id internally; missing reaction is a no-op.
|
|
74
|
+
*/
|
|
75
|
+
removeReactionByEmoji(messageId: string, emoji: string): Promise<void>;
|
|
71
76
|
/**
|
|
72
77
|
* Upload a file to Feishu and send it to the specified chat.
|
|
73
78
|
* Uses the im/v1/files upload API.
|
package/dist/connector/index.js
CHANGED
|
@@ -264,6 +264,18 @@ export class FeishuConnector {
|
|
|
264
264
|
getLogger().error('[feishu] addReaction failed:', this.formatError(err));
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Remove a reaction by emoji (e.g. retract the Typing indicator).
|
|
269
|
+
* The SDK resolves the reaction id internally; missing reaction is a no-op.
|
|
270
|
+
*/
|
|
271
|
+
async removeReactionByEmoji(messageId, emoji) {
|
|
272
|
+
try {
|
|
273
|
+
await this.channel.removeReactionByEmoji(messageId, emoji);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
getLogger().error('[feishu] removeReactionByEmoji failed:', this.formatError(err));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
267
279
|
/**
|
|
268
280
|
* Upload a file to Feishu and send it to the specified chat.
|
|
269
281
|
* Uses the im/v1/files upload API.
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolveAgentChoices, } from './runner/index.js';
|
|
|
3
3
|
import { ensureConfig } from './config/wizard.js';
|
|
4
4
|
import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion, } from './config/dir.js';
|
|
5
5
|
import { FeishuConnector } from './connector/index.js';
|
|
6
|
-
import { ClaudeRunner,
|
|
6
|
+
import { ClaudeRunner, createCodexRunner, OpencodeExecRunner, PiRunner, KimiRunner, } from './runner/index.js';
|
|
7
7
|
import { AgentRegistry } from './runner/registry.js';
|
|
8
8
|
import { probeAllAgents } from './runner/probe.js';
|
|
9
9
|
import { SessionReaderRegistry, SessionStore } from './session/index.js';
|
|
@@ -21,11 +21,11 @@ import { classifyRejection } from './error-classification.js';
|
|
|
21
21
|
import { WorkspaceStore } from './workspace/index.js';
|
|
22
22
|
import { markdownDiv } from './card/collapsible.js';
|
|
23
23
|
import { sessionEventPanel } from './router/card-helpers.js';
|
|
24
|
-
import { newSessionButton, agentDisplayName } from './card/card-shared.js';
|
|
24
|
+
import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
/** Send auto-resume card for a restored session */
|
|
28
|
-
async function sendAutoResumeCard(connector, contact, cwd, session, state) {
|
|
28
|
+
async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false) {
|
|
29
29
|
if (!contact.userId)
|
|
30
30
|
return;
|
|
31
31
|
const recipient = contact.chatId ?? contact.userId;
|
|
@@ -64,17 +64,21 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state) {
|
|
|
64
64
|
const usageStr = formatUsageStats(state.usage);
|
|
65
65
|
elements.push(markdownDiv(usageStr));
|
|
66
66
|
}
|
|
67
|
-
// Add
|
|
68
|
-
const
|
|
67
|
+
// Add action buttons - use column_set+column (div+elements not supported in 2.0)
|
|
68
|
+
const actionButtons = [];
|
|
69
|
+
// Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且未在跑。
|
|
70
|
+
// 启动自动恢复时不会存在该会话的 activeRun,直接渲染。
|
|
71
|
+
if (compactSupported) {
|
|
72
|
+
actionButtons.push(resumeCompactButton(session.sessionId, state.agentKind ?? 'codex'));
|
|
73
|
+
}
|
|
74
|
+
actionButtons.push(newSessionButton());
|
|
69
75
|
elements.push({
|
|
70
76
|
tag: 'column_set',
|
|
71
|
-
columns:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
],
|
|
77
|
+
columns: actionButtons.map((btn) => ({
|
|
78
|
+
tag: 'column',
|
|
79
|
+
width: 'auto',
|
|
80
|
+
elements: [btn],
|
|
81
|
+
})),
|
|
78
82
|
});
|
|
79
83
|
const card = {
|
|
80
84
|
schema: '2.0',
|
|
@@ -163,15 +167,17 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
163
167
|
workspace: ws,
|
|
164
168
|
});
|
|
165
169
|
});
|
|
166
|
-
// Register
|
|
167
|
-
//
|
|
168
|
-
//
|
|
170
|
+
// Register Codex runner. Factory reads latest config from container.current
|
|
171
|
+
// so runtime config changes (serviceMode, model, sandbox, approvalPolicy,
|
|
172
|
+
// reasoningEffort, etc.) take effect after bridge.setConfig() + clearRunners().
|
|
173
|
+
// serviceMode=app-server → CodexAppServerRunner (persistent connection with
|
|
174
|
+
// approval + sandbox); otherwise CodexExecRunner (codex exec --json).
|
|
169
175
|
const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
|
|
170
176
|
agentRegistry.register('codex', (ws) => {
|
|
171
177
|
const container = agentRegistry.getConfigContainer();
|
|
172
178
|
const latestConfig = container?.current ?? config;
|
|
173
179
|
const codexConfig = getAgentConfig(latestConfig, 'codex');
|
|
174
|
-
return
|
|
180
|
+
return createCodexRunner({
|
|
175
181
|
model: codexConfig?.model,
|
|
176
182
|
modelProvider: codexConfig?.modelProvider,
|
|
177
183
|
reasoningEffort: codexConfig?.reasoningEffort,
|
|
@@ -179,6 +185,13 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
179
185
|
pidDir: configDir,
|
|
180
186
|
workspace: ws,
|
|
181
187
|
sessionReader: codexSessionReader,
|
|
188
|
+
serviceMode: codexConfig?.serviceMode,
|
|
189
|
+
sandbox: codexConfig?.sandbox,
|
|
190
|
+
approvalPolicy: codexConfig?.approvalPolicy,
|
|
191
|
+
appServerBinary: codexConfig?.appServer?.binary,
|
|
192
|
+
appServerRequestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
|
|
193
|
+
appServerIdleTtlMs: codexConfig?.appServer?.idleTtlMs,
|
|
194
|
+
appServerTurnIdleTimeoutMinutes: codexConfig?.appServer?.turnIdleTimeoutMinutes,
|
|
182
195
|
});
|
|
183
196
|
});
|
|
184
197
|
// Register OpencodeExecRunner (run mode)
|
|
@@ -479,11 +492,15 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
479
492
|
inputValue: action.raw?.action?.input_value ??
|
|
480
493
|
action.action.input_value,
|
|
481
494
|
};
|
|
482
|
-
// queue.input
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
|
|
495
|
+
// queue.input / config.save / approval.respond / approval.toggle 返回
|
|
496
|
+
// CardActionResponse toast 给点击用户即时反馈。必须直接返回值 --
|
|
497
|
+
// enqueueImmediate / enqueueConfigAction 是 fire-and-forget,会吞掉返回值。
|
|
498
|
+
// 审批响应尤其不能落串行队列:run 任务占用队列头直到 turn 结束,审批响应
|
|
499
|
+
// 排在后面会形成死锁(run 不结束不执行、run 结束 coordinator 已删响应空转)。
|
|
500
|
+
if (actionValue.cmd === 'queue.input' ||
|
|
501
|
+
actionValue.cmd === 'config.save' ||
|
|
502
|
+
actionValue.cmd === 'approval.respond' ||
|
|
503
|
+
actionValue.cmd === 'approval.toggle') {
|
|
487
504
|
return router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
488
505
|
}
|
|
489
506
|
// order.exec → equivalent queued message (Plan A): resolve the order text
|
|
@@ -514,6 +531,8 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
514
531
|
chatId,
|
|
515
532
|
messageId,
|
|
516
533
|
messagePreview: `card action: ${actionValue.cmd}`,
|
|
534
|
+
// Compact 是单向操作,排队卡不允许编辑(编辑预览无意义)。
|
|
535
|
+
editable: actionValue.cmd !== 'codex.compact' && actionValue.cmd !== 'resume.compact',
|
|
517
536
|
},
|
|
518
537
|
});
|
|
519
538
|
}
|
|
@@ -663,7 +682,10 @@ async function main() {
|
|
|
663
682
|
recap: content.recap,
|
|
664
683
|
displayTitle: content.displayTitle,
|
|
665
684
|
agentKind: config.defaultAgent,
|
|
666
|
-
}
|
|
685
|
+
},
|
|
686
|
+
// Compact 仅 codex app-server 模式有实现(runner.runCompact)。
|
|
687
|
+
config.defaultAgent === 'codex' &&
|
|
688
|
+
getAgentConfig(config, 'codex')?.serviceMode === 'app-server');
|
|
667
689
|
}
|
|
668
690
|
catch (err) {
|
|
669
691
|
getLogger().warn('[startup] sendAutoResumeCard failed:', err);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Codex config builder — builds fields and handles field changes for Codex agent.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentConfigCardBuilder, ConfigField } from './types.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type AppConfig } from '../../config/index.js';
|
|
6
6
|
export declare class CodexConfigBuilder implements AgentConfigCardBuilder {
|
|
7
7
|
buildFields(displayConfig: AppConfig): ConfigField[];
|
|
8
8
|
handleFieldChange(key: string, value: unknown, config: AppConfig): Array<{
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Codex config builder — builds fields and handles field changes for Codex agent.
|
|
3
3
|
*/
|
|
4
4
|
import { loadCodexConfig, getReasoningEffortOptions, getDefaultReasoningEffort, } from '../../config/codex-config.js';
|
|
5
|
+
import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
|
|
5
6
|
export class CodexConfigBuilder {
|
|
6
7
|
buildFields(displayConfig) {
|
|
7
8
|
const fields = [];
|
|
@@ -43,6 +44,54 @@ export class CodexConfigBuilder {
|
|
|
43
44
|
options: getReasoningEffortOptions(currentCodexModel),
|
|
44
45
|
currentValue: currentReasoningEffort,
|
|
45
46
|
});
|
|
47
|
+
// Service mode (exec vs app-server)
|
|
48
|
+
const currentServiceMode = displayConfig.agents?.codex?.serviceMode ?? 'exec';
|
|
49
|
+
fields.push({
|
|
50
|
+
key: 'agents.codex.serviceMode',
|
|
51
|
+
label: '运行模式',
|
|
52
|
+
type: 'select',
|
|
53
|
+
options: ['exec', 'app-server'],
|
|
54
|
+
currentValue: currentServiceMode,
|
|
55
|
+
});
|
|
56
|
+
// 审批策略与沙箱模式仅 app-server 模式可配置(CodexAppServerRunner 读取);
|
|
57
|
+
// exec 模式在 runner 层固定 `--sandbox danger-full-access` +
|
|
58
|
+
// `approval_policy="never"`(src/runner/codex/argv.ts),卡片不暴露选项,
|
|
59
|
+
// 只用说明行把默认语义呈现给用户。
|
|
60
|
+
if (currentServiceMode === 'app-server') {
|
|
61
|
+
// Approval policy (Codex 官方 AskForApproval 标准值;on-request 为 codex 默认)
|
|
62
|
+
const currentApprovalPolicy = displayConfig.agents?.codex?.approvalPolicy ?? 'on-request';
|
|
63
|
+
fields.push({
|
|
64
|
+
key: 'agents.codex.approvalPolicy',
|
|
65
|
+
label: '审批策略',
|
|
66
|
+
type: 'select',
|
|
67
|
+
options: ['untrusted', 'on-request', 'never'],
|
|
68
|
+
currentValue: currentApprovalPolicy,
|
|
69
|
+
});
|
|
70
|
+
// Sandbox mode (Codex 官方 SandboxMode 标准值)
|
|
71
|
+
const currentSandbox = displayConfig.agents?.codex?.sandbox ?? 'danger-full-access';
|
|
72
|
+
fields.push({
|
|
73
|
+
key: 'agents.codex.sandbox',
|
|
74
|
+
label: '沙箱模式',
|
|
75
|
+
type: 'select',
|
|
76
|
+
options: ['read-only', 'workspace-write', 'danger-full-access'],
|
|
77
|
+
currentValue: currentSandbox,
|
|
78
|
+
});
|
|
79
|
+
const currentTurnIdleTimeoutMinutes = displayConfig.agents?.codex?.appServer?.turnIdleTimeoutMinutes ??
|
|
80
|
+
DEFAULT_TURN_IDLE_TIMEOUT_MINUTES;
|
|
81
|
+
fields.push({
|
|
82
|
+
key: 'agents.codex.appServer.turnIdleTimeoutMinutes',
|
|
83
|
+
label: 'Turn 空闲超时(分钟, 0关闭)',
|
|
84
|
+
type: 'input',
|
|
85
|
+
currentValue: String(currentTurnIdleTimeoutMinutes),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
fields.push({
|
|
90
|
+
key: 'agents.codex.modeNote',
|
|
91
|
+
label: '命令行模式固定为完全访问(danger-full-access)且无需审批(approval never),不提供审批策略/沙箱配置',
|
|
92
|
+
type: 'note',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
46
95
|
return fields;
|
|
47
96
|
}
|
|
48
97
|
handleFieldChange(key, value, config) {
|
|
@@ -19,6 +19,10 @@ function buildFieldElements(fields, displayConfig) {
|
|
|
19
19
|
else if (field.type === 'select' && field.options) {
|
|
20
20
|
elements.push(buildSelectElement(field, currentValue));
|
|
21
21
|
}
|
|
22
|
+
else if (field.type === 'note') {
|
|
23
|
+
// 纯说明行:无 config.* 回调,只渲染 label 为 markdown 文本。
|
|
24
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: field.label } });
|
|
25
|
+
}
|
|
22
26
|
else {
|
|
23
27
|
elements.push(buildInputElement(field, currentValue));
|
|
24
28
|
}
|
|
@@ -9,7 +9,7 @@ import type { AppConfig } from '../../config/index.js';
|
|
|
9
9
|
export interface ConfigField {
|
|
10
10
|
key: string;
|
|
11
11
|
label: string;
|
|
12
|
-
type: 'boolean' | 'select' | 'input';
|
|
12
|
+
type: 'boolean' | 'select' | 'input' | 'note';
|
|
13
13
|
/** For select type: the list of options shown in the dropdown. */
|
|
14
14
|
options?: readonly string[];
|
|
15
15
|
/** Override the currentValue shown in the card (takes priority over getConfigValue). */
|