@wu529778790/open-im 1.11.8-beta.3 → 1.11.8-beta.30

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 (63) hide show
  1. package/dist/adapters/claude-sdk-adapter.d.ts +1 -0
  2. package/dist/adapters/claude-sdk-adapter.js +1 -0
  3. package/dist/adapters/codebuddy-adapter.d.ts +1 -0
  4. package/dist/adapters/codebuddy-adapter.js +4 -17
  5. package/dist/adapters/codex-adapter.d.ts +1 -0
  6. package/dist/adapters/codex-adapter.js +4 -16
  7. package/dist/adapters/opencode-adapter.d.ts +1 -0
  8. package/dist/adapters/opencode-adapter.js +4 -16
  9. package/dist/adapters/tool-adapter.interface.d.ts +2 -0
  10. package/dist/clawbot/client.js +73 -79
  11. package/dist/clawbot/qr-login.d.ts +1 -0
  12. package/dist/clawbot/qr-login.js +7 -0
  13. package/dist/codebuddy/cli-runner.d.ts +2 -22
  14. package/dist/codebuddy/cli-runner.js +9 -27
  15. package/dist/codex/cli-runner.d.ts +2 -22
  16. package/dist/codex/cli-runner.js +96 -99
  17. package/dist/commands/handler.d.ts +1 -0
  18. package/dist/commands/handler.js +34 -0
  19. package/dist/config/types.d.ts +23 -0
  20. package/dist/config-web-auth.d.ts +13 -0
  21. package/dist/config-web-auth.js +114 -0
  22. package/dist/config-web-browser.d.ts +4 -0
  23. package/dist/config-web-browser.js +48 -0
  24. package/dist/config-web-cors.d.ts +5 -0
  25. package/dist/config-web-cors.js +48 -0
  26. package/dist/config-web-health.d.ts +7 -0
  27. package/dist/config-web-health.js +65 -0
  28. package/dist/config-web-http.d.ts +3 -0
  29. package/dist/config-web-http.js +31 -0
  30. package/dist/config-web-page-i18n.d.ts +34 -0
  31. package/dist/config-web-page-i18n.js +34 -0
  32. package/dist/config-web-payload.d.ts +84 -0
  33. package/dist/config-web-payload.js +260 -0
  34. package/dist/config-web-probes.d.ts +2 -0
  35. package/dist/config-web-probes.js +271 -0
  36. package/dist/config-web.d.ts +3 -11
  37. package/dist/config-web.js +45 -815
  38. package/dist/config.js +22 -1
  39. package/dist/constants.d.ts +10 -0
  40. package/dist/constants.js +11 -0
  41. package/dist/opencode/cli-runner.d.ts +2 -22
  42. package/dist/opencode/cli-runner.js +47 -68
  43. package/dist/opencode/sdk-runner.d.ts +2 -22
  44. package/dist/opencode/sdk-runner.js +75 -1
  45. package/dist/platform/handle-ai-request.js +18 -1
  46. package/dist/qq/client.js +45 -53
  47. package/dist/shared/ai-task.d.ts +15 -0
  48. package/dist/shared/ai-task.js +191 -13
  49. package/dist/shared/cli-runner-base.d.ts +39 -0
  50. package/dist/shared/cli-runner-base.js +108 -0
  51. package/dist/shared/connection-manager.d.ts +111 -0
  52. package/dist/shared/connection-manager.js +157 -0
  53. package/dist/shared/session-invalid-detector.d.ts +5 -0
  54. package/dist/shared/session-invalid-detector.js +20 -0
  55. package/dist/telegram/event-handler.js +6 -16
  56. package/dist/wework/client.js +79 -127
  57. package/dist/workbuddy/client.js +40 -74
  58. package/package.json +3 -1
  59. package/web/dist/assets/index-CbAuy3si.js +57 -0
  60. package/web/dist/assets/index-u3TxtMZ1.css +1 -0
  61. package/web/dist/index.html +2 -2
  62. package/web/dist/assets/index-BuoFTJc-.js +0 -57
  63. package/web/dist/assets/index-XKVTb-0p.css +0 -1
@@ -21,6 +21,7 @@ interface ClaudeSessionMeta {
21
21
  export declare function findLatestClaudeSession(workDir: string): Promise<ClaudeSessionMeta | undefined>;
22
22
  export declare class ClaudeSDKAdapter implements ToolAdapter {
23
23
  readonly toolId = "claude-sdk";
24
+ readonly interactionMode = "native";
24
25
  /**
25
26
  * 清理所有活跃的查询
26
27
  */
@@ -205,6 +205,7 @@ function enrichClaudeErrorMessage(message, stderr) {
205
205
  }
206
206
  export class ClaudeSDKAdapter {
207
207
  toolId = 'claude-sdk';
208
+ interactionMode = 'native';
208
209
  /**
209
210
  * 清理所有活跃的查询
210
211
  */
@@ -2,6 +2,7 @@ import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from './tool-ad
2
2
  export declare class CodeBuddyAdapter implements ToolAdapter {
3
3
  private cliPath;
4
4
  readonly toolId = "codebuddy";
5
+ readonly interactionMode = "open";
5
6
  constructor(cliPath: string);
6
7
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
7
8
  }
@@ -1,7 +1,9 @@
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';
6
+ interactionMode = 'open';
5
7
  constructor(cliPath) {
6
8
  this.cliPath = cliPath;
7
9
  }
@@ -10,25 +12,10 @@ export class CodeBuddyAdapter {
10
12
  onText: callbacks.onText,
11
13
  onThinking: callbacks.onThinking,
12
14
  onToolUse: callbacks.onToolUse,
13
- onComplete: (raw) => {
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
- },
15
+ onComplete: callbacks.onComplete,
26
16
  onError: (err) => {
27
17
  const msg = typeof err === 'string' ? err : String(err);
28
- const friendly = msg.includes('No conversation found') ||
29
- msg.includes('Session not found') ||
30
- msg.includes('Invalid session') ||
31
- msg.includes('Unable to resume')
18
+ const friendly = isSessionInvalidMessage(msg)
32
19
  ? 'CodeBuddy 会话已失效,旧 session 已清理。请直接重试当前请求。'
33
20
  : msg;
34
21
  callbacks.onError(friendly);
@@ -5,6 +5,7 @@ import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from "./tool-ad
5
5
  export declare class CodexAdapter implements ToolAdapter {
6
6
  private cliPath;
7
7
  readonly toolId = "codex";
8
+ readonly interactionMode = "open";
8
9
  constructor(cliPath: string);
9
10
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
10
11
  }
@@ -2,9 +2,11 @@
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";
9
+ interactionMode = "open";
8
10
  constructor(cliPath) {
9
11
  this.cliPath = cliPath;
10
12
  }
@@ -21,24 +23,10 @@ export class CodexAdapter {
21
23
  onText: callbacks.onText,
22
24
  onThinking: callbacks.onThinking,
23
25
  onToolUse: callbacks.onToolUse,
24
- onComplete: (raw) => {
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
- },
26
+ onComplete: callbacks.onComplete,
37
27
  onError: (err) => {
38
28
  const msg = typeof err === "string" ? err : String(err);
39
- const friendly = msg.includes("No session found") ||
40
- msg.includes("No conversation found") ||
41
- msg.includes("Unable to find session")
29
+ const friendly = isSessionInvalidMessage(msg)
42
30
  ? "Codex 会话已失效,旧 session 已清理。请直接重试当前请求。"
43
31
  : msg;
44
32
  callbacks.onError(friendly);
@@ -1,5 +1,6 @@
1
1
  import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from './tool-adapter.interface.js';
2
2
  export declare class OpenCodeAdapter implements ToolAdapter {
3
3
  readonly toolId = "opencode";
4
+ readonly interactionMode = "open";
4
5
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
5
6
  }
@@ -1,29 +1,17 @@
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';
5
+ interactionMode = 'open';
4
6
  run(prompt, sessionId, workDir, callbacks, options) {
5
7
  const handle = runOpenCodeSdk(prompt, sessionId, workDir, {
6
8
  onText: callbacks.onText,
7
9
  onThinking: callbacks.onThinking,
8
10
  onToolUse: callbacks.onToolUse,
9
- onComplete: (raw) => {
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
- },
11
+ onComplete: callbacks.onComplete,
22
12
  onError: (err) => {
23
13
  const msg = typeof err === 'string' ? err : String(err);
24
- const friendly = msg.includes('session not found') ||
25
- msg.includes('Session not found') ||
26
- msg.includes('no sessions found')
14
+ const friendly = isSessionInvalidMessage(msg)
27
15
  ? 'OpenCode 会话已失效,旧 session 已清理。请直接重试当前请求。'
28
16
  : msg;
29
17
  callbacks.onError(friendly);
@@ -40,7 +40,9 @@ export interface RunOptions {
40
40
  export interface RunHandle {
41
41
  abort: () => void;
42
42
  }
43
+ export type InteractionMode = 'native' | 'open';
43
44
  export interface ToolAdapter {
44
45
  readonly toolId: string;
46
+ readonly interactionMode: InteractionMode;
45
47
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
46
48
  }
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { randomBytes } from 'node:crypto';
11
11
  import { createLogger } from '../logger.js';
12
- import { jitteredDelay, isFatalReconnectError, SLOW_PROBE_MS } from '../shared/reconnect.js';
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 channelState;
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
- stateChangeHandler = onStateChange ?? null;
59
+ stateManager.setOnChange(onStateChange);
56
60
  stopped = false;
57
- reconnectAttempt = 0;
58
- fatal = false;
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
- updateState('connected');
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
- lastResponseAt = Date.now();
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
- fatal = true;
96
+ reconnectManager.setFatal(true);
93
97
  getUpdatesBuf = ''; // session expired, cursor is stale
94
98
  clearClawbotContextToken(); // session expired, token is stale
95
- updateState('error');
96
- scheduleReconnect();
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 || reconnectAttempt > 0) {
108
+ if (reconnectManager.fatal || reconnectManager.attemptCount > 0) {
105
109
  log.info('ClawBot connection recovered');
106
- fatal = false;
107
- reconnectAttempt = 0;
110
+ reconnectManager.reset();
108
111
  }
109
112
  // Update cursor for next poll
110
113
  if (res.updatesBuf) {
@@ -122,6 +125,17 @@ function startPolling() {
122
125
  const extracted = extractTextContent(msg);
123
126
  if (!extracted)
124
127
  continue;
128
+ // Skip echoed bot messages (iLink API echoes bot's own messages as USER type)
129
+ // 1) Messages sent by this bot have client_id starting with "open-im:"
130
+ if (msg.client_id?.startsWith('open-im:')) {
131
+ log.debug(`Skipping echoed bot message: client_id=${msg.client_id}`);
132
+ continue;
133
+ }
134
+ // 2) Bot lifecycle notifications and tool call notifications echoed back
135
+ if (isBotNotificationEcho(extracted)) {
136
+ log.debug(`Skipping bot notification echo: content="${extracted.substring(0, 80)}"`);
137
+ continue;
138
+ }
125
139
  const chatId = msg.from_user_id ?? '';
126
140
  const msgId = String(msg.message_id ?? msg.seq ?? '');
127
141
  if (!chatId) {
@@ -178,69 +192,35 @@ function startPolling() {
178
192
  if (err instanceof Error && err.name === 'AbortError')
179
193
  break;
180
194
  log.error('ClawBot polling error:', err);
181
- updateState('error');
182
- scheduleReconnect();
195
+ stateManager.set('error');
196
+ reconnectManager.schedule();
183
197
  return;
184
198
  }
185
199
  }
186
200
  })();
187
201
  }
188
- function scheduleReconnect() {
189
- if (stopped)
190
- return;
191
- if (reconnectTimer) {
192
- clearTimeout(reconnectTimer);
193
- reconnectTimer = null;
194
- }
195
- const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
196
- const delay = fatal ? jitteredDelay(SLOW_PROBE_MS) : jitteredDelay(baseDelay);
197
- reconnectAttempt++;
198
- if (fatal) {
199
- log.warn(`ClawBot fatal error, slow-probe in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempt})...`);
200
- }
201
- else {
202
- log.info(`ClawBot reconnecting in ${delay}ms (attempt ${reconnectAttempt})...`);
203
- }
204
- reconnectTimer = setTimeout(() => {
205
- reconnectTimer = null;
206
- if (stopped)
207
- return;
208
- updateState('connected');
209
- startPolling();
210
- }, delay);
211
- }
212
- function updateState(state) {
213
- channelState = state;
214
- stateChangeHandler?.(state);
215
- log.debug(`ClawBot state: ${state}`);
216
- }
217
202
  /**
218
203
  * Watchdog: periodically check if the poll loop is alive.
219
204
  * After Mac sleep or network drop, the fetch may hang without throwing.
220
205
  * If no successful response for WATCHDOG_STALE_MS, force a reconnect.
221
206
  */
222
207
  function startWatchdog() {
223
- stopWatchdog();
224
- lastResponseAt = Date.now();
225
- watchdogTimer = setInterval(() => {
226
- if (stopped)
227
- return;
228
- const elapsed = Date.now() - lastResponseAt;
229
- if (elapsed > WATCHDOG_STALE_MS) {
230
- log.warn(`ClawBot watchdog: no response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
231
- if (pollController) {
232
- pollController.abort();
233
- pollController = null;
234
- }
235
- updateState('error');
236
- scheduleReconnect();
237
- }
238
- }, WATCHDOG_INTERVAL_MS);
208
+ heartbeatMonitor.recordResponse();
209
+ heartbeatMonitor.start(WATCHDOG_INTERVAL_MS, watchdogTick);
239
210
  }
240
- function stopWatchdog() {
241
- if (watchdogTimer) {
242
- clearInterval(watchdogTimer);
243
- watchdogTimer = null;
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();
244
224
  }
245
225
  }
246
226
  /**
@@ -313,6 +293,23 @@ async function extractImages(msg) {
313
293
  }
314
294
  return paths;
315
295
  }
296
+ /**
297
+ * Check if extracted text is a bot notification being echoed back by the iLink API.
298
+ * Bot notifications have distinctive emoji prefixes that are unlikely in real user messages.
299
+ */
300
+ function isBotNotificationEcho(text) {
301
+ const firstLine = text.split('\n')[0]?.trim() ?? '';
302
+ // Lifecycle/AI info notifications
303
+ if (firstLine.startsWith('🤖 AI:'))
304
+ return true;
305
+ // Service status notifications
306
+ if (firstLine.startsWith('✅ ') || firstLine.startsWith('🔄 ') || firstLine.startsWith('ℹ️ '))
307
+ return true;
308
+ // Tool call notifications (sent by streamUpdate in event-handler)
309
+ if (firstLine.startsWith('⚙️ '))
310
+ return true;
311
+ return false;
312
+ }
316
313
  /**
317
314
  * Extract text content from an iLink message's item_list.
318
315
  * Returns the first text item found, or a placeholder for media types.
@@ -334,7 +331,7 @@ function extractTextContent(msg) {
334
331
  case 3 /* MessageItemType.VOICE */: {
335
332
  const transcript = item.voice_item?.text;
336
333
  if (transcript)
337
- return `[语音转文字] ${transcript}`;
334
+ return transcript;
338
335
  return '[语音消息(无文字转录)]';
339
336
  }
340
337
  case 2 /* MessageItemType.IMAGE */:
@@ -413,13 +410,10 @@ export function stopClawbot() {
413
410
  pollController.abort();
414
411
  pollController = null;
415
412
  }
416
- if (reconnectTimer) {
417
- clearTimeout(reconnectTimer);
418
- reconnectTimer = null;
419
- }
420
- stopWatchdog();
413
+ reconnectManager.stop();
414
+ heartbeatMonitor.stop();
421
415
  messageHandler = null;
422
416
  // Don't clear context_token here — it's persisted for startup notifications
423
- updateState('disconnected');
417
+ stateManager.set('disconnected');
424
418
  log.info('ClawBot client stopped');
425
419
  }
@@ -8,6 +8,7 @@ export interface QRLoginSession {
8
8
  sessionKey: string;
9
9
  qrcode: string;
10
10
  qrcodeUrl: string;
11
+ qrcodeImage?: string;
11
12
  startedAt: number;
12
13
  }
13
14
  export interface QRLoginResult {
@@ -5,6 +5,7 @@
5
5
  * Returns bot_token on success.
6
6
  */
7
7
  import { randomUUID } from 'node:crypto';
8
+ import QRCode from 'qrcode';
8
9
  import { createLogger } from '../logger.js';
9
10
  const log = createLogger('ClawBotQR');
10
11
  const ILINK_BASE_URL = 'https://ilinkai.weixin.qq.com';
@@ -64,10 +65,16 @@ export async function startQRLogin() {
64
65
  log.info('Starting ClawBot QR login...');
65
66
  const { qrcode, qrcodeUrl } = await fetchQRCode();
66
67
  log.info(`QR code received, url=${qrcodeUrl}`);
68
+ // iLink 返回的 qrcodeUrl 是一个 HTML 中转页(X-Frame-Options: SAMEORIGIN,
69
+ // Content-Type: text/html),前端无法用 <img> 或 iframe 直接展示。
70
+ // 把这个 URL 本身编码成二维码 PNG——用户扫码后微信打开中转页完成绑定。
71
+ const qrcodeImage = await QRCode.toDataURL(qrcodeUrl, { width: 280, margin: 1 });
72
+ log.info('QR code PNG generated (base64)');
67
73
  return {
68
74
  sessionKey: randomUUID(),
69
75
  qrcode,
70
76
  qrcodeUrl,
77
+ qrcodeImage,
71
78
  startedAt: Date.now(),
72
79
  };
73
80
  }
@@ -1,29 +1,9 @@
1
- export interface CodeBuddyRunCallbacks {
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: CodeBuddyRunCallbacks, options?: CodeBuddyRunOptions): CodeBuddyRunHandle;
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 && /No conversation found|Session not found|Invalid session|Unable to resume/i.test(message)) {
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
- if (cliTimeoutHandle) {
321
- clearTimeout(cliTimeoutHandle);
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
- if (cliTimeoutHandle) {
357
- clearTimeout(cliTimeoutHandle);
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
- if (cliTimeoutHandle) {
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
- export interface CodexRunCallbacks {
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: CodexRunCallbacks, options?: CodexRunOptions): CodexRunHandle;
15
+ export declare function runCodex(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: CodexRunOptions): RunHandle;