@wu529778790/open-im 1.7.0-beta.2 → 1.7.1-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.
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,76 @@
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,18 +1,18 @@
1
1
  /**
2
- * Claude SDK Adapter - 使用 Agent SDK V2 Session API 实现真正的多轮对话
2
+ * Claude SDK Adapter - 使用 Agent SDK 实现持久会话,无需每次 spawn 进程
3
3
  *
4
- * V2 API 优势:
5
- * 1. 进程内执行 - 无 fork/exec 开销
6
- * 2. 持久会话 - SDKSession 对象保持会话状态,支持真正的多轮对话
7
- * 3. 流式输出 - 支持实时增量更新
4
+ * 优势:
5
+ * 1. 进程内执行 - 无 fork/exec 开销,响应更快
6
+ * 2. 会话复用 - resume 保留上下文,无需重新加载历史
7
+ * 3. 流式输出 - includePartialMessages 支持 text_delta、thinking_delta
8
8
  *
9
- * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
9
+ * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN(claude setup-token)
10
10
  */
11
11
  import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
12
12
  export declare class ClaudeSDKAdapter implements ToolAdapter {
13
13
  readonly toolId = "claude-sdk";
14
14
  /**
15
- * 清理所有活跃的 SDK 会话和流
15
+ * 清理所有活跃的 SDK 查询
16
16
  */
17
17
  static destroy(): void;
18
18
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
@@ -1,21 +1,18 @@
1
1
  /**
2
- * Claude SDK Adapter - 使用 Agent SDK V2 Session API 实现真正的多轮对话
2
+ * Claude SDK Adapter - 使用 Agent SDK 实现持久会话,无需每次 spawn 进程
3
3
  *
4
- * V2 API 优势:
5
- * 1. 进程内执行 - 无 fork/exec 开销
6
- * 2. 持久会话 - SDKSession 对象保持会话状态,支持真正的多轮对话
7
- * 3. 流式输出 - 支持实时增量更新
4
+ * 优势:
5
+ * 1. 进程内执行 - 无 fork/exec 开销,响应更快
6
+ * 2. 会话复用 - resume 保留上下文,无需重新加载历史
7
+ * 3. 流式输出 - includePartialMessages 支持 text_delta、thinking_delta
8
8
  *
9
- * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
9
+ * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN(claude setup-token)
10
10
  */
11
- import { unstable_v2_createSession, unstable_v2_resumeSession } from '@anthropic-ai/claude-agent-sdk';
11
+ import { query } from '@anthropic-ai/claude-agent-sdk';
12
12
  import { createLogger } from '../logger.js';
13
13
  const log = createLogger('ClaudeSDK');
14
- // 存储所有活跃的 SDKSession 对象,key 为 sessionId
15
- // 使用 Map 而不是 Set,因为我们需要通过 sessionId 获取 session
16
- const activeSessions = new Map();
17
- // 存储正在进行的流式迭代器,用于中断
18
- const activeStreams = new Set();
14
+ // 存储所有活跃的查询,用于清理
15
+ const activeQueries = new Set();
19
16
  function isStreamEvent(msg) {
20
17
  return msg.type === 'stream_event';
21
18
  }
@@ -23,82 +20,33 @@ function isSystemInit(msg) {
23
20
  const m = msg;
24
21
  return m.type === 'system' && m.subtype === 'init';
25
22
  }
26
- function isAssistant(msg) {
27
- return msg.type === 'assistant';
28
- }
29
23
  function isResult(msg) {
30
24
  return msg.type === 'result';
31
25
  }
32
- /**
33
- * 获取或创建 SDKSession
34
- * @param sessionId 已有的 sessionId,如果为 undefined 则创建新会话
35
- * @param workDir 工作目录
36
- * @param model 模型名称
37
- * @param permissionMode 权限模式
38
- * @returns SDKSession 对象和实际的 sessionId
39
- */
40
- async function getOrCreateSession(sessionId, _workDir, // 保留参数以备将来使用
41
- model, permissionMode) {
42
- const sessionOptions = {
43
- model: model || 'claude-opus-4-5',
44
- permissionMode,
45
- // 可以添加其他选项,如 hooks, allowedTools 等
46
- };
47
- let session;
48
- if (sessionId) {
49
- // 尝试恢复已有会话
50
- try {
51
- log.info(`Attempting to resume session: ${sessionId}`);
52
- session = unstable_v2_resumeSession(sessionId, sessionOptions);
53
- activeSessions.set(sessionId, session);
54
- log.info(`Successfully resumed session: ${sessionId}`);
55
- return { session, sessionId };
56
- }
57
- catch (err) {
58
- log.warn(`Failed to resume session ${sessionId}, creating new one: ${err}`);
59
- // 恢复失败,创建新会话
60
- }
61
- }
62
- // 创建新会话
63
- session = unstable_v2_createSession(sessionOptions);
64
- // 新会话的 sessionId 需要从第一个消息中获取
65
- // 暂时返回 undefined,稍后在 init 消息中获取
66
- const tempId = `pending-${Date.now()}`;
67
- activeSessions.set(tempId, session);
68
- log.info(`Created new session (tempId: ${tempId})`);
69
- return { session, sessionId: tempId };
26
+ function isAssistant(msg) {
27
+ return msg.type === 'assistant';
70
28
  }
71
29
  export class ClaudeSDKAdapter {
72
30
  toolId = 'claude-sdk';
73
31
  /**
74
- * 清理所有活跃的 SDK 会话和流
32
+ * 清理所有活跃的 SDK 查询
75
33
  */
76
34
  static destroy() {
77
- for (const stream of activeStreams) {
35
+ for (const q of activeQueries) {
78
36
  try {
79
- if (stream && typeof stream.return === 'function') {
80
- stream.return();
37
+ if (q && typeof q.return === 'function') {
38
+ q.return();
81
39
  }
82
40
  }
83
41
  catch {
84
42
  /* ignore */
85
43
  }
86
44
  }
87
- activeStreams.clear();
88
- for (const session of activeSessions.values()) {
89
- try {
90
- session.close();
91
- }
92
- catch {
93
- /* ignore */
94
- }
95
- }
96
- activeSessions.clear();
45
+ activeQueries.clear();
97
46
  }
98
47
  run(prompt, sessionId, workDir, callbacks, options) {
99
48
  const abortController = new AbortController();
100
- let streamClosed = false;
101
- let actualSessionId;
49
+ let queryClosed = false;
102
50
  const permissionMode = options?.skipPermissions
103
51
  ? 'bypassPermissions'
104
52
  : options?.permissionMode === 'acceptEdits'
@@ -106,47 +54,65 @@ export class ClaudeSDKAdapter {
106
54
  : options?.permissionMode === 'plan'
107
55
  ? 'plan'
108
56
  : 'default';
109
- const runSession = async () => {
57
+ const runQuery = async () => {
110
58
  try {
111
- // 检查环境变量
59
+ // 调试:检查关键环境变量
112
60
  const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
113
61
  const hasAuthToken = !!process.env.ANTHROPIC_AUTH_TOKEN;
114
- if (!hasApiKey && !hasAuthToken) {
62
+ const hasBaseUrl = !!process.env.ANTHROPIC_BASE_URL;
63
+ if (!hasApiKey && !hasAuthToken && !hasBaseUrl) {
115
64
  log.warn('Claude SDK: No API credentials found in environment variables');
116
65
  }
117
- log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
118
- // 获取或创建会话
119
- const { session } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
120
- // 发送用户消息
121
- await session.send(prompt);
122
- // 获取响应流
123
- const stream = session.stream();
124
- activeStreams.add(stream);
66
+ const opts = {
67
+ cwd: workDir,
68
+ resume: sessionId,
69
+ includePartialMessages: true,
70
+ permissionMode,
71
+ model: options?.model,
72
+ abortController,
73
+ allowDangerouslySkipPermissions: permissionMode === 'bypassPermissions',
74
+ };
75
+ log.info(`[ClaudeSDK] Starting query: prompt="${prompt.slice(0, 50)}...", sessionId=${sessionId ?? 'new'}, cwd=${workDir}`);
76
+ let q;
77
+ try {
78
+ q = query({
79
+ prompt,
80
+ options: opts,
81
+ });
82
+ }
83
+ catch (queryInitErr) {
84
+ log.error(`[ClaudeSDK] Query initialization failed:`, queryInitErr);
85
+ // 如果是会话文件问题,尝试作为新会话
86
+ if (sessionId && (queryInitErr.code === 'ENOENT' ||
87
+ queryInitErr.message.includes('ENOENT') ||
88
+ queryInitErr.message.includes('not found'))) {
89
+ log.warn(`[ClaudeSDK] Session file not found, starting new session`);
90
+ callbacks.onSessionInvalid?.();
91
+ // 重新创建查询,不带 resume
92
+ const newOpts = { ...opts, resume: undefined };
93
+ q = query({
94
+ prompt,
95
+ options: newOpts,
96
+ });
97
+ }
98
+ else {
99
+ callbacks.onError(`查询初始化失败: ${queryInitErr.message}`);
100
+ return;
101
+ }
102
+ }
103
+ // 将查询添加到活跃列表
104
+ activeQueries.add(q);
125
105
  let accumulated = '';
126
106
  let accumulatedThinking = '';
127
107
  const toolStats = {};
128
108
  try {
129
- for await (const msg of stream) {
130
- if (abortController.signal.aborted) {
131
- log.info('Stream aborted by user');
109
+ for await (const msg of q) {
110
+ if (abortController.signal.aborted)
132
111
  break;
133
- }
134
- // 获取实际的 sessionId(从 init 消息中)
135
112
  if (isSystemInit(msg)) {
136
- const newSessionId = msg.session_id;
137
- if (newSessionId && newSessionId !== actualSessionId) {
138
- // 更新 sessionId 映射
139
- if (actualSessionId && actualSessionId.startsWith('pending-')) {
140
- activeSessions.delete(actualSessionId);
141
- }
142
- activeSessions.set(newSessionId, session);
143
- actualSessionId = newSessionId;
144
- log.info(`[V2] Got actual sessionId: ${newSessionId}`);
145
- callbacks.onSessionId?.(newSessionId);
146
- }
113
+ callbacks.onSessionId?.(msg.session_id);
147
114
  continue;
148
115
  }
149
- // 处理流式事件
150
116
  if (isStreamEvent(msg)) {
151
117
  const ev = msg.event;
152
118
  if (ev?.type === 'content_block_delta' && ev.delta) {
@@ -161,7 +127,6 @@ export class ClaudeSDKAdapter {
161
127
  }
162
128
  continue;
163
129
  }
164
- // 处理助手消息(工具调用)
165
130
  if (isAssistant(msg)) {
166
131
  const content = msg.message?.content;
167
132
  for (const block of content ?? []) {
@@ -172,22 +137,16 @@ export class ClaudeSDKAdapter {
172
137
  }
173
138
  continue;
174
139
  }
175
- // 处理结果消息
176
140
  if (isResult(msg)) {
177
- streamClosed = true;
141
+ queryClosed = true;
178
142
  const m = msg;
179
143
  const success = m.subtype === 'success';
180
144
  const errs = m.errors ?? [];
181
- log.info(`[V2] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
182
- // 检查会话错误
183
- if (!success) {
184
- const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
185
- if (noConvErr) {
186
- log.warn(`Session ${actualSessionId} not found, may need to create new one`);
187
- callbacks.onSessionInvalid?.();
188
- }
189
- const errMsg = errs[0] || '未知错误';
190
- callbacks.onError(errMsg);
145
+ const noConvErr = errs.find((e) => e.includes('No conversation found with session ID'));
146
+ if (!success && noConvErr) {
147
+ log.warn(`SDK session invalid: ${noConvErr}`);
148
+ callbacks.onSessionInvalid?.();
149
+ callbacks.onError('会话已过期,请发送 /new 开始新会话');
191
150
  return;
192
151
  }
193
152
  const resultText = m.result ?? '';
@@ -200,55 +159,71 @@ export class ClaudeSDKAdapter {
200
159
  numTurns: m.num_turns ?? 0,
201
160
  toolStats,
202
161
  };
203
- if (!result.accumulated && result.result) {
162
+ if (!result.accumulated && result.result)
204
163
  result.accumulated = result.result;
205
- }
206
164
  if (!result.accumulated && !result.result && accumulated) {
165
+ log.debug(`Result event had no text but accumulated=${accumulated.length} chars, using accumulated`);
207
166
  result.accumulated = accumulated;
208
167
  result.result = accumulated;
209
168
  }
169
+ if (!result.accumulated && !result.result) {
170
+ const errMsg = errs[0] ?? '未知错误';
171
+ log.warn(`SDK result empty: subtype=${m.subtype}, errors=${JSON.stringify(errs)}`);
172
+ callbacks.onError(errMsg);
173
+ return;
174
+ }
210
175
  callbacks.onComplete(result);
211
176
  return;
212
177
  }
213
178
  }
214
- // 如果流正常结束但没有收到 result 消息
215
- if (!streamClosed && accumulated) {
216
- log.info('Stream ended without result message, using accumulated text');
179
+ if (!queryClosed) {
217
180
  callbacks.onComplete({
218
181
  success: true,
219
182
  result: accumulated,
220
183
  accumulated,
221
184
  cost: 0,
222
185
  durationMs: 0,
223
- numTurns: 1,
186
+ numTurns: 0,
224
187
  toolStats,
225
188
  });
226
189
  }
227
190
  }
228
191
  finally {
229
- // 从活跃列表中移除流
230
- activeStreams.delete(stream);
192
+ q.close();
193
+ // 从活跃列表中移除
194
+ activeQueries.delete(q);
231
195
  }
232
196
  }
233
197
  catch (err) {
234
198
  if (abortController.signal.aborted) {
235
- log.info('Session run aborted');
199
+ log.info('Query aborted by user');
236
200
  return;
237
201
  }
238
202
  const errorObj = err;
239
203
  const msg = errorObj.message || String(err);
240
- log.error(`Claude SDK V2 error: ${msg}`);
241
- if (errorObj.stack) {
242
- log.error(`Error stack: ${errorObj.stack}`);
204
+ const stack = errorObj.stack || '';
205
+ // 输出详细的错误信息用于调试
206
+ log.error(`Claude SDK error: ${msg}`);
207
+ if (stack) {
208
+ log.error(`Error stack: ${stack}`);
209
+ }
210
+ // 特别处理会话文件不存在的情况
211
+ if (errorObj.code === 'ENOENT' ||
212
+ errorObj.syscall === 'lstat' ||
213
+ msg.includes('ENOENT') ||
214
+ msg.includes('session')) {
215
+ log.warn(`Session file access error, likely session is corrupted or missing`);
216
+ callbacks.onSessionInvalid?.();
217
+ callbacks.onError('会话文件不存在或已损坏,请发送 /new 开始新会话');
218
+ }
219
+ else {
220
+ callbacks.onError(msg);
243
221
  }
244
- callbacks.onError(msg);
245
222
  }
246
223
  };
247
- // 启动会话(不等待)
248
- runSession();
224
+ runQuery();
249
225
  return {
250
226
  abort: () => {
251
- log.info('Aborting session run');
252
227
  abortController.abort();
253
228
  },
254
229
  };
@@ -1,4 +1,5 @@
1
1
  import { getConfiguredAiCommands } from '../config.js';
2
+ import { ClaudeAdapter } from './claude-adapter.js';
2
3
  import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
3
4
  import { CodexAdapter } from './codex-adapter.js';
4
5
  import { CodeBuddyAdapter } from './codebuddy-adapter.js';
@@ -9,8 +10,17 @@ export function initAdapters(config) {
9
10
  adapters.clear();
10
11
  for (const aiCommand of getConfiguredAiCommands(config)) {
11
12
  if (aiCommand === 'claude') {
12
- log.info('Claude Agent SDK adapter enabled');
13
- adapters.set('claude', new ClaudeSDKAdapter());
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
+ }
14
24
  continue;
15
25
  }
16
26
  if (aiCommand === 'codex') {
@@ -28,6 +38,7 @@ export function getAdapter(aiCommand) {
28
38
  return adapters.get(aiCommand);
29
39
  }
30
40
  export function cleanupAdapters() {
41
+ ClaudeAdapter.destroy();
31
42
  ClaudeSDKAdapter.destroy();
32
43
  adapters.clear();
33
44
  }
@@ -0,0 +1,29 @@
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;