@wu529778790/open-im 1.11.8-beta.7 → 1.11.8-beta.9
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/dist/adapters/codebuddy-adapter.js +3 -17
- package/dist/adapters/codex-adapter.js +3 -16
- package/dist/adapters/opencode-adapter.js +3 -16
- package/dist/clawbot/client.js +44 -78
- package/dist/codebuddy/cli-runner.d.ts +2 -22
- package/dist/codebuddy/cli-runner.js +9 -27
- package/dist/codex/cli-runner.d.ts +2 -22
- package/dist/codex/cli-runner.js +24 -75
- package/dist/config-web-auth.d.ts +13 -0
- package/dist/config-web-auth.js +114 -0
- package/dist/config-web-browser.d.ts +4 -0
- package/dist/config-web-browser.js +48 -0
- package/dist/config-web-cors.d.ts +5 -0
- package/dist/config-web-cors.js +48 -0
- package/dist/config-web-health.d.ts +7 -0
- package/dist/config-web-health.js +65 -0
- package/dist/config-web-http.d.ts +3 -0
- package/dist/config-web-http.js +31 -0
- package/dist/config-web-payload.d.ts +84 -0
- package/dist/config-web-payload.js +260 -0
- package/dist/config-web-probes.d.ts +2 -0
- package/dist/config-web-probes.js +271 -0
- package/dist/config-web.d.ts +3 -11
- package/dist/config-web.js +17 -821
- package/dist/opencode/cli-runner.d.ts +2 -22
- package/dist/opencode/cli-runner.js +47 -68
- package/dist/opencode/sdk-runner.d.ts +2 -22
- package/dist/opencode/sdk-runner.js +2 -1
- package/dist/qq/client.js +45 -53
- package/dist/shared/cli-runner-base.d.ts +39 -0
- package/dist/shared/cli-runner-base.js +108 -0
- package/dist/shared/connection-manager.d.ts +111 -0
- package/dist/shared/connection-manager.js +157 -0
- package/dist/shared/session-invalid-detector.d.ts +5 -0
- package/dist/shared/session-invalid-detector.js +20 -0
- package/dist/wework/client.js +79 -127
- package/dist/workbuddy/client.js +40 -74
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runCodeBuddy } from '../codebuddy/cli-runner.js';
|
|
2
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
2
3
|
export class CodeBuddyAdapter {
|
|
3
4
|
cliPath;
|
|
4
5
|
toolId = 'codebuddy';
|
|
@@ -10,25 +11,10 @@ export class CodeBuddyAdapter {
|
|
|
10
11
|
onText: callbacks.onText,
|
|
11
12
|
onThinking: callbacks.onThinking,
|
|
12
13
|
onToolUse: callbacks.onToolUse,
|
|
13
|
-
onComplete:
|
|
14
|
-
const result = {
|
|
15
|
-
success: raw.success,
|
|
16
|
-
result: raw.result,
|
|
17
|
-
accumulated: raw.accumulated,
|
|
18
|
-
cost: raw.cost,
|
|
19
|
-
durationMs: raw.durationMs,
|
|
20
|
-
model: raw.model,
|
|
21
|
-
numTurns: raw.numTurns,
|
|
22
|
-
toolStats: raw.toolStats,
|
|
23
|
-
};
|
|
24
|
-
callbacks.onComplete(result);
|
|
25
|
-
},
|
|
14
|
+
onComplete: callbacks.onComplete,
|
|
26
15
|
onError: (err) => {
|
|
27
16
|
const msg = typeof err === 'string' ? err : String(err);
|
|
28
|
-
const friendly = msg
|
|
29
|
-
msg.includes('Session not found') ||
|
|
30
|
-
msg.includes('Invalid session') ||
|
|
31
|
-
msg.includes('Unable to resume')
|
|
17
|
+
const friendly = isSessionInvalidMessage(msg)
|
|
32
18
|
? 'CodeBuddy 会话已失效,旧 session 已清理。请直接重试当前请求。'
|
|
33
19
|
: msg;
|
|
34
20
|
callbacks.onError(friendly);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Codex Adapter - run tasks through OpenAI Codex CLI (`codex exec`)
|
|
3
3
|
*/
|
|
4
4
|
import { runCodex } from "../codex/cli-runner.js";
|
|
5
|
+
import { isSessionInvalidMessage } from "../shared/session-invalid-detector.js";
|
|
5
6
|
export class CodexAdapter {
|
|
6
7
|
cliPath;
|
|
7
8
|
toolId = "codex";
|
|
@@ -21,24 +22,10 @@ export class CodexAdapter {
|
|
|
21
22
|
onText: callbacks.onText,
|
|
22
23
|
onThinking: callbacks.onThinking,
|
|
23
24
|
onToolUse: callbacks.onToolUse,
|
|
24
|
-
onComplete:
|
|
25
|
-
const result = {
|
|
26
|
-
success: raw.success,
|
|
27
|
-
result: raw.result,
|
|
28
|
-
accumulated: raw.accumulated,
|
|
29
|
-
cost: raw.cost,
|
|
30
|
-
durationMs: raw.durationMs,
|
|
31
|
-
model: raw.model,
|
|
32
|
-
numTurns: raw.numTurns,
|
|
33
|
-
toolStats: raw.toolStats,
|
|
34
|
-
};
|
|
35
|
-
callbacks.onComplete(result);
|
|
36
|
-
},
|
|
25
|
+
onComplete: callbacks.onComplete,
|
|
37
26
|
onError: (err) => {
|
|
38
27
|
const msg = typeof err === "string" ? err : String(err);
|
|
39
|
-
const friendly = msg
|
|
40
|
-
msg.includes("No conversation found") ||
|
|
41
|
-
msg.includes("Unable to find session")
|
|
28
|
+
const friendly = isSessionInvalidMessage(msg)
|
|
42
29
|
? "Codex 会话已失效,旧 session 已清理。请直接重试当前请求。"
|
|
43
30
|
: msg;
|
|
44
31
|
callbacks.onError(friendly);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runOpenCodeSdk } from '../opencode/sdk-runner.js';
|
|
2
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
2
3
|
export class OpenCodeAdapter {
|
|
3
4
|
toolId = 'opencode';
|
|
4
5
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
@@ -6,24 +7,10 @@ export class OpenCodeAdapter {
|
|
|
6
7
|
onText: callbacks.onText,
|
|
7
8
|
onThinking: callbacks.onThinking,
|
|
8
9
|
onToolUse: callbacks.onToolUse,
|
|
9
|
-
onComplete:
|
|
10
|
-
const result = {
|
|
11
|
-
success: raw.success,
|
|
12
|
-
result: raw.result,
|
|
13
|
-
accumulated: raw.accumulated,
|
|
14
|
-
cost: raw.cost,
|
|
15
|
-
durationMs: raw.durationMs,
|
|
16
|
-
model: raw.model,
|
|
17
|
-
numTurns: raw.numTurns,
|
|
18
|
-
toolStats: raw.toolStats,
|
|
19
|
-
};
|
|
20
|
-
callbacks.onComplete(result);
|
|
21
|
-
},
|
|
10
|
+
onComplete: callbacks.onComplete,
|
|
22
11
|
onError: (err) => {
|
|
23
12
|
const msg = typeof err === 'string' ? err : String(err);
|
|
24
|
-
const friendly = msg
|
|
25
|
-
msg.includes('Session not found') ||
|
|
26
|
-
msg.includes('no sessions found')
|
|
13
|
+
const friendly = isSessionInvalidMessage(msg)
|
|
27
14
|
? 'OpenCode 会话已失效,旧 session 已清理。请直接重试当前请求。'
|
|
28
15
|
: msg;
|
|
29
16
|
callbacks.onError(friendly);
|
package/dist/clawbot/client.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
11
|
import { createLogger } from '../logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import { isFatalReconnectError } from '../shared/reconnect.js';
|
|
13
|
+
import { ReconnectManager, HeartbeatMonitor, StateManager } from '../shared/connection-manager.js';
|
|
13
14
|
import { cacheContextToken } from './message-sender.js';
|
|
14
15
|
import { setClawbotContextToken, clearClawbotContextToken } from '../shared/active-chats.js';
|
|
15
16
|
import { createMediaTargetPath } from '../shared/media-storage.js';
|
|
@@ -19,27 +20,30 @@ const log = createLogger('ClawBot');
|
|
|
19
20
|
const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
|
|
20
21
|
const BASE_INFO = { channel_version: '0.1.0' };
|
|
21
22
|
let pollController = null;
|
|
22
|
-
let channelState = 'disconnected';
|
|
23
23
|
let messageHandler = null;
|
|
24
|
-
let stateChangeHandler = null;
|
|
25
|
-
let reconnectTimer = null;
|
|
26
|
-
let watchdogTimer = null;
|
|
27
|
-
let reconnectAttempt = 0;
|
|
28
|
-
let fatal = false;
|
|
29
24
|
let stopped = false;
|
|
30
25
|
let apiUrl = 'https://ilinkai.weixin.qq.com';
|
|
31
26
|
let apiToken = '';
|
|
32
27
|
/** Opaque cursor for getupdates pagination (replaces numeric offset) */
|
|
33
28
|
let getUpdatesBuf = '';
|
|
34
|
-
/** Timestamp of last successful poll response (for watchdog) */
|
|
35
|
-
let lastResponseAt = 0;
|
|
36
29
|
/** Per-request timeout for long-polling (ms) */
|
|
37
30
|
const POLL_REQUEST_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
|
|
38
31
|
/** Watchdog interval: force reconnect if no response for this long (ms) */
|
|
39
32
|
const WATCHDOG_INTERVAL_MS = 60_000; // check every 60s
|
|
40
33
|
const WATCHDOG_STALE_MS = 5 * 60 * 1000; // 5 minutes without response = stale
|
|
34
|
+
// Connection infrastructure (shared managers replace raw timers/counters)
|
|
35
|
+
const stateManager = new StateManager('disconnected');
|
|
36
|
+
const heartbeatMonitor = new HeartbeatMonitor();
|
|
37
|
+
const reconnectManager = new ReconnectManager({
|
|
38
|
+
name: 'ClawBot',
|
|
39
|
+
backoff: { mode: 'stepped', delays: RECONNECT_DELAYS_MS },
|
|
40
|
+
onReconnect: () => {
|
|
41
|
+
stateManager.set('connected');
|
|
42
|
+
startPolling();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
41
45
|
export function getChannelState() {
|
|
42
|
-
return
|
|
46
|
+
return stateManager.current;
|
|
43
47
|
}
|
|
44
48
|
export async function initClawbot(config, eventHandler, onStateChange) {
|
|
45
49
|
const pc = config.platforms?.clawbot;
|
|
@@ -52,14 +56,14 @@ export async function initClawbot(config, eventHandler, onStateChange) {
|
|
|
52
56
|
apiUrl = pc.apiUrl ?? 'https://ilinkai.weixin.qq.com';
|
|
53
57
|
apiToken = pc.apiToken;
|
|
54
58
|
messageHandler = eventHandler;
|
|
55
|
-
|
|
59
|
+
stateManager.setOnChange(onStateChange);
|
|
56
60
|
stopped = false;
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
reconnectManager.reset();
|
|
62
|
+
reconnectManager.resume();
|
|
59
63
|
getUpdatesBuf = '';
|
|
60
64
|
// Start polling directly — no blocking connectivity check.
|
|
61
65
|
// The polling loop handles errors and reconnection internally.
|
|
62
|
-
|
|
66
|
+
stateManager.set('connected');
|
|
63
67
|
startPolling();
|
|
64
68
|
log.info('ClawBot client initialized');
|
|
65
69
|
}
|
|
@@ -82,18 +86,18 @@ function startPolling() {
|
|
|
82
86
|
base_info: BASE_INFO,
|
|
83
87
|
}, combinedSignal);
|
|
84
88
|
// Record successful response time for watchdog
|
|
85
|
-
|
|
89
|
+
heartbeatMonitor.recordResponse();
|
|
86
90
|
if (signal.aborted)
|
|
87
91
|
break;
|
|
88
92
|
if (!res.ok) {
|
|
89
93
|
// Detect fatal errors (e.g. errcode -14 "session timeout") — retrying won't help
|
|
90
94
|
if (res.errcode === -14 || isFatalReconnectError(res.error)) {
|
|
91
95
|
log.warn(`ClawBot fatal error (errcode=${res.errcode}), entering slow-probe mode`);
|
|
92
|
-
|
|
96
|
+
reconnectManager.setFatal(true);
|
|
93
97
|
getUpdatesBuf = ''; // session expired, cursor is stale
|
|
94
98
|
clearClawbotContextToken(); // session expired, token is stale
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
stateManager.set('error');
|
|
100
|
+
reconnectManager.schedule();
|
|
97
101
|
return;
|
|
98
102
|
}
|
|
99
103
|
log.warn(`ClawBot getupdates error: ${res.error ?? 'unknown'}`);
|
|
@@ -101,10 +105,9 @@ function startPolling() {
|
|
|
101
105
|
continue;
|
|
102
106
|
}
|
|
103
107
|
// Successful response — clear fatal mode and reset backoff
|
|
104
|
-
if (fatal ||
|
|
108
|
+
if (reconnectManager.fatal || reconnectManager.attemptCount > 0) {
|
|
105
109
|
log.info('ClawBot connection recovered');
|
|
106
|
-
|
|
107
|
-
reconnectAttempt = 0;
|
|
110
|
+
reconnectManager.reset();
|
|
108
111
|
}
|
|
109
112
|
// Update cursor for next poll
|
|
110
113
|
if (res.updatesBuf) {
|
|
@@ -189,69 +192,35 @@ function startPolling() {
|
|
|
189
192
|
if (err instanceof Error && err.name === 'AbortError')
|
|
190
193
|
break;
|
|
191
194
|
log.error('ClawBot polling error:', err);
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
stateManager.set('error');
|
|
196
|
+
reconnectManager.schedule();
|
|
194
197
|
return;
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
})();
|
|
198
201
|
}
|
|
199
|
-
function scheduleReconnect() {
|
|
200
|
-
if (stopped)
|
|
201
|
-
return;
|
|
202
|
-
if (reconnectTimer) {
|
|
203
|
-
clearTimeout(reconnectTimer);
|
|
204
|
-
reconnectTimer = null;
|
|
205
|
-
}
|
|
206
|
-
const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
207
|
-
const delay = fatal ? jitteredDelay(SLOW_PROBE_MS) : jitteredDelay(baseDelay);
|
|
208
|
-
reconnectAttempt++;
|
|
209
|
-
if (fatal) {
|
|
210
|
-
log.warn(`ClawBot fatal error, slow-probe in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempt})...`);
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
log.info(`ClawBot reconnecting in ${delay}ms (attempt ${reconnectAttempt})...`);
|
|
214
|
-
}
|
|
215
|
-
reconnectTimer = setTimeout(() => {
|
|
216
|
-
reconnectTimer = null;
|
|
217
|
-
if (stopped)
|
|
218
|
-
return;
|
|
219
|
-
updateState('connected');
|
|
220
|
-
startPolling();
|
|
221
|
-
}, delay);
|
|
222
|
-
}
|
|
223
|
-
function updateState(state) {
|
|
224
|
-
channelState = state;
|
|
225
|
-
stateChangeHandler?.(state);
|
|
226
|
-
log.debug(`ClawBot state: ${state}`);
|
|
227
|
-
}
|
|
228
202
|
/**
|
|
229
203
|
* Watchdog: periodically check if the poll loop is alive.
|
|
230
204
|
* After Mac sleep or network drop, the fetch may hang without throwing.
|
|
231
205
|
* If no successful response for WATCHDOG_STALE_MS, force a reconnect.
|
|
232
206
|
*/
|
|
233
207
|
function startWatchdog() {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
watchdogTimer = setInterval(() => {
|
|
237
|
-
if (stopped)
|
|
238
|
-
return;
|
|
239
|
-
const elapsed = Date.now() - lastResponseAt;
|
|
240
|
-
if (elapsed > WATCHDOG_STALE_MS) {
|
|
241
|
-
log.warn(`ClawBot watchdog: no response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
|
|
242
|
-
if (pollController) {
|
|
243
|
-
pollController.abort();
|
|
244
|
-
pollController = null;
|
|
245
|
-
}
|
|
246
|
-
updateState('error');
|
|
247
|
-
scheduleReconnect();
|
|
248
|
-
}
|
|
249
|
-
}, WATCHDOG_INTERVAL_MS);
|
|
208
|
+
heartbeatMonitor.recordResponse();
|
|
209
|
+
heartbeatMonitor.start(WATCHDOG_INTERVAL_MS, watchdogTick);
|
|
250
210
|
}
|
|
251
|
-
function
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
211
|
+
function watchdogTick() {
|
|
212
|
+
if (stopped)
|
|
213
|
+
return;
|
|
214
|
+
if (heartbeatMonitor.isStale(WATCHDOG_STALE_MS)) {
|
|
215
|
+
const elapsed = Date.now() - heartbeatMonitor.lastResponseTime;
|
|
216
|
+
log.warn(`ClawBot watchdog: no response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
|
|
217
|
+
heartbeatMonitor.stop();
|
|
218
|
+
if (pollController) {
|
|
219
|
+
pollController.abort();
|
|
220
|
+
pollController = null;
|
|
221
|
+
}
|
|
222
|
+
stateManager.set('error');
|
|
223
|
+
reconnectManager.schedule();
|
|
255
224
|
}
|
|
256
225
|
}
|
|
257
226
|
/**
|
|
@@ -441,13 +410,10 @@ export function stopClawbot() {
|
|
|
441
410
|
pollController.abort();
|
|
442
411
|
pollController = null;
|
|
443
412
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
reconnectTimer = null;
|
|
447
|
-
}
|
|
448
|
-
stopWatchdog();
|
|
413
|
+
reconnectManager.stop();
|
|
414
|
+
heartbeatMonitor.stop();
|
|
449
415
|
messageHandler = null;
|
|
450
416
|
// Don't clear context_token here — it's persisted for startup notifications
|
|
451
|
-
|
|
417
|
+
stateManager.set('disconnected');
|
|
452
418
|
log.info('ClawBot client stopped');
|
|
453
419
|
}
|
|
@@ -1,29 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
onText: (accumulated: string) => void;
|
|
3
|
-
onThinking?: (accumulated: string) => void;
|
|
4
|
-
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
5
|
-
onComplete: (result: {
|
|
6
|
-
success: boolean;
|
|
7
|
-
result: string;
|
|
8
|
-
accumulated: string;
|
|
9
|
-
cost: number;
|
|
10
|
-
durationMs: number;
|
|
11
|
-
model?: string;
|
|
12
|
-
numTurns: number;
|
|
13
|
-
toolStats: Record<string, number>;
|
|
14
|
-
}) => void;
|
|
15
|
-
onError: (error: string) => void;
|
|
16
|
-
onSessionId?: (sessionId: string) => void;
|
|
17
|
-
onSessionInvalid?: () => void;
|
|
18
|
-
}
|
|
1
|
+
import type { RunCallbacks, RunHandle } from '../adapters/tool-adapter.interface.js';
|
|
19
2
|
export interface CodeBuddyRunOptions {
|
|
20
3
|
skipPermissions?: boolean;
|
|
21
4
|
permissionMode?: 'default' | 'acceptEdits' | 'plan';
|
|
22
5
|
model?: string;
|
|
23
6
|
}
|
|
24
|
-
export interface CodeBuddyRunHandle {
|
|
25
|
-
abort: () => void;
|
|
26
|
-
}
|
|
27
7
|
export declare function buildCodeBuddyArgs(prompt: string, sessionId: string | undefined, options?: CodeBuddyRunOptions): string[];
|
|
28
8
|
export declare function extractBufferedPayloads(state: {
|
|
29
9
|
buffer: string;
|
|
@@ -41,4 +21,4 @@ export declare function flushBufferedPayloads(state: {
|
|
|
41
21
|
* - 其他情况 → 拼接(不丢内容)
|
|
42
22
|
*/
|
|
43
23
|
export declare function mergeAssistantReply(previous: string, incoming: string): string;
|
|
44
|
-
export declare function runCodeBuddy(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks:
|
|
24
|
+
export declare function runCodeBuddy(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: CodeBuddyRunOptions): RunHandle;
|
|
@@ -2,8 +2,9 @@ import { execFileSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import { accessSync, constants } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join } from 'node:path';
|
|
4
4
|
import { createLogger } from '../logger.js';
|
|
5
|
-
import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
|
|
6
5
|
import { killProcessTree, trackChild } from '../shared/process-kill.js';
|
|
6
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
7
|
+
import { clearWallClockTimeout, buildBaseSpawnOptions } from '../shared/cli-runner-base.js';
|
|
7
8
|
const log = createLogger('CodeBuddyCli');
|
|
8
9
|
export function buildCodeBuddyArgs(prompt, sessionId, options) {
|
|
9
10
|
const args = ['--print', '--output-format', 'stream-json'];
|
|
@@ -190,24 +191,12 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
190
191
|
...options,
|
|
191
192
|
permissionMode: normalizePermissionMode(options?.permissionMode),
|
|
192
193
|
});
|
|
193
|
-
const env = processEnvForNonClaudeCliChild();
|
|
194
|
-
if (process.platform === 'win32') {
|
|
195
|
-
env.LANG = env.LANG || 'C.UTF-8';
|
|
196
|
-
env.LC_ALL = env.LC_ALL || 'C.UTF-8';
|
|
197
|
-
}
|
|
198
194
|
const isCmd = process.platform === 'win32' && (/\.(cmd|bat)$/i.test(normalizedCliPath) ||
|
|
199
195
|
normalizedCliPath === 'codebuddy');
|
|
200
196
|
const spawnCmd = isCmd ? 'cmd.exe' : normalizedCliPath;
|
|
201
197
|
const spawnArgs = isCmd ? ['/c', normalizedCliPath, ...args] : args;
|
|
202
198
|
log.info(`Spawning CodeBuddy CLI: path=${normalizedCliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${args.join(' ')}`);
|
|
203
|
-
const child = spawn(spawnCmd, spawnArgs,
|
|
204
|
-
cwd: workDir,
|
|
205
|
-
// Unix 上放入独立进程组,使 abort/关停能用 process.kill(-pid) 杀掉整棵子树(含 MCP/shell 孙进程)
|
|
206
|
-
detached: process.platform !== 'win32',
|
|
207
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
208
|
-
env,
|
|
209
|
-
windowsHide: process.platform === 'win32',
|
|
210
|
-
});
|
|
199
|
+
const child = spawn(spawnCmd, spawnArgs, buildBaseSpawnOptions(workDir, ['ignore', 'pipe', 'pipe']));
|
|
211
200
|
trackChild(child);
|
|
212
201
|
let cliTimeoutHandle = null;
|
|
213
202
|
let accumulated = '';
|
|
@@ -222,7 +211,7 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
222
211
|
const MAX_STDERR = 8 * 1024;
|
|
223
212
|
let stderrText = '';
|
|
224
213
|
const handleErrorText = (message) => {
|
|
225
|
-
if (sessionId &&
|
|
214
|
+
if (sessionId && isSessionInvalidMessage(message)) {
|
|
226
215
|
callbacks.onSessionInvalid?.();
|
|
227
216
|
}
|
|
228
217
|
callbacks.onError(message);
|
|
@@ -317,10 +306,8 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
317
306
|
}
|
|
318
307
|
});
|
|
319
308
|
child.on('close', (code) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
cliTimeoutHandle = null;
|
|
323
|
-
}
|
|
309
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
310
|
+
cliTimeoutHandle = null;
|
|
324
311
|
if (completed)
|
|
325
312
|
return;
|
|
326
313
|
if (stdoutState.buffer.trim()) {
|
|
@@ -353,10 +340,8 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
353
340
|
});
|
|
354
341
|
});
|
|
355
342
|
child.on('error', (err) => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
cliTimeoutHandle = null;
|
|
359
|
-
}
|
|
343
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
344
|
+
cliTimeoutHandle = null;
|
|
360
345
|
if (completed)
|
|
361
346
|
return;
|
|
362
347
|
completed = true;
|
|
@@ -378,10 +363,7 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
378
363
|
if (completed)
|
|
379
364
|
return;
|
|
380
365
|
completed = true;
|
|
381
|
-
|
|
382
|
-
clearTimeout(cliTimeoutHandle);
|
|
383
|
-
cliTimeoutHandle = null;
|
|
384
|
-
}
|
|
366
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
385
367
|
killProcessTree(child);
|
|
386
368
|
},
|
|
387
369
|
};
|
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codex CLI runner for `codex exec --json` JSONL output.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
onText: (accumulated: string) => void;
|
|
6
|
-
onThinking?: (accumulated: string) => void;
|
|
7
|
-
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
8
|
-
onComplete: (result: {
|
|
9
|
-
success: boolean;
|
|
10
|
-
result: string;
|
|
11
|
-
accumulated: string;
|
|
12
|
-
cost: number;
|
|
13
|
-
durationMs: number;
|
|
14
|
-
model?: string;
|
|
15
|
-
numTurns: number;
|
|
16
|
-
toolStats: Record<string, number>;
|
|
17
|
-
}) => void;
|
|
18
|
-
onError: (error: string) => void;
|
|
19
|
-
onSessionId?: (sessionId: string) => void;
|
|
20
|
-
onSessionInvalid?: () => void;
|
|
21
|
-
}
|
|
4
|
+
import type { RunCallbacks, RunHandle } from '../adapters/tool-adapter.interface.js';
|
|
22
5
|
export interface CodexRunOptions {
|
|
23
6
|
skipPermissions?: boolean;
|
|
24
7
|
permissionMode?: 'default' | 'acceptEdits' | 'plan';
|
|
@@ -27,9 +10,6 @@ export interface CodexRunOptions {
|
|
|
27
10
|
hookPort?: number;
|
|
28
11
|
proxy?: string;
|
|
29
12
|
}
|
|
30
|
-
export interface CodexRunHandle {
|
|
31
|
-
abort: () => void;
|
|
32
|
-
}
|
|
33
13
|
export declare function extractPromptImagePaths(prompt: string): string[];
|
|
34
14
|
export declare function buildCodexArgs(prompt: string, sessionId: string | undefined, workDir: string, options?: CodexRunOptions): string[];
|
|
35
|
-
export declare function runCodex(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks:
|
|
15
|
+
export declare function runCodex(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: CodexRunOptions): RunHandle;
|
package/dist/codex/cli-runner.js
CHANGED
|
@@ -6,8 +6,9 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
|
-
import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
|
|
10
9
|
import { killProcessTree, trackChild } from '../shared/process-kill.js';
|
|
10
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
11
|
+
import { createStderrBuffer, appendStderr, reconstructStderr, createFinalizeGate, isFinalizeReady, clearWallClockTimeout, buildBaseSpawnOptions, } from '../shared/cli-runner-base.js';
|
|
11
12
|
const log = createLogger('CodexCli');
|
|
12
13
|
const windowsCodexLaunchCache = new Map();
|
|
13
14
|
const SUPPORTED_IMAGE_EXTENSIONS = new Set([
|
|
@@ -163,22 +164,18 @@ function resolveWindowsCodexLaunch(cliPath, args) {
|
|
|
163
164
|
}
|
|
164
165
|
export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options) {
|
|
165
166
|
const args = buildCodexArgs(prompt, sessionId, workDir, options);
|
|
166
|
-
const
|
|
167
|
+
const extraEnv = {};
|
|
167
168
|
if (options?.chatId)
|
|
168
|
-
|
|
169
|
+
extraEnv.CC_IM_CHAT_ID = options.chatId;
|
|
169
170
|
if (options?.hookPort)
|
|
170
|
-
|
|
171
|
+
extraEnv.CC_IM_HOOK_PORT = String(options.hookPort);
|
|
171
172
|
if (options?.proxy) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
if (process.platform === 'win32') {
|
|
180
|
-
env.LANG = env.LANG || 'C.UTF-8';
|
|
181
|
-
env.LC_ALL = env.LC_ALL || 'C.UTF-8';
|
|
173
|
+
extraEnv.HTTPS_PROXY = options.proxy;
|
|
174
|
+
extraEnv.HTTP_PROXY = options.proxy;
|
|
175
|
+
extraEnv.https_proxy = options.proxy;
|
|
176
|
+
extraEnv.http_proxy = options.proxy;
|
|
177
|
+
extraEnv.ALL_PROXY = options.proxy;
|
|
178
|
+
extraEnv.all_proxy = options.proxy;
|
|
182
179
|
}
|
|
183
180
|
const argsForLog = args.join(' ');
|
|
184
181
|
log.info(`Spawning Codex CLI: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${argsForLog}`);
|
|
@@ -200,14 +197,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
200
197
|
`chcp 65001>nul && ${formatWindowsCommandName(cliPath)} ${args.map(quoteForWindowsCmd).join(' ')}`,
|
|
201
198
|
]
|
|
202
199
|
: args;
|
|
203
|
-
const child = spawn(spawnCmd, spawnArgs,
|
|
204
|
-
cwd: workDir,
|
|
205
|
-
// Unix 上放入独立进程组,使 abort/关停能用 process.kill(-pid) 杀掉整棵子树(含 MCP/shell 孙进程)
|
|
206
|
-
detached: process.platform !== 'win32',
|
|
207
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
208
|
-
env,
|
|
209
|
-
windowsHide: process.platform === 'win32',
|
|
210
|
-
});
|
|
200
|
+
const child = spawn(spawnCmd, spawnArgs, buildBaseSpawnOptions(workDir, ['pipe', 'pipe', 'pipe'], extraEnv));
|
|
211
201
|
trackChild(child);
|
|
212
202
|
child.stdin?.write(prompt);
|
|
213
203
|
child.stdin?.end();
|
|
@@ -217,27 +207,10 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
217
207
|
const toolStats = {};
|
|
218
208
|
const startTime = Date.now();
|
|
219
209
|
const rl = createInterface({ input: child.stdout });
|
|
220
|
-
const
|
|
221
|
-
const MAX_STDERR_TAIL = 6 * 1024;
|
|
222
|
-
let stderrHead = '';
|
|
223
|
-
let stderrTail = '';
|
|
224
|
-
let stderrTotal = 0;
|
|
225
|
-
let stderrHeadFull = false;
|
|
210
|
+
const stderrBuf = createStderrBuffer();
|
|
226
211
|
child.stderr?.on('data', (chunk) => {
|
|
227
212
|
const text = chunk.toString();
|
|
228
|
-
|
|
229
|
-
if (!stderrHeadFull) {
|
|
230
|
-
const room = MAX_STDERR_HEAD - stderrHead.length;
|
|
231
|
-
if (room > 0) {
|
|
232
|
-
stderrHead += text.slice(0, room);
|
|
233
|
-
if (stderrHead.length >= MAX_STDERR_HEAD)
|
|
234
|
-
stderrHeadFull = true;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
stderrTail += text;
|
|
238
|
-
if (stderrTail.length > MAX_STDERR_TAIL) {
|
|
239
|
-
stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
|
|
240
|
-
}
|
|
213
|
+
appendStderr(stderrBuf, text);
|
|
241
214
|
log.debug(`[stderr] ${text.trimEnd()}`);
|
|
242
215
|
});
|
|
243
216
|
rl.on('line', (line) => {
|
|
@@ -329,38 +302,18 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
329
302
|
}
|
|
330
303
|
});
|
|
331
304
|
let exitCode = null;
|
|
332
|
-
|
|
333
|
-
let childClosed = false;
|
|
305
|
+
const gate = createFinalizeGate();
|
|
334
306
|
let cliTimeoutHandle = null;
|
|
335
307
|
const finalize = () => {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
if (!rlClosed || !childClosed)
|
|
308
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
309
|
+
cliTimeoutHandle = null;
|
|
310
|
+
if (!isFinalizeReady(gate))
|
|
341
311
|
return;
|
|
342
312
|
if (completed)
|
|
343
313
|
return;
|
|
344
314
|
if (exitCode !== null && exitCode !== 0) {
|
|
345
|
-
|
|
346
|
-
if (
|
|
347
|
-
if (!stderrHeadFull) {
|
|
348
|
-
errMsg = stderrHead;
|
|
349
|
-
}
|
|
350
|
-
else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
|
|
351
|
-
errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
errMsg =
|
|
355
|
-
stderrHead +
|
|
356
|
-
`\n\n... (omitted ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} bytes) ...\n\n` +
|
|
357
|
-
stderrTail;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (sessionId &&
|
|
361
|
-
(errMsg.includes('No session found') ||
|
|
362
|
-
errMsg.includes('No conversation found') ||
|
|
363
|
-
errMsg.includes('Unable to find session'))) {
|
|
315
|
+
const errMsg = reconstructStderr(stderrBuf);
|
|
316
|
+
if (sessionId && isSessionInvalidMessage(errMsg)) {
|
|
364
317
|
callbacks.onSessionInvalid?.();
|
|
365
318
|
}
|
|
366
319
|
callbacks.onError(errMsg || `Codex CLI exited with code ${exitCode}`);
|
|
@@ -379,11 +332,11 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
379
332
|
child.on('close', (code) => {
|
|
380
333
|
log.info(`Codex CLI closed: exitCode=${code}, pid=${child.pid}`);
|
|
381
334
|
exitCode = code;
|
|
382
|
-
childClosed = true;
|
|
335
|
+
gate.childClosed = true;
|
|
383
336
|
finalize();
|
|
384
337
|
});
|
|
385
338
|
rl.on('close', () => {
|
|
386
|
-
rlClosed = true;
|
|
339
|
+
gate.rlClosed = true;
|
|
387
340
|
finalize();
|
|
388
341
|
});
|
|
389
342
|
child.on('error', (err) => {
|
|
@@ -393,7 +346,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
393
346
|
completed = true;
|
|
394
347
|
callbacks.onError(`Failed to start Codex CLI: ${err.message}`);
|
|
395
348
|
}
|
|
396
|
-
childClosed = true;
|
|
349
|
+
gate.childClosed = true;
|
|
397
350
|
finalize();
|
|
398
351
|
});
|
|
399
352
|
// 墙钟超时:防止 CLI 挂死(网络卡住、工具死循环等)永久占用用户队列槽
|
|
@@ -401,7 +354,6 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
401
354
|
cliTimeoutHandle = setTimeout(() => {
|
|
402
355
|
if (completed)
|
|
403
356
|
return;
|
|
404
|
-
log.warn(`Codex CLI 超过 ${cliTimeoutMs}ms,强制终止 (pid=${child.pid})`);
|
|
405
357
|
completed = true;
|
|
406
358
|
rl.close();
|
|
407
359
|
killProcessTree(child);
|
|
@@ -413,10 +365,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
413
365
|
if (completed)
|
|
414
366
|
return;
|
|
415
367
|
completed = true;
|
|
416
|
-
|
|
417
|
-
clearTimeout(cliTimeoutHandle);
|
|
418
|
-
cliTimeoutHandle = null;
|
|
419
|
-
}
|
|
368
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
420
369
|
rl.close();
|
|
421
370
|
killProcessTree(child);
|
|
422
371
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
export interface LoginTokenInfo {
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function getWebConfigHost(): string;
|
|
6
|
+
/** 设为 true 时,非本机绑定的 Web 配置服务跳过登录 Cookie 校验(仅适用于受信网络;生产建议配合 HTTPS 反向代理) */
|
|
7
|
+
export declare function allowRemoteApiWithoutAuth(): boolean;
|
|
8
|
+
export declare function consumeLoginToken(loginToken: string): LoginTokenInfo | undefined;
|
|
9
|
+
export declare function createLoginToken(ttlMs: number): string;
|
|
10
|
+
export declare function createSession(request: IncomingMessage, ttlMs: number): string;
|
|
11
|
+
export declare function isSessionValid(request: IncomingMessage): boolean;
|
|
12
|
+
export declare function buildSessionCookie(sessionId: string, ttlMs: number): string;
|
|
13
|
+
export declare function generateLoginUrl(host: string, port: number, loginTtlMs: number): string;
|