@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.
- package/dist/adapters/registry.js +2 -13
- package/dist/config-web-page-script.js +0 -1
- package/dist/config-web.js +0 -3
- package/dist/config.d.ts +0 -3
- package/dist/config.js +2 -51
- package/dist/shared/ai-task.js +1 -1
- package/dist/shared/ai-task.test.js +0 -1
- package/package.json +1 -1
- package/dist/adapters/claude-adapter.d.ts +0 -26
- package/dist/adapters/claude-adapter.js +0 -76
- package/dist/claude/cli-runner.d.ts +0 -29
- package/dist/claude/cli-runner.js +0 -231
- package/dist/claude/process-pool.d.ts +0 -84
- package/dist/claude/process-pool.js +0 -312
|
@@ -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
|
-
|
|
14
|
-
|
|
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
|
}
|
package/dist/config-web.js
CHANGED
|
@@ -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
|
-
//
|
|
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'
|
|
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
|
}
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -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'
|
|
239
|
+
...(toolId === 'claude' ? {} : { hookPort: config.hookPort }),
|
|
240
240
|
...(toolId === 'codex' && config.codexProxy ? { proxy: config.codexProxy } : {}),
|
|
241
241
|
});
|
|
242
242
|
return activeHandle;
|
package/package.json
CHANGED
|
@@ -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
|
-
}
|