@wu529778790/open-im 1.11.1-beta.7 → 1.11.1-beta.9
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/claude-sdk-adapter.d.ts +41 -20
- package/dist/adapters/claude-sdk-adapter.js +205 -386
- package/dist/adapters/registry.js +1 -2
- package/dist/adapters/tool-adapter.interface.d.ts +4 -0
- package/dist/commands/handler.d.ts +6 -0
- package/dist/commands/handler.js +181 -0
- package/dist/constants.js +0 -1
- package/package.json +3 -2
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude SDK Adapter - 使用 Agent SDK
|
|
2
|
+
* Claude SDK Adapter - 使用 Agent SDK query() API 实现多轮对话
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
9
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
10
|
+
import type { SDKSessionInfo, SessionMessage, ModelInfo, SDKControlGetContextUsageResponse, AccountInfo } from '@anthropic-ai/claude-agent-sdk';
|
|
12
11
|
import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
|
|
13
12
|
interface ClaudeSessionMeta {
|
|
14
13
|
sessionId: string;
|
|
@@ -18,29 +17,51 @@ interface ClaudeSessionMeta {
|
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
19
|
* Find the latest CLI session for a work directory using the SDK's listSessions.
|
|
21
|
-
* Falls back to undefined if no sessions found.
|
|
22
20
|
*/
|
|
23
21
|
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
22
|
export declare class ClaudeSDKAdapter implements ToolAdapter {
|
|
29
23
|
readonly toolId = "claude-sdk";
|
|
30
24
|
/**
|
|
31
|
-
*
|
|
25
|
+
* 清理所有活跃的查询
|
|
32
26
|
*/
|
|
33
27
|
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
28
|
/**
|
|
40
29
|
* List sessions for a directory using the SDK's listSessions API.
|
|
41
|
-
* Replaces the custom file-scanning logic in findLatestClaudeSession.
|
|
42
30
|
*/
|
|
43
31
|
static listSessionsForDir(workDir: string, limit?: number): Promise<SDKSessionInfo[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Get session messages for a given session ID.
|
|
34
|
+
*/
|
|
35
|
+
static getSessionMessagesForId(sessionId: string, workDir: string, limit?: number): Promise<SessionMessage[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Delete a session by ID.
|
|
38
|
+
*/
|
|
39
|
+
static deleteSessionById(sessionId: string, workDir?: string): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Rename a session.
|
|
42
|
+
*/
|
|
43
|
+
static renameSessionById(sessionId: string, title: string, workDir?: string): Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Fork a session.
|
|
46
|
+
*/
|
|
47
|
+
static forkSessionById(sessionId: string, workDir?: string): Promise<string | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Create a short-lived query for fetching session info (models, context, etc).
|
|
50
|
+
* The caller must close the returned query when done.
|
|
51
|
+
*/
|
|
52
|
+
static createInfoQuery(workDir: string, model?: string): Promise<ReturnType<typeof query>>;
|
|
53
|
+
/**
|
|
54
|
+
* Get available models for a work directory.
|
|
55
|
+
*/
|
|
56
|
+
static getSupportedModels(workDir: string, model?: string): Promise<ModelInfo[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Get context usage for a work directory.
|
|
59
|
+
*/
|
|
60
|
+
static getContextUsage(workDir: string, model?: string): Promise<SDKControlGetContextUsageResponse | undefined>;
|
|
61
|
+
/**
|
|
62
|
+
* Get account info for a work directory.
|
|
63
|
+
*/
|
|
64
|
+
static getAccountInfo(workDir: string, model?: string): Promise<AccountInfo | undefined>;
|
|
44
65
|
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
45
66
|
}
|
|
46
67
|
export {};
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude SDK Adapter - 使用 Agent SDK
|
|
2
|
+
* Claude SDK Adapter - 使用 Agent SDK query() API 实现多轮对话
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
9
|
+
import { query, listSessions, getSessionMessages, deleteSession, renameSession, forkSession } 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,179 +192,163 @@ function enrichClaudeErrorMessage(message, stderr) {
|
|
|
299
192
|
}
|
|
300
193
|
return message;
|
|
301
194
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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.`);
|
|
195
|
+
export class ClaudeSDKAdapter {
|
|
196
|
+
toolId = 'claude-sdk';
|
|
197
|
+
/**
|
|
198
|
+
* 清理所有活跃的查询
|
|
199
|
+
*/
|
|
200
|
+
static destroy() {
|
|
201
|
+
// query() API 的 Query 对象通过 abortController.abort() 清理
|
|
316
202
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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();
|
|
203
|
+
/**
|
|
204
|
+
* List sessions for a directory using the SDK's listSessions API.
|
|
205
|
+
*/
|
|
206
|
+
static async listSessionsForDir(workDir, limit = 20) {
|
|
330
207
|
try {
|
|
331
|
-
|
|
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 };
|
|
208
|
+
return await listSessions({ dir: workDir, limit });
|
|
399
209
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
log.warn(`Failed to list sessions for ${workDir}: ${err}`);
|
|
212
|
+
return [];
|
|
404
213
|
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
export class ClaudeSDKAdapter {
|
|
408
|
-
toolId = 'claude-sdk';
|
|
214
|
+
}
|
|
409
215
|
/**
|
|
410
|
-
*
|
|
216
|
+
* Get session messages for a given session ID.
|
|
411
217
|
*/
|
|
412
|
-
static
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
if (stream && typeof stream.return === 'function') {
|
|
417
|
-
stream.return();
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
catch {
|
|
421
|
-
/* ignore */
|
|
422
|
-
}
|
|
218
|
+
static async getSessionMessagesForId(sessionId, workDir, limit = 50) {
|
|
219
|
+
try {
|
|
220
|
+
return await getSessionMessages(sessionId, { dir: workDir, limit });
|
|
423
221
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
222
|
+
catch (err) {
|
|
223
|
+
log.warn(`Failed to get session messages for ${sessionId}: ${err}`);
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Delete a session by ID.
|
|
229
|
+
*/
|
|
230
|
+
static async deleteSessionById(sessionId, workDir) {
|
|
231
|
+
try {
|
|
232
|
+
await deleteSession(sessionId, { dir: workDir });
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
log.warn(`Failed to delete session ${sessionId}: ${err}`);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Rename a session.
|
|
242
|
+
*/
|
|
243
|
+
static async renameSessionById(sessionId, title, workDir) {
|
|
244
|
+
try {
|
|
245
|
+
await renameSession(sessionId, title, { dir: workDir });
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
log.warn(`Failed to rename session ${sessionId}: ${err}`);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Fork a session.
|
|
255
|
+
*/
|
|
256
|
+
static async forkSessionById(sessionId, workDir) {
|
|
257
|
+
try {
|
|
258
|
+
const result = await forkSession(sessionId, { dir: workDir });
|
|
259
|
+
return result.sessionId;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
log.warn(`Failed to fork session ${sessionId}: ${err}`);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Create a short-lived query for fetching session info (models, context, etc).
|
|
268
|
+
* The caller must close the returned query when done.
|
|
269
|
+
*/
|
|
270
|
+
static async createInfoQuery(workDir, model) {
|
|
271
|
+
const resolvedModel = model?.trim() || process.env.ANTHROPIC_MODEL?.trim() || 'claude-opus-4-5';
|
|
272
|
+
return query({
|
|
273
|
+
prompt: '',
|
|
274
|
+
options: {
|
|
275
|
+
cwd: workDir,
|
|
276
|
+
model: resolvedModel,
|
|
277
|
+
permissionMode: 'default',
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Get available models for a work directory.
|
|
283
|
+
*/
|
|
284
|
+
static async getSupportedModels(workDir, model) {
|
|
285
|
+
let q;
|
|
286
|
+
try {
|
|
287
|
+
q = await this.createInfoQuery(workDir, model);
|
|
288
|
+
return await q.supportedModels();
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
log.warn(`Failed to get supported models: ${err}`);
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
if (q) {
|
|
296
|
+
try {
|
|
297
|
+
await q.return(undefined);
|
|
298
|
+
}
|
|
299
|
+
catch { /* ignore */ }
|
|
431
300
|
}
|
|
432
301
|
}
|
|
433
|
-
activeSessions.clear();
|
|
434
|
-
sessionLastUsed.clear();
|
|
435
|
-
sessionWorkDirs.clear();
|
|
436
302
|
}
|
|
437
303
|
/**
|
|
438
|
-
*
|
|
439
|
-
* Useful when the caller knows a session is corrupted.
|
|
304
|
+
* Get context usage for a work directory.
|
|
440
305
|
*/
|
|
441
|
-
static
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
306
|
+
static async getContextUsage(workDir, model) {
|
|
307
|
+
let q;
|
|
308
|
+
try {
|
|
309
|
+
q = await this.createInfoQuery(workDir, model);
|
|
310
|
+
return await q.getContextUsage();
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
log.warn(`Failed to get context usage: ${err}`);
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
if (q) {
|
|
318
|
+
try {
|
|
319
|
+
await q.return(undefined);
|
|
320
|
+
}
|
|
321
|
+
catch { /* ignore */ }
|
|
446
322
|
}
|
|
447
|
-
catch { /* ignore */ }
|
|
448
|
-
activeSessions.delete(sessionId);
|
|
449
|
-
sessionLastUsed.delete(sessionId);
|
|
450
|
-
sessionWorkDirs.delete(sessionId);
|
|
451
|
-
log.info(`Explicitly removed session: ${sessionId}`);
|
|
452
323
|
}
|
|
453
324
|
}
|
|
454
325
|
/**
|
|
455
|
-
*
|
|
456
|
-
* Replaces the custom file-scanning logic in findLatestClaudeSession.
|
|
326
|
+
* Get account info for a work directory.
|
|
457
327
|
*/
|
|
458
|
-
static async
|
|
328
|
+
static async getAccountInfo(workDir, model) {
|
|
329
|
+
let q;
|
|
459
330
|
try {
|
|
460
|
-
|
|
331
|
+
q = await this.createInfoQuery(workDir, model);
|
|
332
|
+
return await q.accountInfo();
|
|
461
333
|
}
|
|
462
334
|
catch (err) {
|
|
463
|
-
log.warn(`Failed to
|
|
464
|
-
return
|
|
335
|
+
log.warn(`Failed to get account info: ${err}`);
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
if (q) {
|
|
340
|
+
try {
|
|
341
|
+
await q.return(undefined);
|
|
342
|
+
}
|
|
343
|
+
catch { /* ignore */ }
|
|
344
|
+
}
|
|
465
345
|
}
|
|
466
346
|
}
|
|
467
347
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
468
|
-
|
|
348
|
+
// 刷新 Claude 环境变量(支持 cc switch 后无需重启即可生效)
|
|
349
|
+
refreshClaudeEnvToProcess();
|
|
469
350
|
const abortController = new AbortController();
|
|
470
|
-
let streamClosed = false;
|
|
471
|
-
let actualSessionId;
|
|
472
|
-
let pendingTempId; // 记录临时 ID,用于 abort 时清理
|
|
473
351
|
let runSettled = false;
|
|
474
|
-
let currentStream; // 用于 abort 时立即中断 stream
|
|
475
352
|
let recentStderr = '';
|
|
476
353
|
const permissionMode = options?.skipPermissions
|
|
477
354
|
? 'bypassPermissions'
|
|
@@ -480,79 +357,66 @@ export class ClaudeSDKAdapter {
|
|
|
480
357
|
: options?.permissionMode === 'plan'
|
|
481
358
|
? 'plan'
|
|
482
359
|
: 'default';
|
|
483
|
-
const
|
|
484
|
-
|
|
360
|
+
const resolvedModel = options?.model?.trim() || process.env.ANTHROPIC_MODEL?.trim() || 'claude-opus-4-5';
|
|
361
|
+
const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
|
|
362
|
+
log.info(`[query] run() entry model=${resolvedModel} baseUrl=${baseUrl} sessionId=${sessionId ?? 'new'} workDir=${workDir}`);
|
|
363
|
+
const runQuery = async () => {
|
|
364
|
+
let accumulated = '';
|
|
365
|
+
let accumulatedThinking = '';
|
|
366
|
+
const toolStats = {};
|
|
367
|
+
let actualSessionId;
|
|
368
|
+
let hadSessionInvalid = false;
|
|
485
369
|
try {
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
370
|
+
// 先尝试自动恢复 CLI 的最新 session(如果用户没有指定 sessionId)
|
|
371
|
+
let resumeId = sessionId;
|
|
372
|
+
if (!resumeId) {
|
|
373
|
+
const latest = await findLatestClaudeSession(workDir);
|
|
374
|
+
if (latest) {
|
|
375
|
+
const cliActive = isCliSessionActive(latest.sessionId, latest.filePath);
|
|
376
|
+
if (cliActive) {
|
|
377
|
+
log.info(`CLI is actively using session ${latest.sessionId}, attempting resume anyway`);
|
|
378
|
+
}
|
|
379
|
+
if (validateSessionFile(latest.filePath, latest.sessionId)) {
|
|
380
|
+
resumeId = latest.sessionId;
|
|
381
|
+
log.info(`Auto-resuming latest CLI session: ${latest.sessionId}${cliActive ? ' (CLI active)' : ''}`);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
log.warn(`Session file validation failed for ${latest.sessionId}, skipping`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
500
387
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
388
|
+
const q = query({
|
|
389
|
+
prompt,
|
|
390
|
+
options: {
|
|
391
|
+
cwd: workDir,
|
|
392
|
+
model: resolvedModel,
|
|
393
|
+
permissionMode,
|
|
394
|
+
...(resumeId ? { resume: resumeId } : {}),
|
|
395
|
+
...(options?.fallbackModel ? { fallbackModel: options.fallbackModel } : {}),
|
|
396
|
+
...(options?.disallowedTools?.length ? { disallowedTools: options.disallowedTools } : {}),
|
|
397
|
+
abortController,
|
|
398
|
+
stderr: (data) => {
|
|
399
|
+
recentStderr = appendStderrSnippet(recentStderr, data);
|
|
400
|
+
},
|
|
401
|
+
},
|
|
512
402
|
});
|
|
513
|
-
let accumulated = '';
|
|
514
|
-
let accumulatedThinking = '';
|
|
515
|
-
const toolStats = {};
|
|
516
403
|
try {
|
|
517
|
-
for await (const msg of
|
|
404
|
+
for await (const msg of q) {
|
|
518
405
|
if (abortController.signal.aborted) {
|
|
519
|
-
log.info('
|
|
406
|
+
log.info('Query aborted by user');
|
|
520
407
|
break;
|
|
521
408
|
}
|
|
522
409
|
// 获取实际的 sessionId(从 init 消息中)
|
|
523
410
|
if (isSystemInit(msg)) {
|
|
524
411
|
const initMsg = msg;
|
|
525
|
-
// 记录 session 加载的插件和技能
|
|
526
412
|
const pluginNames = initMsg.plugins?.map(p => p.name).join(', ') ?? 'none';
|
|
527
413
|
const skillCount = initMsg.skills?.length ?? 0;
|
|
528
414
|
const toolCount = initMsg.tools?.length ?? 0;
|
|
529
|
-
log.info(`[
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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);
|
|
415
|
+
log.info(`[query] Init: plugins=[${pluginNames}], skills=${skillCount}, tools=${toolCount}`);
|
|
416
|
+
if (initMsg.session_id) {
|
|
417
|
+
actualSessionId = initMsg.session_id;
|
|
418
|
+
log.info(`[query] Got sessionId: ${actualSessionId}`);
|
|
419
|
+
callbacks.onSessionId?.(actualSessionId);
|
|
556
420
|
}
|
|
557
421
|
continue;
|
|
558
422
|
}
|
|
@@ -584,25 +448,21 @@ export class ClaudeSDKAdapter {
|
|
|
584
448
|
}
|
|
585
449
|
// 处理结果消息
|
|
586
450
|
if (isResult(msg)) {
|
|
587
|
-
streamClosed = true;
|
|
588
451
|
const m = msg;
|
|
589
452
|
const success = m.subtype === 'success';
|
|
590
453
|
const errs = m.errors ?? [];
|
|
591
|
-
|
|
592
|
-
|
|
454
|
+
// 更新 sessionId(如果 init 消息中没有)
|
|
455
|
+
if (m.session_id && !actualSessionId) {
|
|
456
|
+
actualSessionId = m.session_id;
|
|
457
|
+
callbacks.onSessionId?.(actualSessionId);
|
|
458
|
+
}
|
|
459
|
+
log.info(`[query] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
|
|
593
460
|
if (!success) {
|
|
594
461
|
runSettled = true;
|
|
595
462
|
const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
|
|
596
463
|
if (noConvErr) {
|
|
597
|
-
log.warn(`Session ${actualSessionId} not found
|
|
598
|
-
|
|
599
|
-
activeSessions.delete(actualSessionId);
|
|
600
|
-
sessionLastUsed.delete(actualSessionId);
|
|
601
|
-
try {
|
|
602
|
-
session.close();
|
|
603
|
-
}
|
|
604
|
-
catch { /* ignore */ }
|
|
605
|
-
}
|
|
464
|
+
log.warn(`Session ${actualSessionId} not found`);
|
|
465
|
+
hadSessionInvalid = true;
|
|
606
466
|
callbacks.onSessionInvalid?.();
|
|
607
467
|
}
|
|
608
468
|
const errMsg = enrichClaudeErrorMessage(errs[0] || '未知错误', recentStderr);
|
|
@@ -632,7 +492,7 @@ export class ClaudeSDKAdapter {
|
|
|
632
492
|
}
|
|
633
493
|
}
|
|
634
494
|
// 如果流正常结束但没有收到 result 消息
|
|
635
|
-
if (!
|
|
495
|
+
if (!runSettled) {
|
|
636
496
|
if (accumulated) {
|
|
637
497
|
log.info('Stream ended without result message, using accumulated text');
|
|
638
498
|
runSettled = true;
|
|
@@ -647,7 +507,6 @@ export class ClaudeSDKAdapter {
|
|
|
647
507
|
});
|
|
648
508
|
}
|
|
649
509
|
else {
|
|
650
|
-
// 流结束但无 result 也无 accumulated:必须触发回调,否则 Promise 永远挂起
|
|
651
510
|
log.warn('Stream ended with no result and no accumulated text, calling onError to prevent stuck state');
|
|
652
511
|
runSettled = true;
|
|
653
512
|
callbacks.onError(enrichClaudeErrorMessage('AI 响应异常结束(无输出),请重试', recentStderr));
|
|
@@ -655,81 +514,41 @@ export class ClaudeSDKAdapter {
|
|
|
655
514
|
}
|
|
656
515
|
}
|
|
657
516
|
finally {
|
|
658
|
-
//
|
|
659
|
-
activeStreams.delete(stream);
|
|
517
|
+
// Query 的 cleanup 由 SDK 内部管理
|
|
660
518
|
}
|
|
661
519
|
}
|
|
662
520
|
catch (err) {
|
|
663
521
|
if (abortController.signal.aborted) {
|
|
664
|
-
log.info('
|
|
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
|
-
}
|
|
522
|
+
log.info('Query run aborted');
|
|
671
523
|
return;
|
|
672
524
|
}
|
|
673
525
|
runSettled = true;
|
|
674
526
|
const errorObj = err;
|
|
675
527
|
const msg = enrichClaudeErrorMessage(errorObj.message || String(err), recentStderr);
|
|
676
|
-
log.error(`Claude SDK
|
|
528
|
+
log.error(`Claude SDK error: ${msg}`);
|
|
677
529
|
if (errorObj.stack) {
|
|
678
530
|
log.error(`Error stack: ${errorObj.stack}`);
|
|
679
531
|
}
|
|
680
|
-
|
|
681
|
-
|
|
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}`);
|
|
532
|
+
if (isSessionCorruptionError(msg)) {
|
|
533
|
+
log.warn(`Session corruption detected: ${msg}`);
|
|
698
534
|
callbacks.onSessionInvalid?.();
|
|
699
535
|
}
|
|
700
536
|
callbacks.onError(msg);
|
|
701
537
|
}
|
|
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
538
|
};
|
|
713
|
-
//
|
|
714
|
-
|
|
539
|
+
// 启动查询(不等待),catch 兜底防止 unhandledRejection
|
|
540
|
+
runQuery().catch((err) => {
|
|
715
541
|
if (!runSettled) {
|
|
716
542
|
runSettled = true;
|
|
717
543
|
const msg = err instanceof Error ? err.message : String(err);
|
|
718
|
-
log.error(`Unhandled
|
|
544
|
+
log.error(`Unhandled runQuery error: ${msg}`);
|
|
719
545
|
callbacks.onError(msg);
|
|
720
546
|
}
|
|
721
547
|
});
|
|
722
548
|
return {
|
|
723
549
|
abort: () => {
|
|
724
|
-
log.info('Aborting
|
|
550
|
+
log.info('Aborting query');
|
|
725
551
|
abortController.abort();
|
|
726
|
-
// 立即中断 stream,不等下一条消息
|
|
727
|
-
if (currentStream) {
|
|
728
|
-
try {
|
|
729
|
-
currentStream.return?.();
|
|
730
|
-
}
|
|
731
|
-
catch { /* ignore */ }
|
|
732
|
-
}
|
|
733
552
|
},
|
|
734
553
|
};
|
|
735
554
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getConfiguredAiCommands } from '../config.js';
|
|
2
|
-
import { ClaudeSDKAdapter
|
|
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
|
}
|
|
@@ -30,6 +30,10 @@ export interface RunOptions {
|
|
|
30
30
|
hookPort?: number;
|
|
31
31
|
/** Codex 专用:HTTP/HTTPS 代理地址,如 http://127.0.0.1:7890 */
|
|
32
32
|
proxy?: string;
|
|
33
|
+
/** 备用模型,主模型过载时自动切换 */
|
|
34
|
+
fallbackModel?: string;
|
|
35
|
+
/** 禁用的工具列表 */
|
|
36
|
+
disallowedTools?: string[];
|
|
33
37
|
}
|
|
34
38
|
export interface RunHandle {
|
|
35
39
|
abort: () => void;
|
|
@@ -30,6 +30,12 @@ export declare class CommandHandler {
|
|
|
30
30
|
private handlePwd;
|
|
31
31
|
private handleStatus;
|
|
32
32
|
private handleCd;
|
|
33
|
+
private handleHistory;
|
|
34
|
+
private handleDelete;
|
|
35
|
+
private handleRename;
|
|
36
|
+
private handleFork;
|
|
37
|
+
private handleModels;
|
|
38
|
+
private handleContext;
|
|
33
39
|
private getAiVersion;
|
|
34
40
|
}
|
|
35
41
|
/**
|
package/dist/commands/handler.js
CHANGED
|
@@ -81,6 +81,18 @@ export class CommandHandler {
|
|
|
81
81
|
return this.handleSessions(chatId, userId, platform);
|
|
82
82
|
if (t.startsWith('/resume '))
|
|
83
83
|
return this.handleResume(chatId, userId, t.slice(8).trim(), platform);
|
|
84
|
+
if (t.startsWith('/history'))
|
|
85
|
+
return this.handleHistory(chatId, userId, t.slice(8).trim());
|
|
86
|
+
if (t.startsWith('/delete '))
|
|
87
|
+
return this.handleDelete(chatId, userId, t.slice(8).trim());
|
|
88
|
+
if (t.startsWith('/rename '))
|
|
89
|
+
return this.handleRename(chatId, userId, t.slice(8).trim());
|
|
90
|
+
if (t.startsWith('/fork'))
|
|
91
|
+
return this.handleFork(chatId, userId, t.slice(5).trim());
|
|
92
|
+
if (t === '/models')
|
|
93
|
+
return this.handleModels(chatId, userId, platform);
|
|
94
|
+
if (t === '/context')
|
|
95
|
+
return this.handleContext(chatId, userId, platform);
|
|
84
96
|
if (t === '/pwd')
|
|
85
97
|
return this.handlePwd(chatId, userId);
|
|
86
98
|
if (t === '/status')
|
|
@@ -108,6 +120,12 @@ export class CommandHandler {
|
|
|
108
120
|
'/new - 开始新会话(AI 上下文重置)',
|
|
109
121
|
'/sessions - 查看历史会话',
|
|
110
122
|
'/resume [序号] - 恢复历史会话(无参数恢复最近一条)',
|
|
123
|
+
'/history [序号] - 查看会话对话记录',
|
|
124
|
+
'/delete <序号> - 删除历史会话',
|
|
125
|
+
'/rename <标题> - 重命名当前会话',
|
|
126
|
+
'/fork [序号] - 分支会话(创建副本)',
|
|
127
|
+
'/models - 查看可用模型',
|
|
128
|
+
'/context - 查看上下文窗口占用',
|
|
111
129
|
'/status - 显示状态',
|
|
112
130
|
'/cd <路径> - 切换工作目录',
|
|
113
131
|
'/pwd - 当前工作目录',
|
|
@@ -191,6 +209,20 @@ export class CommandHandler {
|
|
|
191
209
|
`工作目录: ${escapePathForMarkdown(workDir)}`,
|
|
192
210
|
`会话: ${sessionId ?? '无'}`,
|
|
193
211
|
];
|
|
212
|
+
// 账号信息(仅 claude)
|
|
213
|
+
if (aiCommand === 'claude') {
|
|
214
|
+
try {
|
|
215
|
+
const account = await ClaudeSDKAdapter.getAccountInfo(workDir);
|
|
216
|
+
if (account) {
|
|
217
|
+
lines.push('', '👤 账号:');
|
|
218
|
+
if (account.email)
|
|
219
|
+
lines.push(`邮箱: ${account.email}`);
|
|
220
|
+
if (account.organization)
|
|
221
|
+
lines.push(`组织: ${account.organization}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch { /* ignore */ }
|
|
225
|
+
}
|
|
194
226
|
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
195
227
|
return true;
|
|
196
228
|
}
|
|
@@ -217,6 +249,155 @@ export class CommandHandler {
|
|
|
217
249
|
}
|
|
218
250
|
return true;
|
|
219
251
|
}
|
|
252
|
+
async handleHistory(chatId, userId, arg) {
|
|
253
|
+
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
254
|
+
const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
|
|
255
|
+
if (sessions.length === 0) {
|
|
256
|
+
await this.replySender().sendTextReply(chatId, '暂无会话记录。');
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
let targetSession = sessions[0]; // 默认当前/最近会话
|
|
260
|
+
if (arg) {
|
|
261
|
+
const index = parseInt(arg, 10);
|
|
262
|
+
if (isNaN(index) || index < 1 || index > sessions.length) {
|
|
263
|
+
await this.replySender().sendTextReply(chatId, `序号无效,共 ${sessions.length} 个会话。`);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
targetSession = sessions[index - 1];
|
|
267
|
+
}
|
|
268
|
+
const messages = await ClaudeSDKAdapter.getSessionMessagesForId(targetSession.sessionId, workDir, 30);
|
|
269
|
+
if (messages.length === 0) {
|
|
270
|
+
await this.replySender().sendTextReply(chatId, '该会话暂无对话记录。');
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
const preview = truncateSummary(targetSession);
|
|
274
|
+
const lines = [`📜 会话记录: ${preview}`, ''];
|
|
275
|
+
for (const msg of messages) {
|
|
276
|
+
if (msg.type === 'system')
|
|
277
|
+
continue;
|
|
278
|
+
const m = msg.message;
|
|
279
|
+
let text = '';
|
|
280
|
+
if (typeof m === 'string') {
|
|
281
|
+
text = m;
|
|
282
|
+
}
|
|
283
|
+
else if (m && typeof m === 'object') {
|
|
284
|
+
const content = m.content;
|
|
285
|
+
if (Array.isArray(content) && content[0]?.text) {
|
|
286
|
+
text = content[0].text;
|
|
287
|
+
}
|
|
288
|
+
else if (typeof content === 'string') {
|
|
289
|
+
text = content;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!text)
|
|
293
|
+
continue;
|
|
294
|
+
const prefix = msg.type === 'user' ? '👤' : '🤖';
|
|
295
|
+
lines.push(`${prefix} ${text.slice(0, 200)}`);
|
|
296
|
+
}
|
|
297
|
+
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
async handleDelete(chatId, userId, arg) {
|
|
301
|
+
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
302
|
+
const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
|
|
303
|
+
const index = parseInt(arg, 10);
|
|
304
|
+
if (isNaN(index) || index < 1 || index > sessions.length) {
|
|
305
|
+
await this.replySender().sendTextReply(chatId, `用法: /delete <序号>\n共 ${sessions.length} 个会话。`);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
const session = sessions[index - 1];
|
|
309
|
+
const preview = truncateSummary(session);
|
|
310
|
+
const ok = await ClaudeSDKAdapter.deleteSessionById(session.sessionId, workDir);
|
|
311
|
+
await this.replySender().sendTextReply(chatId, ok ? `✅ 已删除会话: ${preview}` : `❌ 删除失败`);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
async handleRename(chatId, userId, title) {
|
|
315
|
+
if (!title) {
|
|
316
|
+
await this.replySender().sendTextReply(chatId, '用法: /rename <新标题>');
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
320
|
+
const convId = this.deps.sessionManager.getConvId(userId);
|
|
321
|
+
const aiCommand = 'claude';
|
|
322
|
+
const sessionId = this.deps.sessionManager.getSessionIdForConv(userId, convId, aiCommand);
|
|
323
|
+
if (!sessionId) {
|
|
324
|
+
await this.replySender().sendTextReply(chatId, '当前没有活动会话。');
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
const ok = await ClaudeSDKAdapter.renameSessionById(sessionId, title, workDir);
|
|
328
|
+
await this.replySender().sendTextReply(chatId, ok ? `✅ 会话已重命名为: ${title}` : '❌ 重命名失败');
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
async handleFork(chatId, userId, arg) {
|
|
332
|
+
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
333
|
+
const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
|
|
334
|
+
if (sessions.length === 0) {
|
|
335
|
+
await this.replySender().sendTextReply(chatId, '暂无会话可分支。');
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
let targetSession = sessions[0];
|
|
339
|
+
if (arg) {
|
|
340
|
+
const index = parseInt(arg, 10);
|
|
341
|
+
if (isNaN(index) || index < 1 || index > sessions.length) {
|
|
342
|
+
await this.replySender().sendTextReply(chatId, `序号无效,共 ${sessions.length} 个会话。`);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
targetSession = sessions[index - 1];
|
|
346
|
+
}
|
|
347
|
+
const newSessionId = await ClaudeSDKAdapter.forkSessionById(targetSession.sessionId, workDir);
|
|
348
|
+
if (newSessionId) {
|
|
349
|
+
this.deps.sessionManager.setActiveSessionId(userId, newSessionId);
|
|
350
|
+
const preview = truncateSummary(targetSession);
|
|
351
|
+
await this.replySender().sendTextReply(chatId, `✅ 已分支会话: ${preview}\n新会话 ID: ${newSessionId.slice(0, 8)}...\n继续发消息即可。`);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
await this.replySender().sendTextReply(chatId, '❌ 分支失败');
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
async handleModels(chatId, userId, _platform) {
|
|
359
|
+
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
360
|
+
const models = await ClaudeSDKAdapter.getSupportedModels(workDir);
|
|
361
|
+
if (models.length === 0) {
|
|
362
|
+
await this.replySender().sendTextReply(chatId, '暂无可用模型信息。');
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
const lines = ['🤖 可用模型:', ''];
|
|
366
|
+
for (const model of models) {
|
|
367
|
+
const name = model.displayName || model.value;
|
|
368
|
+
const desc = model.description ? ` - ${model.description.slice(0, 60)}` : '';
|
|
369
|
+
lines.push(`• ${name}${desc}`);
|
|
370
|
+
}
|
|
371
|
+
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
async handleContext(chatId, userId, _platform) {
|
|
375
|
+
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
376
|
+
const usage = await ClaudeSDKAdapter.getContextUsage(workDir);
|
|
377
|
+
if (!usage) {
|
|
378
|
+
await this.replySender().sendTextReply(chatId, '暂无上下文信息。');
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
const lines = ['📏 上下文窗口占用:', ''];
|
|
382
|
+
if (usage.model)
|
|
383
|
+
lines.push(`模型: ${usage.model}`);
|
|
384
|
+
if (usage.totalTokens)
|
|
385
|
+
lines.push(`已用: ${usage.totalTokens.toLocaleString()} tokens`);
|
|
386
|
+
if (usage.maxTokens)
|
|
387
|
+
lines.push(`上限: ${usage.maxTokens.toLocaleString()} tokens`);
|
|
388
|
+
if (usage.percentage != null)
|
|
389
|
+
lines.push(`使用率: ${usage.percentage}%`);
|
|
390
|
+
if (usage.categories?.length) {
|
|
391
|
+
lines.push('', '分类:');
|
|
392
|
+
for (const cat of usage.categories) {
|
|
393
|
+
if (cat.tokens > 0) {
|
|
394
|
+
lines.push(` ${cat.name}: ${cat.tokens.toLocaleString()}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
220
401
|
getAiVersion(aiCommand) {
|
|
221
402
|
if (aiCommand === 'claude') {
|
|
222
403
|
return Promise.resolve('SDK Mode');
|
package/dist/constants.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wu529778790/open-im",
|
|
3
|
-
"version": "1.11.1-beta.
|
|
3
|
+
"version": "1.11.1-beta.9",
|
|
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.
|
|
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",
|