@wu529778790/open-im 1.11.1-beta.7 → 1.11.1-beta.8

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,12 +1,10 @@
1
1
  /**
2
- * Claude SDK Adapter - 使用 Agent SDK V2 Session API 实现真正的多轮对话
2
+ * Claude SDK Adapter - 使用 Agent SDK query() API 实现多轮对话
3
3
  *
4
- * V2 API 优势:
5
- * 1. 进程内执行 - fork/exec 开销
6
- * 2. 持久会话 - SDKSession 对象保持会话状态,支持真正的多轮对话
7
- * 3. 流式输出 - 支持实时增量更新
8
- *
9
- * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
4
+ * query() API:
5
+ * - 返回 AsyncGenerator<SDKMessage>,直接迭代即可
6
+ * - 支持 resume/cwd/model options,无需 process.chdir hack
7
+ * - SDK 内部管理 session 生命周期,无需手动维护 session pool
10
8
  */
11
9
  import type { SDKSessionInfo } from '@anthropic-ai/claude-agent-sdk';
12
10
  import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
@@ -18,27 +16,16 @@ interface ClaudeSessionMeta {
18
16
  }
19
17
  /**
20
18
  * Find the latest CLI session for a work directory using the SDK's listSessions.
21
- * Falls back to undefined if no sessions found.
22
19
  */
23
20
  export declare function findLatestClaudeSession(workDir: string): Promise<ClaudeSessionMeta | undefined>;
24
- /**
25
- * 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
26
- */
27
- export declare function configureClaudeSdkSessionIdle(ttlMinutes: number): void;
28
21
  export declare class ClaudeSDKAdapter implements ToolAdapter {
29
22
  readonly toolId = "claude-sdk";
30
23
  /**
31
- * 清理所有活跃的 SDK 会话和流
24
+ * 清理所有活跃的查询
32
25
  */
33
26
  static destroy(): void;
34
- /**
35
- * Remove a specific session from the in-memory cache and close it.
36
- * Useful when the caller knows a session is corrupted.
37
- */
38
- static removeSession(sessionId: string): void;
39
27
  /**
40
28
  * List sessions for a directory using the SDK's listSessions API.
41
- * Replaces the custom file-scanning logic in findLatestClaudeSession.
42
29
  */
43
30
  static listSessionsForDir(workDir: string, limit?: number): Promise<SDKSessionInfo[]>;
44
31
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
@@ -1,14 +1,12 @@
1
1
  /**
2
- * Claude SDK Adapter - 使用 Agent SDK V2 Session API 实现真正的多轮对话
2
+ * Claude SDK Adapter - 使用 Agent SDK query() API 实现多轮对话
3
3
  *
4
- * V2 API 优势:
5
- * 1. 进程内执行 - fork/exec 开销
6
- * 2. 持久会话 - SDKSession 对象保持会话状态,支持真正的多轮对话
7
- * 3. 流式输出 - 支持实时增量更新
8
- *
9
- * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
4
+ * query() API:
5
+ * - 返回 AsyncGenerator<SDKMessage>,直接迭代即可
6
+ * - 支持 resume/cwd/model options,无需 process.chdir hack
7
+ * - SDK 内部管理 session 生命周期,无需手动维护 session pool
10
8
  */
11
- import { unstable_v2_createSession, unstable_v2_resumeSession, listSessions } from '@anthropic-ai/claude-agent-sdk';
9
+ import { query, listSessions } from '@anthropic-ai/claude-agent-sdk';
12
10
  import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } from 'fs';
13
11
  import { execSync } from 'child_process';
14
12
  import { homedir } from 'os';
@@ -53,8 +51,6 @@ loadUserPluginSettings();
53
51
  * ~/.claude/projects/-Users-mac-github-open-im/
54
52
  */
55
53
  function workDirToProjectPath(workDir) {
56
- // Claude Code 将路径中的 / 替换为 -,leading - 保留
57
- // /Users/mac/github/open-im → -Users-mac-github-open-im
58
54
  return workDir.replace(/\//g, '-');
59
55
  }
60
56
  /**
@@ -71,14 +67,9 @@ function workDirToProjectPath(workDir) {
71
67
  */
72
68
  function isCliSessionActive(sessionId, sessionFilePath) {
73
69
  try {
74
- // macOS/Linux: 用 ps 搜索包含该 sessionId 的 claude 进程
75
- // 排除 open-im 自己的 SDK 子进程(路径含 claude-agent-sdk),只匹配用户交互式 CLI
76
- // -F 固定字符串匹配,避免正则意外;-- 防止 sessionId 被误认为 flag
77
70
  const result = execSync(`ps -axo pid,command 2>/dev/null | grep -v grep | grep "claude" | grep -v "claude-agent-sdk" | grep -F -- "${sessionId}" || true`, { encoding: 'utf-8', timeout: 3000 });
78
71
  if (result.trim().length === 0)
79
72
  return false;
80
- // 进程存在,但可能只是 idle 在终端等输入。检查文件 mtime:
81
- // 如果 session 文件超过 30 秒未修改,说明 CLI 没在活跃处理。
82
73
  if (sessionFilePath) {
83
74
  try {
84
75
  const stat = statSync(sessionFilePath);
@@ -100,7 +91,6 @@ function isCliSessionActive(sessionId, sessionFilePath) {
100
91
  }
101
92
  /**
102
93
  * Find the latest CLI session for a work directory using the SDK's listSessions.
103
- * Falls back to undefined if no sessions found.
104
94
  */
105
95
  export async function findLatestClaudeSession(workDir) {
106
96
  try {
@@ -110,7 +100,6 @@ export async function findLatestClaudeSession(workDir) {
110
100
  return undefined;
111
101
  }
112
102
  const latest = sessions[0];
113
- // filePath is needed for isCliSessionActive check — derive from standard path
114
103
  const projectDir = join(homedir(), '.claude', 'projects', workDirToProjectPath(workDir));
115
104
  const filePath = join(projectDir, `${latest.sessionId}.jsonl`);
116
105
  log.info(`Found latest session via SDK: ${latest.sessionId} (lastModified: ${new Date(latest.lastModified).toISOString()})`);
@@ -156,104 +145,6 @@ function validateSessionFile(filePath, expectedSessionId) {
156
145
  }
157
146
  }
158
147
  }
159
- // 存储所有活跃的 SDKSession 对象,key 为 sessionId
160
- // 使用 Map 而不是 Set,因为我们需要通过 sessionId 获取 session
161
- const activeSessions = new Map();
162
- // 记录每个 session 创建/恢复时的 workDir,防止跨 workDir 复用已固定 cwd 的子进程
163
- const sessionWorkDirs = new Map();
164
- // 存储正在进行的流式迭代器,用于中断
165
- const activeStreams = new Set();
166
- // 空闲会话清理:跟踪最后使用时间,定期清除超时会话
167
- const sessionLastUsed = new Map();
168
- // 跟踪正在执行任务的 session ID,防止空闲清理误杀运行中的长任务
169
- const runningSessions = new Set();
170
- let sessionIdleTtlMs = 30 * 60 * 1000; // 默认 30 分钟未使用则清理
171
- let sessionIdleCleanupDisabled = false;
172
- const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 每 5 分钟检查一次
173
- const MAX_ACTIVE_SESSIONS = 100;
174
- const MAX_CAPTURED_STDERR_CHARS = 4000;
175
- const MAX_EXPOSED_STDERR_CHARS = 500;
176
- let sessionSeq = 0;
177
- /**
178
- * 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
179
- */
180
- export function configureClaudeSdkSessionIdle(ttlMinutes) {
181
- if (ttlMinutes <= 0) {
182
- sessionIdleCleanupDisabled = true;
183
- log.info('Claude SDK: idle session cleanup disabled (sessionIdleTtlMinutes=0)');
184
- }
185
- else {
186
- sessionIdleCleanupDisabled = false;
187
- sessionIdleTtlMs = ttlMinutes * 60 * 1000;
188
- }
189
- }
190
- const cleanupInterval = setInterval(() => {
191
- if (sessionIdleCleanupDisabled)
192
- return;
193
- const now = Date.now();
194
- for (const [id, lastUsed] of sessionLastUsed) {
195
- if (runningSessions.has(id))
196
- continue; // 跳过正在运行任务的 session
197
- if (now - lastUsed > sessionIdleTtlMs) {
198
- const session = activeSessions.get(id);
199
- if (session) {
200
- try {
201
- session.close();
202
- }
203
- catch { /* ignore */ }
204
- activeSessions.delete(id);
205
- }
206
- sessionLastUsed.delete(id);
207
- sessionWorkDirs.delete(id);
208
- log.info(`Cleaned up idle session (unused ${Math.round((now - lastUsed) / 60000)}min): ${id}`);
209
- }
210
- }
211
- }, CLEANUP_INTERVAL_MS);
212
- cleanupInterval.unref(); // 不阻止进程退出
213
- /**
214
- * 串行化进程级 process.chdir() —— 同一时刻仅一个 chdir 生效。
215
- *
216
- * SDK V2 的 createSession/resumeSession 不接受 cwd 参数;且 send()/stream()
217
- * 会以「调用时的 process.cwd()」派生 Claude Code 子进程。必须用互斥锁串行化
218
- * 「整个 turn 的 cwd 切换」,否则并发多用户会让工具跑错目录。
219
- *
220
- * **TODO:** SDK 支持 cwd 选项后移除此锁。upstream:
221
- * https://github.com/anthropics/claude-agent-sdk/issues
222
- */
223
- let chdirMutex = Promise.resolve();
224
- function withChdirMutex(fn) {
225
- const previous = chdirMutex;
226
- let release;
227
- chdirMutex = new Promise((r) => { release = r; });
228
- return previous.then(async () => {
229
- try {
230
- return await fn();
231
- }
232
- finally {
233
- release();
234
- }
235
- });
236
- }
237
- /**
238
- * 在持有全局 chdir 互斥锁期间,把进程 cwd 切到 workDir 执行 fn,结束后恢复。
239
- * 用于包裹 session.send()+stream(),确保子进程在正确 workDir 派生。
240
- */
241
- function runWithWorkDir(workDir, fn) {
242
- return withChdirMutex(async () => {
243
- const originalCwd = process.cwd();
244
- if (workDir && workDir !== originalCwd) {
245
- process.chdir(workDir);
246
- }
247
- try {
248
- return await fn();
249
- }
250
- finally {
251
- if (workDir && workDir !== originalCwd) {
252
- process.chdir(originalCwd);
253
- }
254
- }
255
- });
256
- }
257
148
  function isStreamEvent(msg) {
258
149
  return msg.type === 'stream_event';
259
150
  }
@@ -270,6 +161,8 @@ function isResult(msg) {
270
161
  function isSessionCorruptionError(msg) {
271
162
  return /session\s*(not found|expired|corrupt)|no\s*conversation\s*found/i.test(msg);
272
163
  }
164
+ const MAX_CAPTURED_STDERR_CHARS = 4000;
165
+ const MAX_EXPOSED_STDERR_CHARS = 500;
273
166
  function appendStderrSnippet(previous, chunk) {
274
167
  const next = previous + chunk;
275
168
  if (next.length <= MAX_CAPTURED_STDERR_CHARS)
@@ -299,161 +192,17 @@ function enrichClaudeErrorMessage(message, stderr) {
299
192
  }
300
193
  return message;
301
194
  }
302
- /**
303
- * 获取或创建 SDKSession
304
- * @param sessionId 已有的 sessionId,如果为 undefined 则创建新会话
305
- * @param workDir 工作目录
306
- * @param model 模型名称
307
- * @param permissionMode 权限模式
308
- * @returns SDKSession 对象和实际的 sessionId
309
- */
310
- async function getOrCreateSession(sessionId, workDir, model, permissionMode, onStderr) {
311
- // 刷新 Claude 环境变量(支持 cc switch 后无需重启即可生效)
312
- refreshClaudeEnvToProcess();
313
- const resolvedModel = model?.trim() || process.env.ANTHROPIC_MODEL?.trim() || 'claude-opus-4-5';
314
- if (activeSessions.size >= MAX_ACTIVE_SESSIONS) {
315
- throw new Error(`Session pool is full (${MAX_ACTIVE_SESSIONS}). Cannot create new session.`);
316
- }
317
- const sessionOptions = {
318
- model: resolvedModel,
319
- permissionMode,
320
- stderr: onStderr,
321
- };
322
- const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
323
- log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl} workDir=${workDir}`);
324
- // NOTE: process.chdir() 是进程级全局副作用,在并发服务器中不理想。
325
- // 但 SDK 的 createSession/resumeSession 不接受 cwd 参数,且这些调用是同步的,
326
- // 所以 mutex + try/finally 已是最优方案。如果 SDK 未来支持 cwd 选项,应移除 chdir。
327
- return withChdirMutex(async () => {
328
- let session;
329
- const originalCwd = process.cwd();
330
- try {
331
- if (workDir && workDir !== originalCwd) {
332
- process.chdir(workDir);
333
- }
334
- if (sessionId) {
335
- // 优先复用内存中已有的 SDKSession,避免每次都启动新进程。
336
- // 仅当 workDir 与创建时一致才复用:否则子进程 cwd 已固定在旧目录,
337
- // 需走 resume 重新派生(在当前 workDir 启动新子进程)。
338
- const existing = activeSessions.get(sessionId);
339
- if (existing && sessionWorkDirs.get(sessionId) === workDir) {
340
- log.info(`Reusing existing in-memory session: ${sessionId}`);
341
- sessionLastUsed.set(sessionId, Date.now());
342
- return { session: existing, sessionId };
343
- }
344
- // 内存中没有(或 workDir 变了),尝试通过 resume 恢复(会启动新 CLI 进程)
345
- try {
346
- log.info(`Attempting to resume session: ${sessionId}`);
347
- session = unstable_v2_resumeSession(sessionId, sessionOptions);
348
- activeSessions.set(sessionId, session);
349
- sessionWorkDirs.set(sessionId, workDir);
350
- sessionLastUsed.set(sessionId, Date.now());
351
- log.info(`Successfully resumed session: ${sessionId}`);
352
- return { session, sessionId };
353
- }
354
- catch (err) {
355
- log.warn(`Failed to resume session ${sessionId}, creating new one: ${err}`);
356
- // 恢复失败,创建新会话
357
- }
358
- }
359
- // 没有指定 sessionId 时,尝试自动恢复 Claude Code CLI 的最新 session
360
- // 实现手机/电脑无缝切换:同目录下默认共享同一个对话
361
- if (!sessionId) {
362
- const latest = await findLatestClaudeSession(workDir);
363
- if (latest) {
364
- // 检测 CLI 是否正在使用该 session(用于日志,不阻止 resume)
365
- const cliActive = isCliSessionActive(latest.sessionId, latest.filePath);
366
- if (cliActive) {
367
- log.info(`CLI is actively using session ${latest.sessionId}, attempting resume anyway (SDK handles concurrency)`);
368
- }
369
- // 验证文件内容一致性
370
- if (validateSessionFile(latest.filePath, latest.sessionId)) {
371
- try {
372
- log.info(`Auto-resuming latest CLI session: ${latest.sessionId}${cliActive ? ' (CLI active)' : ''}`);
373
- session = unstable_v2_resumeSession(latest.sessionId, sessionOptions);
374
- activeSessions.set(latest.sessionId, session);
375
- sessionWorkDirs.set(latest.sessionId, workDir);
376
- sessionLastUsed.set(latest.sessionId, Date.now());
377
- log.info(`Successfully auto-resumed CLI session: ${latest.sessionId}`);
378
- return { session, sessionId: latest.sessionId };
379
- }
380
- catch (err) {
381
- log.warn(`Failed to auto-resume CLI session ${latest.sessionId}, creating new one: ${err}`);
382
- }
383
- }
384
- else {
385
- log.warn(`Session file validation failed for ${latest.sessionId}, skipping`);
386
- }
387
- }
388
- }
389
- // 创建新会话
390
- session = unstable_v2_createSession(sessionOptions);
391
- // 新会话的 sessionId 需要从第一个消息中获取
392
- // 暂时返回 undefined,稍后在 init 消息中获取
393
- const tempId = `pending-${++sessionSeq}`;
394
- activeSessions.set(tempId, session);
395
- sessionWorkDirs.set(tempId, workDir);
396
- sessionLastUsed.set(tempId, Date.now());
397
- log.info(`Created new session (tempId: ${tempId})`);
398
- return { session, sessionId: tempId, wasReused: false };
399
- }
400
- finally {
401
- if (workDir && workDir !== originalCwd) {
402
- process.chdir(originalCwd);
403
- }
404
- }
405
- });
406
- }
407
195
  export class ClaudeSDKAdapter {
408
196
  toolId = 'claude-sdk';
409
197
  /**
410
- * 清理所有活跃的 SDK 会话和流
198
+ * 清理所有活跃的查询
411
199
  */
412
200
  static destroy() {
413
- clearInterval(cleanupInterval);
414
- for (const stream of activeStreams) {
415
- try {
416
- if (stream && typeof stream.return === 'function') {
417
- stream.return();
418
- }
419
- }
420
- catch {
421
- /* ignore */
422
- }
423
- }
424
- activeStreams.clear();
425
- for (const session of activeSessions.values()) {
426
- try {
427
- session.close();
428
- }
429
- catch {
430
- /* ignore */
431
- }
432
- }
433
- activeSessions.clear();
434
- sessionLastUsed.clear();
435
- sessionWorkDirs.clear();
436
- }
437
- /**
438
- * Remove a specific session from the in-memory cache and close it.
439
- * Useful when the caller knows a session is corrupted.
440
- */
441
- static removeSession(sessionId) {
442
- const session = activeSessions.get(sessionId);
443
- if (session) {
444
- try {
445
- session.close();
446
- }
447
- catch { /* ignore */ }
448
- activeSessions.delete(sessionId);
449
- sessionLastUsed.delete(sessionId);
450
- sessionWorkDirs.delete(sessionId);
451
- log.info(`Explicitly removed session: ${sessionId}`);
452
- }
201
+ // query() API 的 Query 对象通过 abortController.abort() 清理
202
+ // 不再需要手动管理 session pool
453
203
  }
454
204
  /**
455
205
  * List sessions for a directory using the SDK's listSessions API.
456
- * Replaces the custom file-scanning logic in findLatestClaudeSession.
457
206
  */
458
207
  static async listSessionsForDir(workDir, limit = 20) {
459
208
  try {
@@ -465,13 +214,10 @@ export class ClaudeSDKAdapter {
465
214
  }
466
215
  }
467
216
  run(prompt, sessionId, workDir, callbacks, options) {
468
- log.info(`[V2] run() entry model=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
217
+ // 刷新 Claude 环境变量(支持 cc switch 后无需重启即可生效)
218
+ refreshClaudeEnvToProcess();
469
219
  const abortController = new AbortController();
470
- let streamClosed = false;
471
- let actualSessionId;
472
- let pendingTempId; // 记录临时 ID,用于 abort 时清理
473
220
  let runSettled = false;
474
- let currentStream; // 用于 abort 时立即中断 stream
475
221
  let recentStderr = '';
476
222
  const permissionMode = options?.skipPermissions
477
223
  ? 'bypassPermissions'
@@ -480,79 +226,64 @@ export class ClaudeSDKAdapter {
480
226
  : options?.permissionMode === 'plan'
481
227
  ? 'plan'
482
228
  : 'default';
483
- const runSession = async () => {
484
- let trackedRunningId; // 用于 finally 中清理 runningSessions
229
+ const resolvedModel = options?.model?.trim() || process.env.ANTHROPIC_MODEL?.trim() || 'claude-opus-4-5';
230
+ const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
231
+ log.info(`[query] run() entry model=${resolvedModel} baseUrl=${baseUrl} sessionId=${sessionId ?? 'new'} workDir=${workDir}`);
232
+ const runQuery = async () => {
233
+ let accumulated = '';
234
+ let accumulatedThinking = '';
235
+ const toolStats = {};
236
+ let actualSessionId;
237
+ let hadSessionInvalid = false;
485
238
  try {
486
- // 检查环境变量
487
- const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
488
- const hasAuthToken = !!process.env.ANTHROPIC_AUTH_TOKEN;
489
- if (!hasApiKey && !hasAuthToken) {
490
- log.warn('Claude SDK: No API credentials found in environment variables');
491
- }
492
- log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
493
- log.info(`[V2] model param=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
494
- // 获取或创建会话
495
- const { session, sessionId: returnedId } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode, (data) => {
496
- recentStderr = appendStderrSnippet(recentStderr, data);
497
- });
498
- if (returnedId.startsWith('pending-')) {
499
- pendingTempId = returnedId;
239
+ // 先尝试自动恢复 CLI 的最新 session(如果用户没有指定 sessionId)
240
+ let resumeId = sessionId;
241
+ if (!resumeId) {
242
+ const latest = await findLatestClaudeSession(workDir);
243
+ if (latest) {
244
+ const cliActive = isCliSessionActive(latest.sessionId, latest.filePath);
245
+ if (cliActive) {
246
+ log.info(`CLI is actively using session ${latest.sessionId}, attempting resume anyway`);
247
+ }
248
+ if (validateSessionFile(latest.filePath, latest.sessionId)) {
249
+ resumeId = latest.sessionId;
250
+ log.info(`Auto-resuming latest CLI session: ${latest.sessionId}${cliActive ? ' (CLI active)' : ''}`);
251
+ }
252
+ else {
253
+ log.warn(`Session file validation failed for ${latest.sessionId}, skipping`);
254
+ }
255
+ }
500
256
  }
501
- runningSessions.add(returnedId);
502
- trackedRunningId = returnedId;
503
- // 在持有 chdir 锁期间完成 send() + stream() 获取:SDK V2 会以当前
504
- // process.cwd() 派生 Claude Code 子进程,必须保证此刻 cwd 为 workDir
505
- // 锁串行化后,并发多用户的子进程不会在错误的目录派生。
506
- const stream = await runWithWorkDir(workDir, async () => {
507
- await session.send(prompt);
508
- const s = session.stream();
509
- currentStream = s;
510
- activeStreams.add(s);
511
- return s;
257
+ const q = query({
258
+ prompt,
259
+ options: {
260
+ cwd: workDir,
261
+ model: resolvedModel,
262
+ permissionMode,
263
+ ...(resumeId ? { resume: resumeId } : {}),
264
+ abortController,
265
+ stderr: (data) => {
266
+ recentStderr = appendStderrSnippet(recentStderr, data);
267
+ },
268
+ },
512
269
  });
513
- let accumulated = '';
514
- let accumulatedThinking = '';
515
- const toolStats = {};
516
270
  try {
517
- for await (const msg of stream) {
271
+ for await (const msg of q) {
518
272
  if (abortController.signal.aborted) {
519
- log.info('Stream aborted by user');
273
+ log.info('Query aborted by user');
520
274
  break;
521
275
  }
522
276
  // 获取实际的 sessionId(从 init 消息中)
523
277
  if (isSystemInit(msg)) {
524
278
  const initMsg = msg;
525
- // 记录 session 加载的插件和技能
526
279
  const pluginNames = initMsg.plugins?.map(p => p.name).join(', ') ?? 'none';
527
280
  const skillCount = initMsg.skills?.length ?? 0;
528
281
  const toolCount = initMsg.tools?.length ?? 0;
529
- log.info(`[V2] Init: plugins=[${pluginNames}], skills=${skillCount}, tools=${toolCount}`);
530
- const newSessionId = initMsg.session_id;
531
- if (newSessionId && newSessionId !== actualSessionId) {
532
- // 更新 sessionId 映射
533
- // 清理 pending 临时 ID(actualSessionId 尚未赋值时用 pendingTempId)
534
- const idToClean = actualSessionId ?? pendingTempId;
535
- const inheritedWorkDir = idToClean ? sessionWorkDirs.get(idToClean) : undefined;
536
- if (idToClean?.startsWith('pending-')) {
537
- activeSessions.delete(idToClean);
538
- }
539
- activeSessions.set(newSessionId, session);
540
- if (inheritedWorkDir !== undefined) {
541
- sessionWorkDirs.set(newSessionId, inheritedWorkDir);
542
- }
543
- sessionLastUsed.set(newSessionId, Date.now());
544
- if (idToClean) {
545
- sessionLastUsed.delete(idToClean);
546
- sessionWorkDirs.delete(idToClean);
547
- }
548
- // 更新 runningSessions:移除旧 ID,添加新 ID
549
- if (idToClean)
550
- runningSessions.delete(idToClean);
551
- runningSessions.add(newSessionId);
552
- trackedRunningId = newSessionId;
553
- actualSessionId = newSessionId;
554
- log.info(`[V2] Got actual sessionId: ${newSessionId}`);
555
- callbacks.onSessionId?.(newSessionId);
282
+ log.info(`[query] Init: plugins=[${pluginNames}], skills=${skillCount}, tools=${toolCount}`);
283
+ if (initMsg.session_id) {
284
+ actualSessionId = initMsg.session_id;
285
+ log.info(`[query] Got sessionId: ${actualSessionId}`);
286
+ callbacks.onSessionId?.(actualSessionId);
556
287
  }
557
288
  continue;
558
289
  }
@@ -584,25 +315,21 @@ export class ClaudeSDKAdapter {
584
315
  }
585
316
  // 处理结果消息
586
317
  if (isResult(msg)) {
587
- streamClosed = true;
588
318
  const m = msg;
589
319
  const success = m.subtype === 'success';
590
320
  const errs = m.errors ?? [];
591
- log.info(`[V2] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
592
- // 检查会话错误
321
+ // 更新 sessionId(如果 init 消息中没有)
322
+ if (m.session_id && !actualSessionId) {
323
+ actualSessionId = m.session_id;
324
+ callbacks.onSessionId?.(actualSessionId);
325
+ }
326
+ log.info(`[query] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
593
327
  if (!success) {
594
328
  runSettled = true;
595
329
  const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
596
330
  if (noConvErr) {
597
- log.warn(`Session ${actualSessionId} not found, removing from active sessions`);
598
- if (actualSessionId) {
599
- activeSessions.delete(actualSessionId);
600
- sessionLastUsed.delete(actualSessionId);
601
- try {
602
- session.close();
603
- }
604
- catch { /* ignore */ }
605
- }
331
+ log.warn(`Session ${actualSessionId} not found`);
332
+ hadSessionInvalid = true;
606
333
  callbacks.onSessionInvalid?.();
607
334
  }
608
335
  const errMsg = enrichClaudeErrorMessage(errs[0] || '未知错误', recentStderr);
@@ -632,7 +359,7 @@ export class ClaudeSDKAdapter {
632
359
  }
633
360
  }
634
361
  // 如果流正常结束但没有收到 result 消息
635
- if (!streamClosed) {
362
+ if (!runSettled) {
636
363
  if (accumulated) {
637
364
  log.info('Stream ended without result message, using accumulated text');
638
365
  runSettled = true;
@@ -647,7 +374,6 @@ export class ClaudeSDKAdapter {
647
374
  });
648
375
  }
649
376
  else {
650
- // 流结束但无 result 也无 accumulated:必须触发回调,否则 Promise 永远挂起
651
377
  log.warn('Stream ended with no result and no accumulated text, calling onError to prevent stuck state');
652
378
  runSettled = true;
653
379
  callbacks.onError(enrichClaudeErrorMessage('AI 响应异常结束(无输出),请重试', recentStderr));
@@ -655,81 +381,41 @@ export class ClaudeSDKAdapter {
655
381
  }
656
382
  }
657
383
  finally {
658
- // 从活跃列表中移除流
659
- activeStreams.delete(stream);
384
+ // Query 的 cleanup 由 SDK 内部管理
660
385
  }
661
386
  }
662
387
  catch (err) {
663
388
  if (abortController.signal.aborted) {
664
- log.info('Session run aborted');
665
- // 清理 pending tempId(abort 可能在 init 消息之前发生)
666
- const idToClean = actualSessionId ?? pendingTempId;
667
- if (idToClean?.startsWith('pending-')) {
668
- activeSessions.delete(idToClean);
669
- log.info(`Cleaned up pending session: ${idToClean}`);
670
- }
389
+ log.info('Query run aborted');
671
390
  return;
672
391
  }
673
392
  runSettled = true;
674
393
  const errorObj = err;
675
394
  const msg = enrichClaudeErrorMessage(errorObj.message || String(err), recentStderr);
676
- log.error(`Claude SDK V2 error: ${msg}`);
395
+ log.error(`Claude SDK error: ${msg}`);
677
396
  if (errorObj.stack) {
678
397
  log.error(`Error stack: ${errorObj.stack}`);
679
398
  }
680
- // 清理 pending tempId(session 在获取真实 ID 前就失败了)
681
- const errIdToClean = actualSessionId ?? pendingTempId;
682
- if (errIdToClean?.startsWith('pending-')) {
683
- activeSessions.delete(errIdToClean);
684
- log.warn(`Cleaned up pending session after error: ${errIdToClean}`);
685
- }
686
- // If error suggests a corrupted session, remove it from cache to prevent reuse
687
- if (actualSessionId && isSessionCorruptionError(msg)) {
688
- const corrupted = activeSessions.get(actualSessionId);
689
- activeSessions.delete(actualSessionId);
690
- sessionLastUsed.delete(actualSessionId);
691
- if (corrupted) {
692
- try {
693
- corrupted.close();
694
- }
695
- catch { /* ignore */ }
696
- }
697
- log.warn(`Removed corrupted session ${actualSessionId} after error: ${msg}`);
399
+ if (isSessionCorruptionError(msg)) {
400
+ log.warn(`Session corruption detected: ${msg}`);
698
401
  callbacks.onSessionInvalid?.();
699
402
  }
700
403
  callbacks.onError(msg);
701
404
  }
702
- finally {
703
- // 无论成功、失败还是 abort,都从运行中集合移除
704
- if (trackedRunningId) {
705
- runningSessions.delete(trackedRunningId);
706
- }
707
- // 也清理 actualSessionId(可能在 init 后更新了)
708
- if (actualSessionId && actualSessionId !== trackedRunningId) {
709
- runningSessions.delete(actualSessionId);
710
- }
711
- }
712
405
  };
713
- // 启动会话(不等待),catch 兜底防止 unhandledRejection 导致用户请求挂起
714
- runSession().catch((err) => {
406
+ // 启动查询(不等待),catch 兜底防止 unhandledRejection
407
+ runQuery().catch((err) => {
715
408
  if (!runSettled) {
716
409
  runSettled = true;
717
410
  const msg = err instanceof Error ? err.message : String(err);
718
- log.error(`Unhandled runSession error: ${msg}`);
411
+ log.error(`Unhandled runQuery error: ${msg}`);
719
412
  callbacks.onError(msg);
720
413
  }
721
414
  });
722
415
  return {
723
416
  abort: () => {
724
- log.info('Aborting session run');
417
+ log.info('Aborting query');
725
418
  abortController.abort();
726
- // 立即中断 stream,不等下一条消息
727
- if (currentStream) {
728
- try {
729
- currentStream.return?.();
730
- }
731
- catch { /* ignore */ }
732
- }
733
419
  },
734
420
  };
735
421
  }
@@ -1,5 +1,5 @@
1
1
  import { getConfiguredAiCommands } from '../config.js';
2
- import { ClaudeSDKAdapter, configureClaudeSdkSessionIdle } from './claude-sdk-adapter.js';
2
+ import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
3
3
  import { CodexAdapter } from './codex-adapter.js';
4
4
  import { CodeBuddyAdapter } from './codebuddy-adapter.js';
5
5
  import { OpenCodeAdapter } from './opencode-adapter.js';
@@ -12,7 +12,6 @@ export function initAdapters(config) {
12
12
  for (const aiCommand of getConfiguredAiCommands(config)) {
13
13
  if (aiCommand === 'claude') {
14
14
  log.info('Claude Agent SDK adapter enabled');
15
- configureClaudeSdkSessionIdle(config.claudeSessionIdleTtlMinutes);
16
15
  adapters.set('claude', new ClaudeSDKAdapter());
17
16
  continue;
18
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.1-beta.7",
3
+ "version": "1.11.1-beta.8",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,7 +51,8 @@
51
51
  "url": "https://github.com/wu529778790/open-im/issues"
52
52
  },
53
53
  "dependencies": {
54
- "@anthropic-ai/claude-agent-sdk": "^0.2.92",
54
+ "@anthropic-ai/claude-agent-sdk": "^0.3.179",
55
+ "@anthropic-ai/sdk": "^0.104.2",
55
56
  "@aws-sdk/client-s3": "^3.1035.0",
56
57
  "@larksuiteoapi/node-sdk": "^1.59.0",
57
58
  "centrifuge": "^5.5.3",