@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.
Files changed (60) hide show
  1. package/dist/adapters/codebuddy-adapter.js +3 -17
  2. package/dist/adapters/codex-adapter.js +3 -16
  3. package/dist/adapters/opencode-adapter.d.ts +0 -5
  4. package/dist/adapters/opencode-adapter.js +10 -25
  5. package/dist/adapters/registry.js +10 -1
  6. package/dist/clawbot/client.js +72 -78
  7. package/dist/clawbot/qr-login.d.ts +1 -0
  8. package/dist/clawbot/qr-login.js +7 -0
  9. package/dist/codebuddy/cli-runner.d.ts +2 -22
  10. package/dist/codebuddy/cli-runner.js +9 -27
  11. package/dist/codex/cli-runner.d.ts +2 -22
  12. package/dist/codex/cli-runner.js +24 -75
  13. package/dist/commands/handler.d.ts +1 -0
  14. package/dist/commands/handler.js +34 -0
  15. package/dist/config/types.d.ts +23 -0
  16. package/dist/config-web-auth.d.ts +13 -0
  17. package/dist/config-web-auth.js +114 -0
  18. package/dist/config-web-browser.d.ts +4 -0
  19. package/dist/config-web-browser.js +48 -0
  20. package/dist/config-web-cors.d.ts +5 -0
  21. package/dist/config-web-cors.js +48 -0
  22. package/dist/config-web-health.d.ts +7 -0
  23. package/dist/config-web-health.js +65 -0
  24. package/dist/config-web-http.d.ts +3 -0
  25. package/dist/config-web-http.js +31 -0
  26. package/dist/config-web-page-i18n.d.ts +36 -2
  27. package/dist/config-web-page-i18n.js +36 -2
  28. package/dist/config-web-payload.d.ts +84 -0
  29. package/dist/config-web-payload.js +260 -0
  30. package/dist/config-web-probes.d.ts +2 -0
  31. package/dist/config-web-probes.js +271 -0
  32. package/dist/config-web.d.ts +3 -11
  33. package/dist/config-web.js +45 -815
  34. package/dist/config.js +22 -1
  35. package/dist/constants.d.ts +10 -0
  36. package/dist/constants.js +11 -0
  37. package/dist/opencode/cli-runner.d.ts +2 -22
  38. package/dist/opencode/cli-runner.js +47 -68
  39. package/dist/opencode/sdk-manager.d.ts +11 -0
  40. package/dist/opencode/sdk-manager.js +46 -0
  41. package/dist/opencode/sdk-runner.d.ts +7 -0
  42. package/dist/opencode/sdk-runner.js +225 -0
  43. package/dist/platform/handle-ai-request.js +18 -1
  44. package/dist/qq/client.js +45 -53
  45. package/dist/shared/ai-task.d.ts +15 -0
  46. package/dist/shared/ai-task.js +174 -2
  47. package/dist/shared/cli-runner-base.d.ts +39 -0
  48. package/dist/shared/cli-runner-base.js +108 -0
  49. package/dist/shared/connection-manager.d.ts +111 -0
  50. package/dist/shared/connection-manager.js +157 -0
  51. package/dist/shared/session-invalid-detector.d.ts +5 -0
  52. package/dist/shared/session-invalid-detector.js +20 -0
  53. package/dist/wework/client.js +79 -127
  54. package/dist/workbuddy/client.js +40 -74
  55. package/package.json +4 -1
  56. package/web/dist/assets/index-6e51Wtaa.css +1 -0
  57. package/web/dist/assets/index-Da9qKWA2.js +57 -0
  58. package/web/dist/index.html +2 -2
  59. package/web/dist/assets/index-XKVTb-0p.css +0 -1
  60. 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
  }
@@ -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
- export interface OpenCodeRunCallbacks {
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: OpenCodeRunCallbacks, options?: OpenCodeRunOptions): OpenCodeRunHandle;
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 { processEnvForNonClaudeCliChild } from '../config/file-io.js';
10
- import { killProcessTree, trackChild } from '../shared/process-kill.js';
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 = spawn(cliPath, args, {
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 MAX_STDERR_HEAD = 4 * 1024;
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
- stderrTotal += text.length;
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
- let rlClosed = false;
98
- let childClosed = false;
72
+ const gate = createFinalizeGate();
73
+ let cliTimeoutHandle = null;
99
74
  const finalize = () => {
100
- if (!rlClosed || !childClosed)
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
- let errMsg = '';
106
- if (stderrTotal > 0) {
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
- if (!completed) {
131
- completed = true;
132
- callbacks.onComplete({
133
- success: true,
134
- result: accumulated,
135
- accumulated,
136
- cost: 0,
137
- durationMs: Date.now() - startTime,
138
- numTurns: 1,
139
- toolStats,
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
- callbacks.onError(`Failed to start OpenCode: ${err.message}`);
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({ config, sessionManager }, {
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,