@wu529778790/open-im 1.11.8-beta.2 → 1.11.8-beta.21
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.d.ts +0 -5
- package/dist/adapters/opencode-adapter.js +10 -25
- package/dist/adapters/registry.js +10 -1
- package/dist/clawbot/client.js +72 -78
- package/dist/clawbot/qr-login.d.ts +1 -0
- package/dist/clawbot/qr-login.js +7 -0
- 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/commands/handler.d.ts +1 -0
- package/dist/commands/handler.js +34 -0
- package/dist/config/types.d.ts +23 -0
- 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-page-i18n.d.ts +36 -2
- package/dist/config-web-page-i18n.js +36 -2
- 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 +45 -815
- package/dist/config.js +22 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/opencode/cli-runner.d.ts +2 -22
- package/dist/opencode/cli-runner.js +47 -68
- package/dist/opencode/sdk-manager.d.ts +11 -0
- package/dist/opencode/sdk-manager.js +46 -0
- package/dist/opencode/sdk-runner.d.ts +7 -0
- package/dist/opencode/sdk-runner.js +225 -0
- package/dist/platform/handle-ai-request.js +18 -1
- package/dist/qq/client.js +45 -53
- package/dist/shared/ai-task.d.ts +15 -0
- package/dist/shared/ai-task.js +174 -2
- 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 +4 -1
- package/web/dist/assets/index-6e51Wtaa.css +1 -0
- package/web/dist/assets/index-Da9qKWA2.js +57 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-XKVTb-0p.css +0 -1
- package/web/dist/assets/index-yDVGC_84.js +0 -57
package/dist/config.js
CHANGED
|
@@ -8,7 +8,7 @@ import { accessSync, constants } from 'node:fs';
|
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { join, isAbsolute } from 'node:path';
|
|
10
10
|
import { createLogger } from './logger.js';
|
|
11
|
-
import { APP_HOME, } from './constants.js';
|
|
11
|
+
import { APP_HOME, AUTOPILOT_ENABLED_DEFAULT, AUTOPILOT_MAX_RETRIES, AUTOPILOT_DEFAULT_INTERVAL_HOURS, AUTOPILOT_SHORT_RETRY_SECONDS, AUTOPILOT_DEFAULT_PROMPT, } from './constants.js';
|
|
12
12
|
const log = createLogger('config');
|
|
13
13
|
import { isAiCommand, AI_TOOL_BY_ID } from './adapters/tool-registry.js';
|
|
14
14
|
/**
|
|
@@ -200,6 +200,24 @@ export function loadConfig() {
|
|
|
200
200
|
const tc = file.tools?.claude ?? {};
|
|
201
201
|
const tcod = file.tools?.codex ?? {};
|
|
202
202
|
const tcb = file.tools?.codebuddy ?? {};
|
|
203
|
+
// Autopilot(限流自动恢复)配置
|
|
204
|
+
const autopilotFile = tc.autopilot ?? {};
|
|
205
|
+
const autopilot = {
|
|
206
|
+
enabled: process.env.OPEN_IM_AUTOPILOT !== 'false'
|
|
207
|
+
&& (autopilotFile.enabled ?? AUTOPILOT_ENABLED_DEFAULT),
|
|
208
|
+
maxRetries: (() => {
|
|
209
|
+
const envVal = process.env.OPEN_IM_AUTOPILOT_MAX_RETRIES;
|
|
210
|
+
if (envVal !== undefined && envVal !== '') {
|
|
211
|
+
const n = Number.parseInt(envVal, 10);
|
|
212
|
+
if (Number.isFinite(n) && n >= 0)
|
|
213
|
+
return n;
|
|
214
|
+
}
|
|
215
|
+
return autopilotFile.maxRetries ?? AUTOPILOT_MAX_RETRIES;
|
|
216
|
+
})(),
|
|
217
|
+
defaultIntervalHours: autopilotFile.defaultIntervalHours ?? AUTOPILOT_DEFAULT_INTERVAL_HOURS,
|
|
218
|
+
shortRetrySeconds: autopilotFile.shortRetrySeconds ?? AUTOPILOT_SHORT_RETRY_SECONDS,
|
|
219
|
+
autoResumePrompt: autopilotFile.autoResumePrompt ?? AUTOPILOT_DEFAULT_PROMPT,
|
|
220
|
+
};
|
|
203
221
|
const claudeProxy = process.env.CLAUDE_PROXY ?? tc.proxy;
|
|
204
222
|
const codexProxy = process.env.CODEX_PROXY ?? tcod.proxy;
|
|
205
223
|
// Windows 下若 CLI 路径仍是默认名,尝试在 npm 全局目录定位 .cmd。
|
|
@@ -226,6 +244,7 @@ export function loadConfig() {
|
|
|
226
244
|
const codebuddyCliPath = process.env.CODEBUDDY_CLI_PATH ?? tcb.cliPath ?? resolveWindowsCliPath(AI_TOOL_BY_ID.codebuddy.cliDefault ?? 'codebuddy');
|
|
227
245
|
const topencode = file.tools?.opencode ?? {};
|
|
228
246
|
const opencodeCliPath = process.env.OPENCODE_CLI_PATH ?? topencode.cliPath ?? resolveWindowsCliPath(AI_TOOL_BY_ID.opencode.cliDefault ?? 'opencode');
|
|
247
|
+
const opencodeModel = process.env.OPENCODE_MODEL ?? topencode.model;
|
|
229
248
|
const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? tc.workDir ?? process.cwd();
|
|
230
249
|
const skipPermissions = process.env.OPEN_IM_SKIP_PERMISSIONS === 'false'
|
|
231
250
|
? false
|
|
@@ -519,6 +538,7 @@ export function loadConfig() {
|
|
|
519
538
|
codexCliPath,
|
|
520
539
|
codebuddyCliPath,
|
|
521
540
|
opencodeCliPath,
|
|
541
|
+
opencodeModel,
|
|
522
542
|
claudeProxy,
|
|
523
543
|
codexProxy,
|
|
524
544
|
claudeWorkDir,
|
|
@@ -530,6 +550,7 @@ export function loadConfig() {
|
|
|
530
550
|
telemetry: {
|
|
531
551
|
enabled: telemetryEnabled,
|
|
532
552
|
},
|
|
553
|
+
autopilot,
|
|
533
554
|
platforms,
|
|
534
555
|
};
|
|
535
556
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -71,5 +71,15 @@ export declare const MAX_WORKBUDDY_MESSAGE_LENGTH = 2000;
|
|
|
71
71
|
export declare const MAX_CLAWBOT_MESSAGE_LENGTH = 2000;
|
|
72
72
|
/** ClawBot 长轮询间隔 */
|
|
73
73
|
export declare const CLAWBOT_POLL_INTERVAL_MS = 3000;
|
|
74
|
+
/** Autopilot 默认开启 */
|
|
75
|
+
export declare const AUTOPILOT_ENABLED_DEFAULT = true;
|
|
76
|
+
/** 最大连续自动重试次数 */
|
|
77
|
+
export declare const AUTOPILOT_MAX_RETRIES = 5;
|
|
78
|
+
/** 无法从错误消息提取重置时间时的默认等待小时数 */
|
|
79
|
+
export declare const AUTOPILOT_DEFAULT_INTERVAL_HOURS = 5;
|
|
80
|
+
/** 529/临时限流的短延迟秒数 */
|
|
81
|
+
export declare const AUTOPILOT_SHORT_RETRY_SECONDS = 60;
|
|
82
|
+
/** 自动恢复时发送的提示词 */
|
|
83
|
+
export declare const AUTOPILOT_DEFAULT_PROMPT = "\u7EE7\u7EED";
|
|
74
84
|
export declare const DEFAULT_OPEN_IM_COAUTHOR_ADDR = "529778790@qq.com";
|
|
75
85
|
export declare const TERMINAL_ONLY_COMMANDS: Set<string>;
|
package/dist/constants.js
CHANGED
|
@@ -90,6 +90,17 @@ export const MAX_CLAWBOT_MESSAGE_LENGTH = 2000;
|
|
|
90
90
|
// ─── 轮询间隔 ───
|
|
91
91
|
/** ClawBot 长轮询间隔 */
|
|
92
92
|
export const CLAWBOT_POLL_INTERVAL_MS = 3000;
|
|
93
|
+
// ─── Autopilot(限流自动恢复) ───
|
|
94
|
+
/** Autopilot 默认开启 */
|
|
95
|
+
export const AUTOPILOT_ENABLED_DEFAULT = true;
|
|
96
|
+
/** 最大连续自动重试次数 */
|
|
97
|
+
export const AUTOPILOT_MAX_RETRIES = 5;
|
|
98
|
+
/** 无法从错误消息提取重置时间时的默认等待小时数 */
|
|
99
|
+
export const AUTOPILOT_DEFAULT_INTERVAL_HOURS = 5;
|
|
100
|
+
/** 529/临时限流的短延迟秒数 */
|
|
101
|
+
export const AUTOPILOT_SHORT_RETRY_SECONDS = 60;
|
|
102
|
+
/** 自动恢复时发送的提示词 */
|
|
103
|
+
export const AUTOPILOT_DEFAULT_PROMPT = "继续";
|
|
93
104
|
// ─── Git 共同作者 ───
|
|
94
105
|
export const DEFAULT_OPEN_IM_COAUTHOR_ADDR = "529778790@qq.com";
|
|
95
106
|
// ─── 终端独占命令 ───
|
|
@@ -3,30 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* OpenCode outputs plain text to stdout. Session continuation uses `-s <id>`.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
onText: (accumulated: string) => void;
|
|
8
|
-
onThinking?: (accumulated: string) => void;
|
|
9
|
-
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
10
|
-
onComplete: (result: {
|
|
11
|
-
success: boolean;
|
|
12
|
-
result: string;
|
|
13
|
-
accumulated: string;
|
|
14
|
-
cost: number;
|
|
15
|
-
durationMs: number;
|
|
16
|
-
model?: string;
|
|
17
|
-
numTurns: number;
|
|
18
|
-
toolStats: Record<string, number>;
|
|
19
|
-
}) => void;
|
|
20
|
-
onError: (error: string) => void;
|
|
21
|
-
onSessionId?: (sessionId: string) => void;
|
|
22
|
-
onSessionInvalid?: () => void;
|
|
23
|
-
}
|
|
6
|
+
import type { RunCallbacks, RunHandle } from '../adapters/tool-adapter.interface.js';
|
|
24
7
|
export interface OpenCodeRunOptions {
|
|
25
8
|
skipPermissions?: boolean;
|
|
26
9
|
model?: string;
|
|
27
10
|
}
|
|
28
|
-
export interface OpenCodeRunHandle {
|
|
29
|
-
abort: () => void;
|
|
30
|
-
}
|
|
31
11
|
export declare function buildOpenCodeArgs(prompt: string, sessionId: string | undefined, workDir: string, options?: OpenCodeRunOptions): string[];
|
|
32
|
-
export declare function runOpenCode(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks:
|
|
12
|
+
export declare function runOpenCode(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: OpenCodeRunOptions): RunHandle;
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* OpenCode outputs plain text to stdout. Session continuation uses `-s <id>`.
|
|
5
5
|
*/
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
6
|
import { createInterface } from 'node:readline';
|
|
8
7
|
import { createLogger } from '../logger.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { killProcessTree } from '../shared/process-kill.js';
|
|
9
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
10
|
+
import { createStderrBuffer, appendStderr, reconstructStderr, createFinalizeGate, isFinalizeReady, clearWallClockTimeout, spawnCliProcess, } from '../shared/cli-runner-base.js';
|
|
11
11
|
const log = createLogger('OpenCodeCli');
|
|
12
12
|
export function buildOpenCodeArgs(prompt, sessionId, workDir, options) {
|
|
13
13
|
const args = ['run'];
|
|
@@ -31,16 +31,8 @@ export function buildOpenCodeArgs(prompt, sessionId, workDir, options) {
|
|
|
31
31
|
}
|
|
32
32
|
export function runOpenCode(cliPath, prompt, sessionId, workDir, callbacks, options) {
|
|
33
33
|
const args = buildOpenCodeArgs(prompt, sessionId, workDir, options);
|
|
34
|
-
const env = processEnvForNonClaudeCliChild();
|
|
35
34
|
log.info(`Spawning OpenCode: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}`);
|
|
36
|
-
const child =
|
|
37
|
-
cwd: workDir,
|
|
38
|
-
detached: process.platform !== 'win32',
|
|
39
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
-
env,
|
|
41
|
-
windowsHide: process.platform === 'win32',
|
|
42
|
-
});
|
|
43
|
-
trackChild(child);
|
|
35
|
+
const child = spawnCliProcess(cliPath, args, workDir);
|
|
44
36
|
// Close stdin — prompt is passed as argument
|
|
45
37
|
child.stdin?.end();
|
|
46
38
|
let accumulated = '';
|
|
@@ -48,27 +40,10 @@ export function runOpenCode(cliPath, prompt, sessionId, workDir, callbacks, opti
|
|
|
48
40
|
const toolStats = {};
|
|
49
41
|
const startTime = Date.now();
|
|
50
42
|
const rl = createInterface({ input: child.stdout });
|
|
51
|
-
const
|
|
52
|
-
const MAX_STDERR_TAIL = 6 * 1024;
|
|
53
|
-
let stderrHead = '';
|
|
54
|
-
let stderrTail = '';
|
|
55
|
-
let stderrTotal = 0;
|
|
56
|
-
let stderrHeadFull = false;
|
|
43
|
+
const stderrBuf = createStderrBuffer();
|
|
57
44
|
child.stderr?.on('data', (chunk) => {
|
|
58
45
|
const text = chunk.toString();
|
|
59
|
-
|
|
60
|
-
if (!stderrHeadFull) {
|
|
61
|
-
const room = MAX_STDERR_HEAD - stderrHead.length;
|
|
62
|
-
if (room > 0) {
|
|
63
|
-
stderrHead += text.slice(0, room);
|
|
64
|
-
if (stderrHead.length >= MAX_STDERR_HEAD)
|
|
65
|
-
stderrHeadFull = true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
stderrTail += text;
|
|
69
|
-
if (stderrTail.length > MAX_STDERR_TAIL) {
|
|
70
|
-
stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
|
|
71
|
-
}
|
|
46
|
+
appendStderr(stderrBuf, text);
|
|
72
47
|
log.debug(`[stderr] ${text.trimEnd()}`);
|
|
73
48
|
});
|
|
74
49
|
rl.on('line', (line) => {
|
|
@@ -94,67 +69,71 @@ export function runOpenCode(cliPath, prompt, sessionId, workDir, callbacks, opti
|
|
|
94
69
|
}
|
|
95
70
|
});
|
|
96
71
|
let exitCode = null;
|
|
97
|
-
|
|
98
|
-
let
|
|
72
|
+
const gate = createFinalizeGate();
|
|
73
|
+
let cliTimeoutHandle = null;
|
|
99
74
|
const finalize = () => {
|
|
100
|
-
|
|
75
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
76
|
+
cliTimeoutHandle = null;
|
|
77
|
+
if (!isFinalizeReady(gate))
|
|
101
78
|
return;
|
|
102
79
|
if (completed)
|
|
103
80
|
return;
|
|
104
81
|
if (exitCode !== null && exitCode !== 0) {
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
if (!stderrHeadFull) {
|
|
108
|
-
errMsg = stderrHead;
|
|
109
|
-
}
|
|
110
|
-
else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
|
|
111
|
-
errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
errMsg =
|
|
115
|
-
stderrHead +
|
|
116
|
-
`\n\n... (omitted ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} bytes) ...\n\n` +
|
|
117
|
-
stderrTail;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (sessionId &&
|
|
121
|
-
(errMsg.includes('session not found') ||
|
|
122
|
-
errMsg.includes('Session not found') ||
|
|
123
|
-
errMsg.includes('no sessions found'))) {
|
|
82
|
+
const errMsg = reconstructStderr(stderrBuf);
|
|
83
|
+
if (sessionId && isSessionInvalidMessage(errMsg)) {
|
|
124
84
|
callbacks.onSessionInvalid?.();
|
|
125
85
|
}
|
|
126
86
|
callbacks.onError(errMsg || `OpenCode exited with code ${exitCode}`);
|
|
127
87
|
return;
|
|
128
88
|
}
|
|
129
89
|
// Exit code 0 but no onComplete fired yet — treat accumulated text as result
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
}
|
|
90
|
+
completed = true;
|
|
91
|
+
callbacks.onComplete({
|
|
92
|
+
success: true,
|
|
93
|
+
result: accumulated,
|
|
94
|
+
accumulated,
|
|
95
|
+
cost: 0,
|
|
96
|
+
durationMs: Date.now() - startTime,
|
|
97
|
+
numTurns: 1,
|
|
98
|
+
toolStats,
|
|
99
|
+
});
|
|
142
100
|
};
|
|
143
101
|
rl.on('close', () => {
|
|
144
|
-
rlClosed = true;
|
|
102
|
+
gate.rlClosed = true;
|
|
145
103
|
finalize();
|
|
146
104
|
});
|
|
147
105
|
child.on('close', (code) => {
|
|
148
106
|
exitCode = code;
|
|
149
|
-
childClosed = true;
|
|
107
|
+
gate.childClosed = true;
|
|
150
108
|
finalize();
|
|
151
109
|
});
|
|
152
110
|
child.on('error', (err) => {
|
|
153
111
|
log.error('OpenCode spawn error:', err);
|
|
154
|
-
|
|
112
|
+
if (!completed) {
|
|
113
|
+
completed = true;
|
|
114
|
+
callbacks.onError(`Failed to start OpenCode: ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
gate.childClosed = true;
|
|
117
|
+
finalize();
|
|
155
118
|
});
|
|
119
|
+
// 墙钟超时:防止 CLI 挂死永久占用用户队列槽
|
|
120
|
+
const cliTimeoutMs = Number(process.env.OPEN_IM_CLI_TIMEOUT_MS) || 30 * 60 * 1000;
|
|
121
|
+
cliTimeoutHandle = setTimeout(() => {
|
|
122
|
+
if (completed)
|
|
123
|
+
return;
|
|
124
|
+
completed = true;
|
|
125
|
+
rl.close();
|
|
126
|
+
killProcessTree(child);
|
|
127
|
+
callbacks.onError(`OpenCode CLI 运行超时(${Math.round(cliTimeoutMs / 1000)}s),已终止。请重试。`);
|
|
128
|
+
}, cliTimeoutMs);
|
|
129
|
+
cliTimeoutHandle.unref();
|
|
156
130
|
return {
|
|
157
131
|
abort: () => {
|
|
132
|
+
if (completed)
|
|
133
|
+
return;
|
|
134
|
+
completed = true;
|
|
135
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
136
|
+
rl.close();
|
|
158
137
|
killProcessTree(child);
|
|
159
138
|
},
|
|
160
139
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OpencodeClient } from '@opencode-ai/sdk/v2/client';
|
|
2
|
+
export declare function startOpencode(options?: {
|
|
3
|
+
hostname?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}): Promise<OpencodeClient>;
|
|
8
|
+
export declare function stopOpencode(): void;
|
|
9
|
+
export declare function getOpencodeClient(): OpencodeClient;
|
|
10
|
+
export declare function isOpencodeRunning(): boolean;
|
|
11
|
+
export declare function connectToOpencode(baseUrl?: string): Promise<OpencodeClient>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createOpencode, createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
2
|
+
import { createLogger } from '../logger.js';
|
|
3
|
+
const log = createLogger('OpenCodeSDK');
|
|
4
|
+
let instance = null;
|
|
5
|
+
export async function startOpencode(options) {
|
|
6
|
+
if (instance) {
|
|
7
|
+
log.info('OpenCode SDK already running');
|
|
8
|
+
return instance.client;
|
|
9
|
+
}
|
|
10
|
+
log.info('Starting OpenCode SDK server...');
|
|
11
|
+
const result = await createOpencode({
|
|
12
|
+
hostname: options?.hostname ?? '127.0.0.1',
|
|
13
|
+
port: options?.port ?? 0,
|
|
14
|
+
signal: options?.signal,
|
|
15
|
+
timeout: options?.timeout ?? 15000,
|
|
16
|
+
});
|
|
17
|
+
log.info(`OpenCode server started at ${result.server.url}`);
|
|
18
|
+
instance = result;
|
|
19
|
+
return instance.client;
|
|
20
|
+
}
|
|
21
|
+
export function stopOpencode() {
|
|
22
|
+
if (instance) {
|
|
23
|
+
log.info('Stopping OpenCode SDK server...');
|
|
24
|
+
instance.server.close();
|
|
25
|
+
instance = null;
|
|
26
|
+
log.info('OpenCode SDK server stopped');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function getOpencodeClient() {
|
|
30
|
+
if (!instance) {
|
|
31
|
+
throw new Error('OpenCode SDK not started');
|
|
32
|
+
}
|
|
33
|
+
return instance.client;
|
|
34
|
+
}
|
|
35
|
+
export function isOpencodeRunning() {
|
|
36
|
+
return instance !== null;
|
|
37
|
+
}
|
|
38
|
+
export async function connectToOpencode(baseUrl) {
|
|
39
|
+
if (instance)
|
|
40
|
+
return instance.client;
|
|
41
|
+
const url = baseUrl ?? 'http://127.0.0.1:4096';
|
|
42
|
+
log.info(`Connecting to existing OpenCode server at ${url}`);
|
|
43
|
+
const client = createOpencodeClient({ baseUrl: url });
|
|
44
|
+
instance = { client, server: { url, close() { } } };
|
|
45
|
+
return client;
|
|
46
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RunCallbacks, RunHandle } from '../adapters/tool-adapter.interface.js';
|
|
2
|
+
export interface OpenCodeSdkRunOptions {
|
|
3
|
+
skipPermissions?: boolean;
|
|
4
|
+
model?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function runOpenCodeSdk(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: OpenCodeSdkRunOptions): Promise<RunHandle>;
|
|
7
|
+
export declare function clearSessionCache(workDir?: string): void;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { startOpencode } from './sdk-manager.js';
|
|
2
|
+
import { createLogger } from '../logger.js';
|
|
3
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
4
|
+
const log = createLogger('OpenCodeSDKRunner');
|
|
5
|
+
const sessionCache = new Map();
|
|
6
|
+
function parseModel(modelStr) {
|
|
7
|
+
const slashIdx = modelStr.indexOf('/');
|
|
8
|
+
if (slashIdx > 0) {
|
|
9
|
+
return { providerID: modelStr.slice(0, slashIdx), modelID: modelStr.slice(slashIdx + 1) };
|
|
10
|
+
}
|
|
11
|
+
return { providerID: 'anthropic', modelID: modelStr };
|
|
12
|
+
}
|
|
13
|
+
export async function runOpenCodeSdk(prompt, sessionId, workDir, callbacks, options) {
|
|
14
|
+
const client = await startOpencode();
|
|
15
|
+
const abortController = new AbortController();
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
let currentSessionId = sessionId;
|
|
18
|
+
if (!currentSessionId) {
|
|
19
|
+
currentSessionId = sessionCache.get(workDir);
|
|
20
|
+
}
|
|
21
|
+
if (!currentSessionId) {
|
|
22
|
+
try {
|
|
23
|
+
const projectName = workDir.split('/').filter(Boolean).pop() || 'project';
|
|
24
|
+
const result = await client.session.create({
|
|
25
|
+
directory: workDir,
|
|
26
|
+
title: `open-im: ${projectName}`,
|
|
27
|
+
});
|
|
28
|
+
currentSessionId = result.data?.id;
|
|
29
|
+
if (!currentSessionId) {
|
|
30
|
+
throw new Error('Session creation returned no ID');
|
|
31
|
+
}
|
|
32
|
+
sessionCache.set(workDir, currentSessionId);
|
|
33
|
+
callbacks.onSessionId?.(currentSessionId);
|
|
34
|
+
log.info(`Created session ${currentSessionId} for ${workDir}`);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const msg = (err instanceof Error ? err.message : String(err));
|
|
38
|
+
log.error('Failed to create session:', msg);
|
|
39
|
+
callbacks.onError(msg);
|
|
40
|
+
return { abort: () => { } };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
log.info(`Using ${sessionId ? 'provided' : 'cached'} session ${currentSessionId}`);
|
|
45
|
+
}
|
|
46
|
+
let accumulatedText = '';
|
|
47
|
+
let accumulatedThinking = '';
|
|
48
|
+
const toolStats = {};
|
|
49
|
+
let runSettled = false;
|
|
50
|
+
let sseConnected = false;
|
|
51
|
+
// 用于等待 SSE 连接就绪后再发 prompt,避免竞态
|
|
52
|
+
const sseReadyResolve = () => { sseConnected = true; };
|
|
53
|
+
const subscribeEvents = async () => {
|
|
54
|
+
try {
|
|
55
|
+
const sse = await client.global.event({ signal: abortController.signal });
|
|
56
|
+
// SSE 连接已建立,通知主流程可以开始 prompt
|
|
57
|
+
sseReadyResolve();
|
|
58
|
+
log.debug('SSE stream connected');
|
|
59
|
+
for await (const raw of sse.stream) {
|
|
60
|
+
const ev = raw;
|
|
61
|
+
const payload = ev?.payload;
|
|
62
|
+
if (!payload?.properties)
|
|
63
|
+
continue;
|
|
64
|
+
if (payload.properties.sessionID !== currentSessionId)
|
|
65
|
+
continue;
|
|
66
|
+
switch (payload.type) {
|
|
67
|
+
case 'session.next.text.delta': {
|
|
68
|
+
const delta = payload.properties.delta;
|
|
69
|
+
if (delta) {
|
|
70
|
+
accumulatedText += delta;
|
|
71
|
+
callbacks.onText(accumulatedText);
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'session.next.text.ended': {
|
|
76
|
+
// text.ended 携带完整文本,作为 SSE 流的兜底
|
|
77
|
+
const fullText = payload.properties.text;
|
|
78
|
+
if (fullText && !accumulatedText) {
|
|
79
|
+
accumulatedText = fullText;
|
|
80
|
+
callbacks.onText(accumulatedText);
|
|
81
|
+
log.debug(`SSE text.ended fallback: got ${fullText.length} chars`);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case 'session.next.reasoning.delta': {
|
|
86
|
+
const delta = payload.properties.delta;
|
|
87
|
+
if (delta) {
|
|
88
|
+
accumulatedThinking += delta;
|
|
89
|
+
callbacks.onThinking?.(accumulatedThinking);
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'session.next.reasoning.ended': {
|
|
94
|
+
// reasoning.ended 携带完整推理文本
|
|
95
|
+
const fullReasoning = payload.properties.text;
|
|
96
|
+
if (fullReasoning && !accumulatedThinking) {
|
|
97
|
+
accumulatedThinking = fullReasoning;
|
|
98
|
+
callbacks.onThinking?.(accumulatedThinking);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case 'session.next.tool.called': {
|
|
103
|
+
const tool = payload.properties.tool;
|
|
104
|
+
if (tool) {
|
|
105
|
+
toolStats[tool] = (toolStats[tool] || 0) + 1;
|
|
106
|
+
callbacks.onToolUse?.(tool, payload.properties.input);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
log.debug(`SSE unhandled event: ${payload.type}`);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (!abortController.signal.aborted) {
|
|
118
|
+
log.debug('SSE stream ended:', err?.message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const background = subscribeEvents();
|
|
123
|
+
// 等待 SSE 连接就绪(最多 3 秒),避免 prompt 在 SSE 未连接时就开始
|
|
124
|
+
// 如果 3 秒内 SSE 未连接,仍然继续执行(SSE 可能后续连接并补收事件)
|
|
125
|
+
const sseReady = new Promise((resolve) => {
|
|
126
|
+
const check = setInterval(() => {
|
|
127
|
+
if (sseConnected) {
|
|
128
|
+
clearInterval(check);
|
|
129
|
+
resolve();
|
|
130
|
+
}
|
|
131
|
+
}, 100);
|
|
132
|
+
// 3 秒超时:SSE 可能需要重试连接,不等太久
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
clearInterval(check);
|
|
135
|
+
if (!sseConnected)
|
|
136
|
+
log.warn('SSE not connected within 3s, proceeding with prompt anyway');
|
|
137
|
+
resolve();
|
|
138
|
+
}, 3000);
|
|
139
|
+
});
|
|
140
|
+
await sseReady;
|
|
141
|
+
try {
|
|
142
|
+
const result = await client.session.prompt({
|
|
143
|
+
sessionID: currentSessionId,
|
|
144
|
+
directory: workDir,
|
|
145
|
+
parts: [{ type: 'text', text: prompt }],
|
|
146
|
+
...(options?.model ? { model: parseModel(options.model) } : {}),
|
|
147
|
+
});
|
|
148
|
+
// 调试:dump SDK 返回值结构
|
|
149
|
+
log.info(`SDK prompt raw result: hasData=${!!result.data}, hasError=${!!result.error}, keys=${Object.keys(result).join(',')}`);
|
|
150
|
+
if (result.error) {
|
|
151
|
+
const errStr = result.error instanceof Error ? result.error.message : JSON.stringify(result.error).substring(0, 500);
|
|
152
|
+
log.info(`SDK prompt error detail: ${errStr}`);
|
|
153
|
+
}
|
|
154
|
+
if (result.data) {
|
|
155
|
+
const d = result.data;
|
|
156
|
+
log.info(`SDK prompt data keys: ${Object.keys(d).join(',')}, parts count: ${Array.isArray(d.parts) ? d.parts.length : 'N/A'}, info keys: ${d.info && typeof d.info === 'object' ? Object.keys(d.info).join(',') : 'N/A'}`);
|
|
157
|
+
}
|
|
158
|
+
abortController.abort();
|
|
159
|
+
await background.catch(() => { });
|
|
160
|
+
runSettled = true;
|
|
161
|
+
// SDK 返回 { data, error } 二元组;error 存在时 data 为 undefined
|
|
162
|
+
const sdkError = result.error;
|
|
163
|
+
if (sdkError) {
|
|
164
|
+
const errMsg = sdkError instanceof Error ? sdkError.message : JSON.stringify(sdkError);
|
|
165
|
+
log.error(`SDK prompt returned error: ${errMsg}`);
|
|
166
|
+
callbacks.onError(errMsg);
|
|
167
|
+
return { abort: () => { } };
|
|
168
|
+
}
|
|
169
|
+
const data = result.data;
|
|
170
|
+
if (!data) {
|
|
171
|
+
log.error(`SDK prompt returned no data and no error — result keys: ${Object.keys(result).join(',')}`);
|
|
172
|
+
callbacks.onError('SDK 返回空数据');
|
|
173
|
+
return { abort: () => { } };
|
|
174
|
+
}
|
|
175
|
+
const parts = data?.parts ?? [];
|
|
176
|
+
const finalText = parts
|
|
177
|
+
.filter((p) => p.type === 'text' && typeof p.text === 'string')
|
|
178
|
+
.map(p => p.text)
|
|
179
|
+
.join('\n');
|
|
180
|
+
const cost = data?.info?.cost ?? 0;
|
|
181
|
+
log.info(`SDK prompt completed: accumulatedText=${accumulatedText.length}, finalText=${finalText.length}, parts=${parts.length}, cost=${cost}`);
|
|
182
|
+
if (!accumulatedText && !finalText) {
|
|
183
|
+
log.warn(`SDK prompt returned empty output — data keys: ${data ? Object.keys(data).join(',') : 'null'}, parts types: ${parts.map(p => p.type).join(',')}`);
|
|
184
|
+
}
|
|
185
|
+
callbacks.onComplete({
|
|
186
|
+
success: true,
|
|
187
|
+
result: finalText,
|
|
188
|
+
accumulated: accumulatedText || finalText,
|
|
189
|
+
cost,
|
|
190
|
+
durationMs: Date.now() - startTime,
|
|
191
|
+
numTurns: 1,
|
|
192
|
+
toolStats,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
abortController.abort();
|
|
197
|
+
await background.catch(() => { });
|
|
198
|
+
if (runSettled)
|
|
199
|
+
return { abort: () => { } };
|
|
200
|
+
const msg = (err instanceof Error ? err.message : String(err));
|
|
201
|
+
log.error('OpenCode SDK error:', msg);
|
|
202
|
+
if (isSessionInvalidMessage(msg)) {
|
|
203
|
+
sessionCache.delete(workDir);
|
|
204
|
+
callbacks.onSessionInvalid?.();
|
|
205
|
+
}
|
|
206
|
+
runSettled = true;
|
|
207
|
+
callbacks.onError(msg);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
abort: () => {
|
|
211
|
+
if (!abortController.signal.aborted) {
|
|
212
|
+
abortController.abort();
|
|
213
|
+
client.session.abort({ sessionID: currentSessionId }).catch(() => { });
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export function clearSessionCache(workDir) {
|
|
219
|
+
if (workDir) {
|
|
220
|
+
sessionCache.delete(workDir);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
sessionCache.clear();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -133,7 +133,24 @@ export function createPlatformAIRequestHandler(deps) {
|
|
|
133
133
|
};
|
|
134
134
|
// 7. Run AI task
|
|
135
135
|
try {
|
|
136
|
-
await runAITask({
|
|
136
|
+
await runAITask({
|
|
137
|
+
config,
|
|
138
|
+
sessionManager,
|
|
139
|
+
autopilot: {
|
|
140
|
+
onAutoPilotContinue: (continuePrompt) => {
|
|
141
|
+
log.info(`[${platform}] Autopilot: re-enqueuing "${continuePrompt}" for user ${userId}`);
|
|
142
|
+
handleAIRequest({
|
|
143
|
+
userId,
|
|
144
|
+
chatId,
|
|
145
|
+
prompt: continuePrompt,
|
|
146
|
+
workDir,
|
|
147
|
+
convId,
|
|
148
|
+
}).catch((err) => {
|
|
149
|
+
log.error(`[${platform}] Autopilot re-enqueue failed:`, err);
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}, {
|
|
137
154
|
userId,
|
|
138
155
|
chatId,
|
|
139
156
|
workDir,
|