lark-remote 0.1.4 → 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 +38 -0
- package/README.en.md +0 -1
- package/README.md +0 -1
- 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 +423 -20
- 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 +78 -7
- package/dist/card/run-state.d.ts +25 -3
- package/dist/card/run-state.js +210 -2
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +4 -0
- package/dist/config/index.d.ts +43 -0
- package/dist/config/index.js +25 -1
- package/dist/connector/index.d.ts +5 -0
- package/dist/connector/index.js +32 -0
- package/dist/index.js +95 -23
- 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/index.d.ts +0 -10
- package/dist/router/config/index.js +0 -17
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/index.d.ts +61 -1
- package/dist/router/index.js +323 -48
- 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/argv.d.ts +1 -1
- package/dist/runner/codex/argv.js +1 -1
- 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/runner-utils.d.ts +12 -0
- package/dist/runner/common/runner-utils.js +21 -0
- package/dist/runner/common/spawning-runner.js +9 -1
- 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/claude/session-index.d.ts +93 -0
- package/dist/session/claude/session-index.js +374 -0
- package/dist/session/claude/sessions.d.ts +0 -18
- package/dist/session/claude/sessions.js +102 -319
- package/dist/session/codex/rollout-reader.js +46 -3
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +11 -0
- package/dist/update/install.d.ts +29 -0
- package/dist/update/install.js +93 -0
- package/dist/update/startup-hint.d.ts +5 -0
- package/dist/update/startup-hint.js +11 -0
- package/dist/update/version-check.d.ts +31 -0
- package/dist/update/version-check.js +149 -0
- package/package.json +1 -14
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')
|
|
@@ -61,11 +257,23 @@ function reduceSystemEvent(state, event) {
|
|
|
61
257
|
function reduceResultEvent(state, event) {
|
|
62
258
|
if (event.type !== 'result')
|
|
63
259
|
return state;
|
|
260
|
+
// Pre-init result guard: Claude CLI --resume emits a historical result (from
|
|
261
|
+
// the previous turn) before sending system.init for the new run. Without this
|
|
262
|
+
// guard, that stale result would prematurely transition to 'finalizing',
|
|
263
|
+
// freezing the card at "⏳ 等待进程退出" for the entire run duration.
|
|
264
|
+
// sessionId is set by system.init; undefined means init hasn't arrived yet.
|
|
265
|
+
if (state.sessionId === undefined)
|
|
266
|
+
return state;
|
|
64
267
|
const resultEvent = event;
|
|
65
268
|
const incomingSubtype = event.subtype;
|
|
66
269
|
const incomingErrorMsg = incomingSubtype === 'error' ? (resultEvent.errorMessage ?? 'Agent 返回错误结果') : undefined;
|
|
67
|
-
// 合并 subtype:error
|
|
68
|
-
|
|
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';
|
|
69
277
|
// 合并 errorMsg:首个非 undefined 胜出(existing ?? incoming)
|
|
70
278
|
const mergedErrorMsg = state.errorMsg ?? incomingErrorMsg;
|
|
71
279
|
return {
|
package/dist/config/dir.d.ts
CHANGED
package/dist/config/dir.js
CHANGED
|
@@ -52,6 +52,9 @@ export function parseCliArgs(args = process.argv.slice(2)) {
|
|
|
52
52
|
else if (arg === '--dev') {
|
|
53
53
|
result.dev = true;
|
|
54
54
|
}
|
|
55
|
+
else if (arg === '--update') {
|
|
56
|
+
result.update = true;
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
59
|
return result;
|
|
57
60
|
}
|
|
@@ -77,6 +80,7 @@ export function printHelp() {
|
|
|
77
80
|
' --config-dir <path> 自定义配置目录(默认 ~/.lark-remote,可用于同机多实例)',
|
|
78
81
|
' --settings <path> 指定 Claude 配置文件路径',
|
|
79
82
|
' --dev 开发模式:标记从源码 bun src/index.ts 启动(看门狗据此选择拉起方式)',
|
|
83
|
+
' --update 升级到最新版本后退出(用于 cron/脚本自动化升级)',
|
|
80
84
|
' -h, --help 显示本帮助信息',
|
|
81
85
|
' -v, --version 显示版本号',
|
|
82
86
|
];
|
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>;
|
|
@@ -151,6 +193,7 @@ export declare const AppConfigSchema: z.ZodObject<{
|
|
|
151
193
|
pi: "pi";
|
|
152
194
|
kimi: "kimi";
|
|
153
195
|
}>>;
|
|
196
|
+
checkUpdateOnStartup: z.ZodDefault<z.ZodBoolean>;
|
|
154
197
|
}, z.core.$strip>;
|
|
155
198
|
export type AppConfig = z.infer<typeof AppConfigSchema>;
|
|
156
199
|
export declare function loadConfig(configPath: string): AppConfig;
|
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({
|
|
@@ -153,6 +175,8 @@ export const AppConfigSchema = z.object({
|
|
|
153
175
|
logging: LoggingConfigSchema.default(LoggingConfigSchema.parse({})),
|
|
154
176
|
/** Default agent for new runs. */
|
|
155
177
|
defaultAgent: z.enum(['claude', 'codex', 'opencode', 'pi', 'kimi']).default('claude'),
|
|
178
|
+
/** Check for updates on bridge startup (default: false). */
|
|
179
|
+
checkUpdateOnStartup: z.boolean().default(false),
|
|
156
180
|
});
|
|
157
181
|
const TEMPLATE = `feishu:
|
|
158
182
|
appId: ""
|
|
@@ -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
|
@@ -3,7 +3,20 @@ import { getLogger } from '../logger/index.js';
|
|
|
3
3
|
import { MAX_FILE_UPLOAD_SIZE } from './file-limits.js';
|
|
4
4
|
import axios from 'axios';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
|
+
import https from 'node:https';
|
|
6
7
|
import FormData from 'form-data';
|
|
8
|
+
/**
|
|
9
|
+
* Dedicated https.Agent with keepAlive disabled for sendFile's three axios
|
|
10
|
+
* calls (token, upload, send). Bun's Node.js http compat layer has a bug
|
|
11
|
+
* where it reuses keep-alive sockets that the server has already RST'd —
|
|
12
|
+
* the write goes into a dead socket and the caller gets ECONNRESET after
|
|
13
|
+
* ~30s. Node.js proper does not exhibit this because it checks socket
|
|
14
|
+
* health before reuse. Disabling keep-alive forces a fresh TCP+TLS
|
|
15
|
+
* handshake per request, eliminating the stale-socket path at the cost of
|
|
16
|
+
* one extra round-trip per file send (acceptable for an infrequent
|
|
17
|
+
* operation).
|
|
18
|
+
*/
|
|
19
|
+
const noKeepAliveAgent = new https.Agent({ keepAlive: false });
|
|
7
20
|
/**
|
|
8
21
|
* dedup 缓存 TTL(毫秒)。控制飞书事件去重窗口,全局作用于 message + cardAction。
|
|
9
22
|
*
|
|
@@ -251,6 +264,18 @@ export class FeishuConnector {
|
|
|
251
264
|
getLogger().error('[feishu] addReaction failed:', this.formatError(err));
|
|
252
265
|
}
|
|
253
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
|
+
}
|
|
254
279
|
/**
|
|
255
280
|
* Upload a file to Feishu and send it to the specified chat.
|
|
256
281
|
* Uses the im/v1/files upload API.
|
|
@@ -288,18 +313,22 @@ export class FeishuConnector {
|
|
|
288
313
|
form.append('file_name', fileName);
|
|
289
314
|
form.append('file_type', 'stream');
|
|
290
315
|
// P2-18: timeout on the upload (large file, slow link) — 120s.
|
|
316
|
+
// httpsAgent: noKeepAlive — prevents Bun from reusing a server-RST'd
|
|
317
|
+
// keep-alive socket (ECONNRESET after ~30s; see noKeepAliveAgent docs).
|
|
291
318
|
const uploadResp = await axios.post('https://open.feishu.cn/open-apis/im/v1/files', form, {
|
|
292
319
|
headers: {
|
|
293
320
|
...form.getHeaders(),
|
|
294
321
|
Authorization: `Bearer ${accessToken}`,
|
|
295
322
|
},
|
|
296
323
|
timeout: 120000,
|
|
324
|
+
httpsAgent: noKeepAliveAgent,
|
|
297
325
|
});
|
|
298
326
|
if (uploadResp.data.code !== 0) {
|
|
299
327
|
throw new Error(`File upload failed: ${uploadResp.data.msg}`);
|
|
300
328
|
}
|
|
301
329
|
const fileKey = uploadResp.data.data.file_key;
|
|
302
330
|
// Send file message. P2-18: timeout 30s.
|
|
331
|
+
// httpsAgent: noKeepAlive — same Bun keep-alive fix as upload above.
|
|
303
332
|
const sendResp = await axios.post('https://open.feishu.cn/open-apis/im/v1/messages', {
|
|
304
333
|
receive_id: chatId,
|
|
305
334
|
msg_type: 'file',
|
|
@@ -311,6 +340,7 @@ export class FeishuConnector {
|
|
|
311
340
|
},
|
|
312
341
|
params: { receive_id_type: 'chat_id' },
|
|
313
342
|
timeout: 30000,
|
|
343
|
+
httpsAgent: noKeepAliveAgent,
|
|
314
344
|
});
|
|
315
345
|
if (sendResp.data.code !== 0) {
|
|
316
346
|
throw new Error(`File message failed: ${sendResp.data.msg}`);
|
|
@@ -347,12 +377,14 @@ export class FeishuConnector {
|
|
|
347
377
|
return this.cachedToken;
|
|
348
378
|
}
|
|
349
379
|
// P2-18: timeout 30s on the token request.
|
|
380
|
+
// httpsAgent: noKeepAlive — same Bun keep-alive fix as upload above.
|
|
350
381
|
const tokenResp = await axios.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
|
|
351
382
|
app_id: this.appId,
|
|
352
383
|
app_secret: this.appSecret,
|
|
353
384
|
}, {
|
|
354
385
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
355
386
|
timeout: 30000,
|
|
387
|
+
httpsAgent: noKeepAliveAgent,
|
|
356
388
|
});
|
|
357
389
|
if (tokenResp.data.code !== 0) {
|
|
358
390
|
throw new Error(`Failed to get tenant_access_token: ${tokenResp.data.msg ?? 'unknown error'}`);
|