@wu529778790/open-im 1.6.8 → 1.6.9-beta.0

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.
@@ -1,5 +1,4 @@
1
1
  import { getConfiguredAiCommands } from '../config.js';
2
- import { ClaudeAdapter } from './claude-adapter.js';
3
2
  import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
4
3
  import { CodexAdapter } from './codex-adapter.js';
5
4
  import { CodeBuddyAdapter } from './codebuddy-adapter.js';
@@ -10,17 +9,8 @@ export function initAdapters(config) {
10
9
  adapters.clear();
11
10
  for (const aiCommand of getConfiguredAiCommands(config)) {
12
11
  if (aiCommand === 'claude') {
13
- if (config.useSdkMode) {
14
- log.info('Claude Agent SDK adapter enabled');
15
- adapters.set('claude', new ClaudeSDKAdapter());
16
- }
17
- else {
18
- log.info('Claude CLI adapter enabled');
19
- adapters.set('claude', new ClaudeAdapter(config.claudeCliPath, {
20
- useProcessPool: true,
21
- idleTimeoutMs: 2 * 60 * 1000,
22
- }));
23
- }
12
+ log.info('Claude Agent SDK adapter enabled');
13
+ adapters.set('claude', new ClaudeSDKAdapter());
24
14
  continue;
25
15
  }
26
16
  if (aiCommand === 'codex') {
@@ -38,7 +28,6 @@ export function getAdapter(aiCommand) {
38
28
  return adapters.get(aiCommand);
39
29
  }
40
30
  export function cleanupAdapters() {
41
- ClaudeAdapter.destroy();
42
31
  ClaudeSDKAdapter.destroy();
43
32
  adapters.clear();
44
33
  }
@@ -667,7 +667,6 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
667
667
  codebuddyCliPath: getValue("ai-codebuddyCliPath"),
668
668
  hookPort: getNumber("ai-hookPort"),
669
669
  logLevel: getValue("ai-logLevel"),
670
- useSdkMode: true,
671
670
  },
672
671
  });
673
672
 
@@ -153,7 +153,6 @@ function buildInitialPayload(file) {
153
153
  hookPort: file.hookPort ?? 35801,
154
154
  logDir: file.logDir ?? "",
155
155
  logLevel: file.logLevel ?? "default",
156
- useSdkMode: file.useSdkMode ?? true,
157
156
  },
158
157
  };
159
158
  }
@@ -277,7 +276,6 @@ function createProbeConfig(values) {
277
276
  hookPort: 35801,
278
277
  logDir: "",
279
278
  logLevel: "INFO",
280
- useSdkMode: true,
281
279
  codebuddyCliPath: "codebuddy",
282
280
  platforms: {},
283
281
  ...values,
@@ -405,7 +403,6 @@ function toFileConfig(payload, existing) {
405
403
  hookPort: payload.ai.hookPort,
406
404
  logDir: payload.ai.logDir === undefined ? existing.logDir : clean(payload.ai.logDir),
407
405
  logLevel: payload.ai.logLevel === "default" ? undefined : payload.ai.logLevel,
408
- useSdkMode: payload.ai.useSdkMode,
409
406
  tools: {
410
407
  claude: {
411
408
  ...existing.tools?.claude,
package/dist/config.d.ts CHANGED
@@ -45,8 +45,6 @@ export interface Config {
45
45
  hookPort: number;
46
46
  logDir: string;
47
47
  logLevel: LogLevel;
48
- /** 是否使用 Agent SDK(进程内执行,无 spawn 开销,响应更快) */
49
- useSdkMode: boolean;
50
48
  platforms: {
51
49
  telegram?: {
52
50
  enabled: boolean;
@@ -185,7 +183,6 @@ export interface FileConfig {
185
183
  hookPort?: number;
186
184
  logDir?: string;
187
185
  logLevel?: LogLevel;
188
- useSdkMode?: boolean;
189
186
  }
190
187
  export declare const CONFIG_PATH: string;
191
188
  export declare function loadFileConfig(): FileConfig;
package/dist/config.js CHANGED
@@ -428,14 +428,9 @@ export function loadConfig() {
428
428
  const hookPort = process.env.HOOK_PORT !== undefined
429
429
  ? parseInt(process.env.HOOK_PORT, 10) || 35801
430
430
  : file.hookPort ?? 35801;
431
- // 当使用 Claude 时,强制使用 SDK 模式(更快,无需安装 CLI
432
- // 使用其他工具(codex/codebuddy)时,才根据配置决定
433
- const useSdkMode = aiCommand === 'claude' || (process.env.USE_SDK_MODE !== undefined
434
- ? process.env.USE_SDK_MODE === 'true'
435
- : file.useSdkMode ?? true);
436
- // 6. 校验 Claude API 凭证(SDK 模式需要)
431
+ // 6. 校验 Claude API 凭证(Claude 固定使用 Agent SDK
437
432
  // 支持:官方 API Key、Auth Token、或自定义 API(第三方模型等,BASE_URL + token)
438
- if (aiCommand === 'claude' && useSdkMode) {
433
+ if (aiCommand === 'claude') {
439
434
  const hasCreds = !!(process.env.ANTHROPIC_API_KEY ||
440
435
  process.env.ANTHROPIC_AUTH_TOKEN ||
441
436
  process.env.CLAUDE_CODE_OAUTH_TOKEN ||
@@ -542,49 +537,6 @@ export function loadConfig() {
542
537
  }
543
538
  }
544
539
  }
545
- // 9. 校验 Claude CLI(SDK 模式不需要 CLI)
546
- if (aiCommand === 'claude' && !useSdkMode) {
547
- if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
548
- try {
549
- accessSync(claudeCliPath, constants.F_OK | constants.X_OK);
550
- }
551
- catch {
552
- throw new Error(`Claude CLI 不可执行: ${claudeCliPath}`);
553
- }
554
- }
555
- else {
556
- // 检查命令是否存在(Windows 用 where,Unix 用 which)
557
- const checkCommand = process.platform === 'win32' ? 'where' : 'which';
558
- try {
559
- execFileSync(checkCommand, [claudeCliPath], {
560
- stdio: 'pipe',
561
- windowsHide: process.platform === 'win32',
562
- });
563
- }
564
- catch {
565
- const installGuide = [
566
- '',
567
- '━━━ Claude CLI 未安装 ━━━',
568
- '',
569
- 'open-im 需要 Claude Code CLI 才能运行。',
570
- '',
571
- '安装方法:',
572
- '',
573
- ' npm install -g @anthropic-ai/claude-code',
574
- '',
575
- '或者:',
576
- ' 1. 访问 https://claude.ai/download',
577
- ' 2. 下载并安装 Claude Code',
578
- '',
579
- '安装后重新运行,例如:',
580
- ' open-im dev',
581
- ' 或 open-im start',
582
- '',
583
- ].join('\n');
584
- throw new Error(installGuide);
585
- }
586
- }
587
- }
588
540
  // 7. 日志与平台配置
589
541
  const logDir = process.env.LOG_DIR ?? file.logDir ?? join(APP_HOME, 'logs');
590
542
  const logLevel = (process.env.LOG_LEVEL?.toUpperCase() ?? file.logLevel ?? 'INFO');
@@ -715,7 +667,6 @@ export function loadConfig() {
715
667
  hookPort,
716
668
  logDir,
717
669
  logLevel,
718
- useSdkMode,
719
670
  platforms,
720
671
  };
721
672
  }
@@ -236,7 +236,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
236
236
  timeoutMs,
237
237
  model: sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel,
238
238
  chatId: ctx.chatId,
239
- ...(toolId === 'claude' && config.useSdkMode ? {} : { hookPort: config.hookPort }),
239
+ ...(toolId === 'claude' ? {} : { hookPort: config.hookPort }),
240
240
  ...(toolId === 'codex' && config.codexProxy ? { proxy: config.codexProxy } : {}),
241
241
  });
242
242
  return activeHandle;
@@ -38,7 +38,6 @@ describe("runAITask", () => {
38
38
  claudeTimeoutMs: 600000,
39
39
  claudeSkipPermissions: false,
40
40
  claudeModel: "",
41
- useSdkMode: true,
42
41
  hookPort: 35801,
43
42
  codexProxy: "",
44
43
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.6.8",
3
+ "version": "1.6.9-beta.0",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,26 +0,0 @@
1
- import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
2
- export declare class ClaudeAdapter implements ToolAdapter {
3
- private cliPath;
4
- readonly toolId = "claude";
5
- constructor(cliPath: string, adapterOptions?: {
6
- useProcessPool?: boolean;
7
- idleTimeoutMs?: number;
8
- });
9
- run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
10
- /**
11
- * Get the number of cached entries in the pool.
12
- */
13
- static getCacheSize(): number;
14
- /**
15
- * Get the number of active processes in the pool.
16
- */
17
- static getActiveProcessCount(): number;
18
- /**
19
- * Terminate all cached entries and processes.
20
- */
21
- static terminateAll(): void;
22
- /**
23
- * Destroy the process pool and cleanup resources.
24
- */
25
- static destroy(): void;
26
- }
@@ -1,76 +0,0 @@
1
- import { runClaude } from '../claude/cli-runner.js';
2
- import { ClaudeProcessPool } from '../claude/process-pool.js';
3
- // Global process pool instance
4
- let processPool = null;
5
- export class ClaudeAdapter {
6
- cliPath;
7
- toolId = 'claude';
8
- constructor(cliPath, adapterOptions) {
9
- this.cliPath = cliPath;
10
- const useProcessPool = adapterOptions?.useProcessPool ?? true;
11
- const idleTimeoutMs = adapterOptions?.idleTimeoutMs ?? 2 * 60 * 1000; // 2 minutes default
12
- if (useProcessPool && !processPool) {
13
- // Initialize process pool with configurable idle timeout
14
- processPool = new ClaudeProcessPool(idleTimeoutMs);
15
- }
16
- }
17
- run(prompt, sessionId, workDir, callbacks, options) {
18
- const opts = {
19
- skipPermissions: options?.skipPermissions,
20
- permissionMode: options?.permissionMode,
21
- timeoutMs: options?.timeoutMs,
22
- model: options?.model,
23
- chatId: options?.chatId,
24
- hookPort: options?.hookPort,
25
- };
26
- // Use process pool if enabled and userId is available
27
- if (processPool && opts.chatId) {
28
- let aborted = false;
29
- // Execute using process pool with userId from chatId
30
- processPool
31
- .execute(opts.chatId, sessionId, this.cliPath, prompt, workDir, callbacks, opts)
32
- .catch((err) => {
33
- if (!aborted && callbacks.onError) {
34
- callbacks.onError(err.message);
35
- }
36
- });
37
- return {
38
- abort: () => {
39
- aborted = true;
40
- processPool.terminate(opts.chatId, sessionId);
41
- },
42
- };
43
- }
44
- // Fall back to original implementation
45
- return runClaude(this.cliPath, prompt, sessionId, workDir, callbacks, opts);
46
- }
47
- /**
48
- * Get the number of cached entries in the pool.
49
- */
50
- static getCacheSize() {
51
- return processPool?.size() ?? 0;
52
- }
53
- /**
54
- * Get the number of active processes in the pool.
55
- */
56
- static getActiveProcessCount() {
57
- return processPool?.activeCount() ?? 0;
58
- }
59
- /**
60
- * Terminate all cached entries and processes.
61
- */
62
- static terminateAll() {
63
- if (processPool) {
64
- processPool.terminateAll();
65
- }
66
- }
67
- /**
68
- * Destroy the process pool and cleanup resources.
69
- */
70
- static destroy() {
71
- if (processPool) {
72
- processPool.destroy();
73
- processPool = null;
74
- }
75
- }
76
- }
@@ -1,29 +0,0 @@
1
- export interface ClaudeRunCallbacks {
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
- }
18
- export interface ClaudeRunOptions {
19
- skipPermissions?: boolean;
20
- permissionMode?: 'default' | 'acceptEdits' | 'plan';
21
- timeoutMs?: number;
22
- model?: string;
23
- chatId?: string;
24
- hookPort?: number;
25
- }
26
- export interface ClaudeRunHandle {
27
- abort: () => void;
28
- }
29
- export declare function runClaude(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: ClaudeRunCallbacks, options?: ClaudeRunOptions): ClaudeRunHandle;
@@ -1,231 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createInterface } from "node:readline";
3
- import { parseStreamLine, extractTextDelta, extractThinkingDelta, extractResult, } from "./stream-parser.js";
4
- import { isStreamInit, isContentBlockStart, isContentBlockDelta, isContentBlockStop, } from "./types.js";
5
- import { createLogger } from "../logger.js";
6
- const log = createLogger("CliRunner");
7
- export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, options) {
8
- const args = [
9
- "-p",
10
- "--output-format",
11
- "stream-json",
12
- "--verbose",
13
- "--include-partial-messages",
14
- ];
15
- if (options?.skipPermissions) {
16
- args.push("--dangerously-skip-permissions");
17
- }
18
- else if (options?.permissionMode) {
19
- args.push("--permission-mode", options.permissionMode);
20
- }
21
- if (options?.model)
22
- args.push("--model", options.model);
23
- if (sessionId)
24
- args.push("--resume", sessionId);
25
- args.push("--", prompt);
26
- const env = {};
27
- for (const [k, v] of Object.entries(process.env)) {
28
- // Skip CLAUDECODE to prevent nested session detection
29
- if (k === "CLAUDECODE")
30
- continue;
31
- if (v !== undefined)
32
- env[k] = v;
33
- }
34
- if (options?.chatId)
35
- env.CC_IM_CHAT_ID = options.chatId;
36
- if (options?.hookPort)
37
- env.CC_IM_HOOK_PORT = String(options.hookPort);
38
- // 使用 shell: false 直接 spawn,避免 shell 对参数按空格拆分
39
- // (用户 prompt 如 "npm 你好" 在 shell: true 下会被拆成 "npm" 和 "你好",CLI 只收到第一个)
40
- log.info(`Spawning CLI: path=${cliPath}, platform=${process.platform}`);
41
- const child = spawn(cliPath, args, {
42
- cwd: workDir,
43
- stdio: ["ignore", "pipe", "pipe"],
44
- env,
45
- windowsHide: process.platform === "win32",
46
- });
47
- log.info(`Claude CLI: pid=${child.pid}, cwd=${workDir}, session=${sessionId ?? "new"}`);
48
- let accumulated = "";
49
- let accumulatedThinking = "";
50
- let completed = false;
51
- let model = "";
52
- const toolStats = {};
53
- const pendingToolInputs = new Map();
54
- const MAX_TIMEOUT = 2_147_483_647;
55
- const timeoutMs = options?.timeoutMs && options.timeoutMs > 0
56
- ? Math.min(options.timeoutMs, MAX_TIMEOUT)
57
- : 0;
58
- let timeoutHandle = null;
59
- if (timeoutMs > 0) {
60
- timeoutHandle = setTimeout(() => {
61
- if (!completed && !child.killed) {
62
- completed = true;
63
- log.warn(`Claude CLI timeout after ${timeoutMs}ms, killing pid=${child.pid}`);
64
- child.kill("SIGTERM");
65
- callbacks.onError(`执行超时(${timeoutMs}ms),已终止进程`);
66
- }
67
- }, timeoutMs);
68
- }
69
- // stderr 截断:只保留首 4KB + 尾 6KB,减少 I/O 和内存
70
- const MAX_STDERR_HEAD = 4 * 1024;
71
- const MAX_STDERR_TAIL = 6 * 1024;
72
- let stderrHead = "";
73
- let stderrTail = "";
74
- let stderrTotal = 0;
75
- let stderrHeadFull = false;
76
- child.stderr?.on("data", (chunk) => {
77
- const text = chunk.toString();
78
- stderrTotal += text.length;
79
- if (!stderrHeadFull) {
80
- const room = MAX_STDERR_HEAD - stderrHead.length;
81
- if (room > 0) {
82
- stderrHead += text.slice(0, room);
83
- if (stderrHead.length >= MAX_STDERR_HEAD)
84
- stderrHeadFull = true;
85
- }
86
- }
87
- stderrTail += text;
88
- if (stderrTail.length > MAX_STDERR_TAIL) {
89
- stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
90
- }
91
- });
92
- const rl = createInterface({ input: child.stdout });
93
- rl.on("line", (line) => {
94
- const event = parseStreamLine(line);
95
- if (!event)
96
- return;
97
- if (isStreamInit(event)) {
98
- model = event.model;
99
- callbacks.onSessionId?.(event.session_id);
100
- }
101
- const delta = extractTextDelta(event);
102
- if (delta) {
103
- accumulated += delta.text;
104
- callbacks.onText(accumulated);
105
- return;
106
- }
107
- const thinking = extractThinkingDelta(event);
108
- if (thinking) {
109
- accumulatedThinking += thinking.text;
110
- callbacks.onThinking?.(accumulatedThinking);
111
- return;
112
- }
113
- if (isContentBlockStart(event) &&
114
- event.event.content_block?.type === "tool_use") {
115
- const name = event.event.content_block.name;
116
- if (name)
117
- pendingToolInputs.set(event.event.index, { name, json: "" });
118
- return;
119
- }
120
- if (isContentBlockDelta(event) &&
121
- event.event.delta?.type === "input_json_delta") {
122
- const pending = pendingToolInputs.get(event.event.index);
123
- if (pending)
124
- pending.json += event.event.delta.partial_json ?? "";
125
- return;
126
- }
127
- if (isContentBlockStop(event)) {
128
- const pending = pendingToolInputs.get(event.event.index);
129
- if (pending) {
130
- toolStats[pending.name] = (toolStats[pending.name] || 0) + 1;
131
- let input;
132
- try {
133
- input = JSON.parse(pending.json);
134
- }
135
- catch {
136
- /* empty */
137
- }
138
- callbacks.onToolUse?.(pending.name, input);
139
- pendingToolInputs.delete(event.event.index);
140
- }
141
- return;
142
- }
143
- const result = extractResult(event);
144
- if (result) {
145
- completed = true;
146
- if (timeoutHandle)
147
- clearTimeout(timeoutHandle);
148
- const fullResult = {
149
- ...result,
150
- accumulated,
151
- model,
152
- toolStats,
153
- };
154
- if (!accumulated && fullResult.result)
155
- accumulated = fullResult.result;
156
- callbacks.onComplete(fullResult);
157
- }
158
- });
159
- let exitCode = null;
160
- let rlClosed = false;
161
- let childClosed = false;
162
- const finalize = () => {
163
- if (!rlClosed || !childClosed)
164
- return;
165
- if (timeoutHandle)
166
- clearTimeout(timeoutHandle);
167
- if (!completed) {
168
- if (exitCode !== null && exitCode !== 0) {
169
- let errMsg = "";
170
- if (stderrTotal > 0) {
171
- if (!stderrHeadFull) {
172
- errMsg = stderrHead;
173
- }
174
- else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
175
- errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
176
- }
177
- else {
178
- errMsg =
179
- stderrHead +
180
- `\n\n... (省略 ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} 字节) ...\n\n` +
181
- stderrTail;
182
- }
183
- }
184
- callbacks.onError(errMsg || `Claude CLI exited with code ${exitCode}`);
185
- }
186
- else {
187
- callbacks.onComplete({
188
- success: true,
189
- result: accumulated,
190
- accumulated,
191
- cost: 0,
192
- durationMs: 0,
193
- model,
194
- numTurns: 0,
195
- toolStats,
196
- });
197
- }
198
- }
199
- };
200
- child.on("close", (code) => {
201
- log.info(`Claude CLI closed: exitCode=${code}, pid=${child.pid}`);
202
- exitCode = code;
203
- childClosed = true;
204
- finalize();
205
- });
206
- rl.on("close", () => {
207
- rlClosed = true;
208
- finalize();
209
- });
210
- child.on("error", (err) => {
211
- const errorCode = err.code;
212
- log.error(`Claude CLI spawn error: ${err.message}, code=${errorCode}, path=${cliPath}`);
213
- if (timeoutHandle)
214
- clearTimeout(timeoutHandle);
215
- if (!completed) {
216
- completed = true;
217
- callbacks.onError(`Failed to start Claude CLI: ${err.message}`);
218
- }
219
- childClosed = true;
220
- finalize();
221
- });
222
- return {
223
- abort: () => {
224
- if (timeoutHandle)
225
- clearTimeout(timeoutHandle);
226
- rl.close();
227
- if (!child.killed)
228
- child.kill("SIGTERM");
229
- },
230
- };
231
- }
@@ -1,84 +0,0 @@
1
- export interface ClaudeRunCallbacks {
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
- }
18
- export interface ClaudeResult {
19
- success: boolean;
20
- result: string;
21
- accumulated: string;
22
- cost: number;
23
- durationMs: number;
24
- model?: string;
25
- numTurns: number;
26
- toolStats: Record<string, number>;
27
- }
28
- export interface ClaudeRunOptions {
29
- skipPermissions?: boolean;
30
- permissionMode?: 'default' | 'acceptEdits' | 'plan';
31
- timeoutMs?: number;
32
- model?: string;
33
- chatId?: string;
34
- hookPort?: number;
35
- }
36
- /**
37
- * Process pool that manages cached session configurations.
38
- *
39
- * Since Claude CLI doesn't support persistent mode, we use this pool to:
40
- * 1. Cache active sessions for faster resume using --resume
41
- * 2. Track which sessions are actively being used
42
- * 3. Clean up stale entries
43
- *
44
- * The main benefit is that resumed sessions don't need to reload conversation history.
45
- */
46
- export declare class ClaudeProcessPool {
47
- private entries;
48
- private activeProcesses;
49
- private cleanupTimer;
50
- private readonly ttl;
51
- constructor(ttlMs?: number);
52
- /**
53
- * Execute a prompt, reusing cached session if available.
54
- */
55
- execute(userId: string, sessionId: string | undefined, cliPath: string, prompt: string, workDir: string, callbacks: ClaudeRunCallbacks, options?: ClaudeRunOptions): Promise<ClaudeResult>;
56
- /**
57
- * Run a Claude CLI process for a single request.
58
- */
59
- private runProcess;
60
- /**
61
- * Clean up expired entries.
62
- */
63
- private cleanup;
64
- /**
65
- * Terminate the active process for a session.
66
- */
67
- terminate(userId: string, sessionId: string | undefined): void;
68
- /**
69
- * Terminate all active processes and clear cache.
70
- */
71
- terminateAll(): void;
72
- /**
73
- * Get the number of cached entries.
74
- */
75
- size(): number;
76
- /**
77
- * Get the number of active processes.
78
- */
79
- activeCount(): number;
80
- /**
81
- * Destroy the process pool and cleanup resources.
82
- */
83
- destroy(): void;
84
- }
@@ -1,312 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createInterface } from "node:readline";
3
- import { createLogger } from "../logger.js";
4
- import { parseStreamLine, extractTextDelta, extractThinkingDelta, extractResult, } from "./stream-parser.js";
5
- import { isStreamInit, isContentBlockStart, isContentBlockDelta, isContentBlockStop, } from "./types.js";
6
- const log = createLogger("ProcessPool");
7
- /**
8
- * Process pool that manages cached session configurations.
9
- *
10
- * Since Claude CLI doesn't support persistent mode, we use this pool to:
11
- * 1. Cache active sessions for faster resume using --resume
12
- * 2. Track which sessions are actively being used
13
- * 3. Clean up stale entries
14
- *
15
- * The main benefit is that resumed sessions don't need to reload conversation history.
16
- */
17
- export class ClaudeProcessPool {
18
- entries = new Map();
19
- activeProcesses = new Map();
20
- cleanupTimer = null;
21
- ttl;
22
- constructor(ttlMs = 2 * 60 * 1000) {
23
- this.ttl = ttlMs;
24
- log.info(`Process pool created with TTL: ${ttlMs}ms`);
25
- // Periodic cleanup of expired entries
26
- this.cleanupTimer = setInterval(() => {
27
- this.cleanup();
28
- }, 60 * 1000); // Every minute
29
- }
30
- /**
31
- * Execute a prompt, reusing cached session if available.
32
- */
33
- async execute(userId, sessionId, cliPath, prompt, workDir, callbacks, options) {
34
- const key = `${userId}:${sessionId || "default"}`;
35
- // Update cache entry (tracks active sessions)
36
- const entry = this.entries.get(key);
37
- if (entry) {
38
- entry.lastUsed = Date.now();
39
- }
40
- else {
41
- this.entries.set(key, { lastUsed: Date.now() });
42
- }
43
- // Check if there's an active process for this session
44
- const activePid = this.activeProcesses.get(key);
45
- if (activePid && !activePid.killed) {
46
- log.info(`Session has active process: key=${key}, pid=${activePid.pid}`);
47
- // Wait a bit for the previous process to complete
48
- await new Promise(resolve => setTimeout(resolve, 100));
49
- }
50
- // Run the Claude CLI process
51
- return this.runProcess(key, cliPath, prompt, sessionId, workDir, callbacks, options || {});
52
- }
53
- /**
54
- * Run a Claude CLI process for a single request.
55
- */
56
- runProcess(key, cliPath, prompt, sessionId, workDir, callbacks, options) {
57
- return new Promise((resolve, reject) => {
58
- const args = [
59
- "-p",
60
- "--output-format",
61
- "stream-json",
62
- "--verbose",
63
- "--include-partial-messages",
64
- ];
65
- if (options.skipPermissions) {
66
- args.push("--dangerously-skip-permissions");
67
- }
68
- else if (options.permissionMode) {
69
- args.push("--permission-mode", options.permissionMode);
70
- }
71
- if (options.model)
72
- args.push("--model", options.model);
73
- if (sessionId)
74
- args.push("--resume", sessionId);
75
- args.push("--", prompt);
76
- // Environment setup
77
- const env = {};
78
- for (const [k, v] of Object.entries(process.env)) {
79
- if (k === "CLAUDECODE")
80
- continue;
81
- if (v !== undefined)
82
- env[k] = v;
83
- }
84
- if (options.chatId)
85
- env.CC_IM_CHAT_ID = options.chatId;
86
- if (options.hookPort)
87
- env.CC_IM_HOOK_PORT = String(options.hookPort);
88
- // 使用 shell: false 直接 spawn,避免 shell 对参数按空格拆分
89
- // (用户 prompt 如 "npm 你好" 在 shell: true 下会被拆成 "npm" 和 "你好",CLI 只收到第一个)
90
- const child = spawn(cliPath, args, {
91
- cwd: workDir,
92
- stdio: ["ignore", "pipe", "pipe"],
93
- env,
94
- windowsHide: process.platform === "win32",
95
- });
96
- log.info(`Started process: pid=${child.pid}, key=${key}`);
97
- // Track active process
98
- this.activeProcesses.set(key, child);
99
- // State tracking
100
- let accumulated = "";
101
- let accumulatedThinking = "";
102
- let model = "";
103
- const toolStats = {};
104
- const pendingToolInputs = new Map();
105
- const startTime = Date.now();
106
- // stderr 截断:只保留首 4KB + 尾 6KB
107
- const MAX_STDERR_HEAD = 4 * 1024;
108
- const MAX_STDERR_TAIL = 6 * 1024;
109
- let stderrHead = "";
110
- let stderrTail = "";
111
- let stderrTotal = 0;
112
- let stderrHeadFull = false;
113
- child.stderr?.on("data", (chunk) => {
114
- const text = chunk.toString();
115
- stderrTotal += text.length;
116
- if (!stderrHeadFull) {
117
- const room = MAX_STDERR_HEAD - stderrHead.length;
118
- if (room > 0) {
119
- stderrHead += text.slice(0, room);
120
- if (stderrHead.length >= MAX_STDERR_HEAD)
121
- stderrHeadFull = true;
122
- }
123
- }
124
- stderrTail += text;
125
- if (stderrTail.length > MAX_STDERR_TAIL) {
126
- stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
127
- }
128
- });
129
- const rl = createInterface({ input: child.stdout });
130
- rl.on("line", (line) => {
131
- const event = parseStreamLine(line);
132
- if (!event)
133
- return;
134
- if (isStreamInit(event)) {
135
- model = event.model;
136
- callbacks.onSessionId?.(event.session_id);
137
- return;
138
- }
139
- const delta = extractTextDelta(event);
140
- if (delta) {
141
- accumulated += delta.text;
142
- callbacks.onText(accumulated);
143
- return;
144
- }
145
- const thinking = extractThinkingDelta(event);
146
- if (thinking) {
147
- accumulatedThinking += thinking.text;
148
- callbacks.onThinking?.(accumulatedThinking);
149
- return;
150
- }
151
- if (isContentBlockStart(event) &&
152
- event.event.content_block?.type === "tool_use") {
153
- const name = event.event.content_block.name;
154
- if (name)
155
- pendingToolInputs.set(event.event.index, { name, json: "" });
156
- return;
157
- }
158
- if (isContentBlockDelta(event) &&
159
- event.event.delta?.type === "input_json_delta") {
160
- const pending = pendingToolInputs.get(event.event.index);
161
- if (pending)
162
- pending.json += event.event.delta.partial_json ?? "";
163
- return;
164
- }
165
- if (isContentBlockStop(event)) {
166
- const pending = pendingToolInputs.get(event.event.index);
167
- if (pending) {
168
- toolStats[pending.name] = (toolStats[pending.name] || 0) + 1;
169
- let input;
170
- try {
171
- input = JSON.parse(pending.json);
172
- }
173
- catch {
174
- /* empty */
175
- }
176
- callbacks.onToolUse?.(pending.name, input);
177
- pendingToolInputs.delete(event.event.index);
178
- }
179
- return;
180
- }
181
- const result = extractResult(event);
182
- if (result) {
183
- const fullResult = {
184
- ...result,
185
- accumulated,
186
- model,
187
- toolStats,
188
- };
189
- if (!accumulated && fullResult.result) {
190
- accumulated = fullResult.result;
191
- }
192
- callbacks.onComplete(fullResult);
193
- resolve(fullResult);
194
- }
195
- });
196
- let exitCode = null;
197
- let rlClosed = false;
198
- let childClosed = false;
199
- let resolved = false;
200
- const finalize = () => {
201
- if (!rlClosed || !childClosed || resolved)
202
- return;
203
- this.activeProcesses.delete(key);
204
- resolved = true;
205
- if (exitCode !== null && exitCode !== 0) {
206
- let errorMsg = "";
207
- if (stderrTotal > 0) {
208
- if (!stderrHeadFull) {
209
- errorMsg = stderrHead;
210
- }
211
- else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
212
- errorMsg =
213
- stderrHead +
214
- stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
215
- }
216
- else {
217
- errorMsg =
218
- stderrHead +
219
- `\n\n... (省略 ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} 字节) ...\n\n` +
220
- stderrTail;
221
- }
222
- }
223
- const msg = errorMsg || `Claude CLI exited with code ${exitCode}`;
224
- callbacks.onError(msg);
225
- reject(new Error(msg));
226
- }
227
- // If exitCode is 0 and we haven't resolved yet, the result was already sent
228
- // via the extractResult handler. This is just cleanup.
229
- };
230
- child.on("close", (code) => {
231
- log.info(`Process closed: code=${code}, pid=${child.pid}, key=${key}`);
232
- exitCode = code;
233
- childClosed = true;
234
- finalize();
235
- });
236
- rl.on("close", () => {
237
- rlClosed = true;
238
- finalize();
239
- });
240
- child.on("error", (err) => {
241
- log.error(`Process error: ${err.message}, pid=${child.pid}, key=${key}`);
242
- this.activeProcesses.delete(key);
243
- const errorMsg = `Failed to start Claude CLI: ${err.message}`;
244
- callbacks.onError(errorMsg);
245
- reject(new Error(errorMsg));
246
- });
247
- });
248
- }
249
- /**
250
- * Clean up expired entries.
251
- */
252
- cleanup() {
253
- const now = Date.now();
254
- let cleaned = 0;
255
- for (const [key, entry] of this.entries.entries()) {
256
- if (now - entry.lastUsed > this.ttl) {
257
- this.entries.delete(key);
258
- cleaned++;
259
- }
260
- }
261
- if (cleaned > 0) {
262
- log.info(`Cleaned up ${cleaned} expired entries, ${this.entries.size} remaining`);
263
- }
264
- }
265
- /**
266
- * Terminate the active process for a session.
267
- */
268
- terminate(userId, sessionId) {
269
- const key = `${userId}:${sessionId || "default"}`;
270
- const child = this.activeProcesses.get(key);
271
- if (child && !child.killed) {
272
- child.kill("SIGTERM");
273
- this.activeProcesses.delete(key);
274
- }
275
- // Also remove from cache
276
- this.entries.delete(key);
277
- }
278
- /**
279
- * Terminate all active processes and clear cache.
280
- */
281
- terminateAll() {
282
- for (const child of this.activeProcesses.values()) {
283
- if (!child.killed) {
284
- child.kill("SIGTERM");
285
- }
286
- }
287
- this.activeProcesses.clear();
288
- this.entries.clear();
289
- }
290
- /**
291
- * Get the number of cached entries.
292
- */
293
- size() {
294
- return this.entries.size;
295
- }
296
- /**
297
- * Get the number of active processes.
298
- */
299
- activeCount() {
300
- return this.activeProcesses.size;
301
- }
302
- /**
303
- * Destroy the process pool and cleanup resources.
304
- */
305
- destroy() {
306
- if (this.cleanupTimer) {
307
- clearInterval(this.cleanupTimer);
308
- this.cleanupTimer = null;
309
- }
310
- this.terminateAll();
311
- }
312
- }