@wu529778790/open-im 1.11.1-beta.2 → 1.11.1-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,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
  }
@@ -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;
@@ -10,5 +10,5 @@
10
10
  import type { Config } from '../config.js';
11
11
  import type { ClawBotState } from './types.js';
12
12
  export declare function getChannelState(): ClawBotState;
13
- export declare function initClawbot(config: Config, eventHandler: (chatId: string, msgId: string, content: string) => Promise<void>, onStateChange?: (state: ClawBotState) => void): Promise<void>;
13
+ export declare function initClawbot(config: Config, eventHandler: (chatId: string, msgId: string, content: string, imagePaths?: string[]) => Promise<void>, onStateChange?: (state: ClawBotState) => void): Promise<void>;
14
14
  export declare function stopClawbot(): void;
@@ -12,6 +12,7 @@ import { createLogger } from '../logger.js';
12
12
  import { jitteredDelay, isFatalReconnectError, SLOW_PROBE_MS } from '../shared/reconnect.js';
13
13
  import { cacheContextToken } from './message-sender.js';
14
14
  import { setClawbotContextToken, clearClawbotContextToken } from '../shared/active-chats.js';
15
+ import { decryptAes256CbcMedia, saveBufferMedia, createMediaTargetPath } from '../shared/media-storage.js';
15
16
  import { CLAWBOT_POLL_INTERVAL_MS } from '../constants.js';
16
17
  const log = createLogger('ClawBot');
17
18
  const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
@@ -110,6 +111,8 @@ function startPolling() {
110
111
  }
111
112
  // Process messages
112
113
  const messages = res.messages ?? [];
114
+ // Step 1: Extract valid USER messages and cache context tokens
115
+ const userMessages = [];
113
116
  for (const msg of messages) {
114
117
  if (signal.aborted)
115
118
  break;
@@ -120,7 +123,6 @@ function startPolling() {
120
123
  continue;
121
124
  const chatId = msg.from_user_id ?? '';
122
125
  const msgId = String(msg.message_id ?? msg.seq ?? '');
123
- const content = extracted;
124
126
  if (!chatId) {
125
127
  log.warn('ClawBot message missing from_user_id, skipping');
126
128
  continue;
@@ -130,10 +132,34 @@ function startPolling() {
130
132
  cacheContextToken(chatId, msg.context_token);
131
133
  setClawbotContextToken(msg.context_token);
132
134
  }
133
- log.info(`ClawBot message: chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
135
+ // Extract and download images from message
136
+ const imagePaths = await extractImages(msg);
137
+ userMessages.push({ chatId, msgId, content: extracted, imagePaths: imagePaths.length > 0 ? imagePaths : undefined });
138
+ }
139
+ // Step 2: Aggregate consecutive messages from the same user
140
+ // ClawBot splits image+text into separate messages; combine them
141
+ const aggregated = [];
142
+ for (const m of userMessages) {
143
+ const last = aggregated[aggregated.length - 1];
144
+ if (last && last.chatId === m.chatId) {
145
+ // Same user — merge content and image paths
146
+ last.content = `${last.content}\n${m.content}`;
147
+ if (m.imagePaths?.length) {
148
+ last.imagePaths = [...(last.imagePaths ?? []), ...m.imagePaths];
149
+ }
150
+ }
151
+ else {
152
+ aggregated.push({ chatId: m.chatId, msgId: m.msgId, content: m.content, imagePaths: m.imagePaths });
153
+ }
154
+ }
155
+ // Step 3: Dispatch aggregated messages
156
+ for (const m of aggregated) {
157
+ if (signal.aborted)
158
+ break;
159
+ log.info(`ClawBot message: chatId=${m.chatId}, msgId=${m.msgId}, content="${m.content.substring(0, 100)}", images=${m.imagePaths?.length ?? 0}`);
134
160
  if (messageHandler) {
135
161
  try {
136
- await messageHandler(chatId, msgId, content);
162
+ await messageHandler(m.chatId, m.msgId, m.content, m.imagePaths);
137
163
  }
138
164
  catch (err) {
139
165
  log.error('Error in ClawBot message handler:', err);
@@ -212,6 +238,48 @@ function stopWatchdog() {
212
238
  watchdogTimer = null;
213
239
  }
214
240
  }
241
+ /**
242
+ * Extract and download images from a ClawBot message's item_list.
243
+ * Images are encrypted with AES and hosted on CDN.
244
+ */
245
+ async function extractImages(msg) {
246
+ if (!msg.item_list?.length)
247
+ return [];
248
+ const paths = [];
249
+ for (const item of msg.item_list) {
250
+ if (item.type !== 2 /* MessageItemType.IMAGE */)
251
+ continue;
252
+ const media = item.image_item?.media;
253
+ if (!media?.cdn_url)
254
+ continue;
255
+ try {
256
+ // Download from CDN
257
+ const response = await fetch(media.cdn_url, { signal: AbortSignal.timeout(30_000) });
258
+ if (!response.ok) {
259
+ log.warn(`Image download failed: HTTP ${response.status}`);
260
+ continue;
261
+ }
262
+ const buffer = Buffer.from(await response.arrayBuffer());
263
+ // Decrypt if AES key provided
264
+ let decrypted;
265
+ if (media.aes_key) {
266
+ decrypted = decryptAes256CbcMedia(buffer, media.aes_key);
267
+ }
268
+ else {
269
+ decrypted = buffer;
270
+ }
271
+ // Save to disk
272
+ const targetPath = createMediaTargetPath('.jpg', `clawbot-${Date.now()}`);
273
+ await saveBufferMedia(decrypted, targetPath);
274
+ paths.push(targetPath);
275
+ log.info(`ClawBot image saved: ${targetPath}`);
276
+ }
277
+ catch (err) {
278
+ log.warn('Failed to process ClawBot image:', err);
279
+ }
280
+ }
281
+ return paths;
282
+ }
215
283
  /**
216
284
  * Extract text content from an iLink message's item_list.
217
285
  * Returns the first text item found, or a placeholder for media types.
@@ -7,6 +7,6 @@ export interface ClawBotEventHandlerHandle {
7
7
  stop: () => void;
8
8
  runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
9
9
  getRunningTaskCount: () => number;
10
- handleEvent: (chatId: string, msgId: string, content: string) => Promise<void>;
10
+ handleEvent: (chatId: string, msgId: string, content: string, imagePaths?: string[]) => Promise<void>;
11
11
  }
12
12
  export declare function setupClawbotHandlers(config: Config, sessionManager: SessionManager): ClawBotEventHandlerHandle;
@@ -33,8 +33,8 @@ export function setupClawbotHandlers(config, sessionManager) {
33
33
  return () => { };
34
34
  },
35
35
  };
36
- async function handleEvent(chatId, msgId, content) {
37
- log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
36
+ async function handleEvent(chatId, msgId, content, imagePaths) {
37
+ log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}", images=${imagePaths?.length ?? 0}`);
38
38
  const userId = chatId;
39
39
  const text = content.trim();
40
40
  const msgIdSender = {
@@ -61,11 +61,18 @@ export function setupClawbotHandlers(config, sessionManager) {
61
61
  },
62
62
  }),
63
63
  });
64
+ // Prepend image paths to prompt so Claude can read them with its Read tool
65
+ let enrichedText = text;
66
+ if (imagePaths?.length) {
67
+ const imgRefs = imagePaths.map(p => `[用户发送了图片: ${p}]`).join('\n');
68
+ enrichedText = `${imgRefs}\n${text}`;
69
+ }
64
70
  await handleTextFlow({
65
71
  platform: 'clawbot',
66
72
  userId,
67
73
  chatId,
68
- text,
74
+ text: enrichedText,
75
+ msgId,
69
76
  ctx,
70
77
  handleAIRequest,
71
78
  sendTextReply: (c, t) => sendTextReply(c, t),
@@ -24,12 +24,19 @@ export declare class CommandHandler {
24
24
  /** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
25
25
  senderOverride?: MessageSender): Promise<boolean>;
26
26
  private handleHelp;
27
+ private handlePlugins;
27
28
  private handleSessions;
28
29
  private handleResume;
29
30
  private handleNew;
30
31
  private handlePwd;
31
32
  private handleStatus;
32
33
  private handleCd;
34
+ private handleHistory;
35
+ private handleDelete;
36
+ private handleRename;
37
+ private handleFork;
38
+ private handleModels;
39
+ private handleContext;
33
40
  private getAiVersion;
34
41
  }
35
42
  /**
@@ -2,11 +2,35 @@ import { resolvePlatformAiCommand } from '../config.js';
2
2
  import { escapePathForMarkdown } from '../shared/utils.js';
3
3
  import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
4
4
  import { createLogger } from '../logger.js';
5
+ import { ClaudeSDKAdapter } from '../adapters/claude-sdk-adapter.js';
5
6
  const log = createLogger('Commands');
7
+ function formatRelativeTime(ts) {
8
+ const sec = Math.floor((Date.now() - ts) / 1000);
9
+ if (sec < 60)
10
+ return '刚刚';
11
+ const min = Math.floor(sec / 60);
12
+ if (min < 60)
13
+ return `${min}分钟前`;
14
+ const hr = Math.floor(min / 60);
15
+ if (hr < 24)
16
+ return `${hr}小时前`;
17
+ const day = Math.floor(hr / 24);
18
+ if (day === 1)
19
+ return '昨天';
20
+ if (day < 30)
21
+ return `${day}天前`;
22
+ return new Date(ts).toLocaleDateString('zh-CN');
23
+ }
24
+ function truncateSummary(session, maxLen = 30) {
25
+ const text = session.customTitle || session.summary || session.firstPrompt || '新会话';
26
+ const firstLine = text.split('\n')[0].trim();
27
+ return firstLine.length > maxLen ? firstLine.slice(0, maxLen) + '...' : firstLine;
28
+ }
6
29
  import { AsyncLocalStorage } from 'node:async_hooks';
7
30
  import { execFile } from 'node:child_process';
8
- import { readdirSync } from 'node:fs';
31
+ import { readdirSync, existsSync, readFileSync } from 'node:fs';
9
32
  import { dirname, join } from 'node:path';
33
+ import { homedir } from 'node:os';
10
34
  /**
11
35
  * Telegram 群聊等场景下命令常为 `/new@BotName`,需与 `/new` 等价。
12
36
  * 仅去掉「第一个」命令词上的 `@suffix`,保留 `/resume 1` 等参数。
@@ -52,12 +76,26 @@ export class CommandHandler {
52
76
  }
53
77
  if (t === '/help')
54
78
  return this.handleHelp(chatId);
79
+ if (t === '/plugins')
80
+ return this.handlePlugins(chatId);
55
81
  if (t === '/new')
56
82
  return this.handleNew(chatId, userId);
57
83
  if (t === '/sessions' || t === '/resume')
58
84
  return this.handleSessions(chatId, userId, platform);
59
85
  if (t.startsWith('/resume '))
60
86
  return this.handleResume(chatId, userId, t.slice(8).trim(), platform);
87
+ if (t.startsWith('/history'))
88
+ return this.handleHistory(chatId, userId, t.slice(8).trim());
89
+ if (t.startsWith('/delete '))
90
+ return this.handleDelete(chatId, userId, t.slice(8).trim());
91
+ if (t.startsWith('/rename '))
92
+ return this.handleRename(chatId, userId, t.slice(8).trim());
93
+ if (t.startsWith('/fork'))
94
+ return this.handleFork(chatId, userId, t.slice(5).trim());
95
+ if (t === '/models')
96
+ return this.handleModels(chatId, userId, platform);
97
+ if (t === '/context')
98
+ return this.handleContext(chatId, userId, platform);
61
99
  if (t === '/pwd')
62
100
  return this.handlePwd(chatId, userId);
63
101
  if (t === '/status')
@@ -84,7 +122,14 @@ export class CommandHandler {
84
122
  '/help - 显示帮助',
85
123
  '/new - 开始新会话(AI 上下文重置)',
86
124
  '/sessions - 查看历史会话',
87
- '/resume <序号> - 恢复历史会话',
125
+ '/resume [序号] - 恢复历史会话(无参数恢复最近一条)',
126
+ '/history [序号] - 查看会话对话记录',
127
+ '/delete <序号> - 删除历史会话',
128
+ '/rename <标题> - 重命名当前会话',
129
+ '/fork [序号] - 分支会话(创建副本)',
130
+ '/models - 查看可用模型',
131
+ '/plugins - 查看已安装插件',
132
+ '/context - 查看上下文窗口占用',
88
133
  '/status - 显示状态',
89
134
  '/cd <路径> - 切换工作目录',
90
135
  '/pwd - 当前工作目录',
@@ -92,45 +137,81 @@ export class CommandHandler {
92
137
  await this.replySender().sendTextReply(chatId, help);
93
138
  return true;
94
139
  }
140
+ async handlePlugins(chatId) {
141
+ try {
142
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
143
+ if (!existsSync(settingsPath)) {
144
+ await this.replySender().sendTextReply(chatId, '📦 未找到 ~/.claude/settings.json\n💡 先在 Claude Code 终端运行一次会自动创建');
145
+ return true;
146
+ }
147
+ const raw = JSON.parse(readFileSync(settingsPath, 'utf-8'));
148
+ const plugins = (raw.enabledPlugins ?? {});
149
+ const entries = Object.entries(plugins);
150
+ if (entries.length === 0) {
151
+ await this.replySender().sendTextReply(chatId, '📦 暂无已安装插件\n💡 在 Claude Code 终端用 /install 安装插件');
152
+ return true;
153
+ }
154
+ const lines = ['📦 已安装插件:', ''];
155
+ for (const [name, enabled] of entries) {
156
+ lines.push(enabled ? ` ✅ ${name}` : ` ❌ ${name}`);
157
+ }
158
+ lines.push('');
159
+ lines.push('💡 在 ~/.claude/settings.json 管理,或在 Claude Code 终端用 /install');
160
+ await this.replySender().sendTextReply(chatId, lines.join('\n'));
161
+ }
162
+ catch (e) {
163
+ log.warn('Failed to read plugins:', e);
164
+ await this.replySender().sendTextReply(chatId, '❌ 读取插件列表失败');
165
+ }
166
+ return true;
167
+ }
95
168
  async handleSessions(chatId, userId, _platform) {
96
- const history = this.deps.sessionManager.listConvHistory(userId);
97
- const active = this.deps.sessionManager.getActiveConvInfo(userId);
98
- if (history.length === 0 && !active) {
169
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
170
+ const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
171
+ if (sessions.length === 0) {
99
172
  await this.replySender().sendTextReply(chatId, '📋 暂无会话记录。');
100
173
  return true;
101
174
  }
102
175
  const lines = ['📋 会话列表:', ''];
103
- history.forEach((entry, i) => {
104
- lines.push(`${i + 1}. ${entry.convId} · ${entry.totalTurns}轮`);
176
+ sessions.forEach((session, i) => {
177
+ const preview = truncateSummary(session);
178
+ const time = session.lastModified ? ` · ${formatRelativeTime(session.lastModified)}` : '';
179
+ lines.push(`${i + 1}. ${preview}${time}`);
105
180
  });
106
- if (active) {
107
- const num = history.length + 1;
108
- lines.push(`▸ ${num}. ${active.convId} · ${active.totalTurns}轮(当前)`);
109
- }
110
- lines.push('', '使用 /resume <序号> 恢复历史会话');
181
+ lines.push('', '使用 /resume <序号> 恢复,或 /resume 恢复最近一条');
111
182
  await this.replySender().sendTextReply(chatId, lines.join('\n'));
112
183
  return true;
113
184
  }
114
185
  async handleResume(chatId, userId, arg, _platform) {
186
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
187
+ const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
188
+ // /resume (no arg) — resume the most recent session
189
+ if (!arg) {
190
+ if (sessions.length === 0) {
191
+ await this.replySender().sendTextReply(chatId, '没有可恢复的历史会话。');
192
+ return true;
193
+ }
194
+ const session = sessions[0];
195
+ this.deps.requestQueue.cancelUser(userId);
196
+ this.deps.sessionManager.setActiveSessionId(userId, session.sessionId);
197
+ const preview = truncateSummary(session);
198
+ await this.replySender().sendTextReply(chatId, `✅ 已恢复最近会话: ${preview}\n继续发消息即可。`);
199
+ return true;
200
+ }
115
201
  const index = parseInt(arg, 10);
116
202
  if (isNaN(index) || index < 1) {
117
- await this.replySender().sendTextReply(chatId, '用法: /resume <序号>\n\n使用 /sessions 查看会话列表。');
203
+ await this.replySender().sendTextReply(chatId, '用法: /resume [序号]\n\n不带序号则恢复最近一条会话。');
118
204
  return true;
119
205
  }
120
- const history = this.deps.sessionManager.listConvHistory(userId);
121
- if (index > history.length) {
122
- await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${history.length} 个历史会话。`);
206
+ if (index > sessions.length) {
207
+ await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${sessions.length} 个历史会话。`);
123
208
  return true;
124
209
  }
125
- const entry = history[index - 1];
210
+ const session = sessions[index - 1];
126
211
  this.deps.requestQueue.cancelUser(userId);
127
- const ok = this.deps.sessionManager.resumeConv(userId, entry.convId);
128
- if (ok) {
129
- await this.replySender().sendTextReply(chatId, `✅ 已恢复会话 ${index} (${entry.convId}),共 ${entry.totalTurns}轮对话。\n继续发消息即可。`);
130
- }
131
- else {
132
- await this.replySender().sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
133
- }
212
+ this.deps.sessionManager.setActiveSessionId(userId, session.sessionId);
213
+ const preview = truncateSummary(session);
214
+ await this.replySender().sendTextReply(chatId, `✅ 已恢复会话: ${preview}\n继续发消息即可。`);
134
215
  return true;
135
216
  }
136
217
  async handleNew(chatId, userId) {
@@ -160,6 +241,20 @@ export class CommandHandler {
160
241
  `工作目录: ${escapePathForMarkdown(workDir)}`,
161
242
  `会话: ${sessionId ?? '无'}`,
162
243
  ];
244
+ // 账号信息(仅 claude)
245
+ if (aiCommand === 'claude') {
246
+ try {
247
+ const account = await ClaudeSDKAdapter.getAccountInfo(workDir);
248
+ if (account) {
249
+ lines.push('', '👤 账号:');
250
+ if (account.email)
251
+ lines.push(`邮箱: ${account.email}`);
252
+ if (account.organization)
253
+ lines.push(`组织: ${account.organization}`);
254
+ }
255
+ }
256
+ catch { /* ignore */ }
257
+ }
163
258
  await this.replySender().sendTextReply(chatId, lines.join('\n'));
164
259
  return true;
165
260
  }
@@ -178,15 +273,163 @@ export class CommandHandler {
178
273
  }
179
274
  try {
180
275
  this.deps.requestQueue.cancelUser(userId);
181
- const resolved = await this.deps.sessionManager.setWorkDir(userId, dir);
182
- await this.replySender().sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(resolved)}\n\n` +
183
- `🔄 AI 会话已重置,下一条消息将使用全新上下文。`);
276
+ const result = await this.deps.sessionManager.setWorkDir(userId, dir);
277
+ await this.replySender().sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n下一条消息将自动查找该目录的最近会话。`);
184
278
  }
185
279
  catch (err) {
186
280
  await this.replySender().sendTextReply(chatId, err instanceof Error ? err.message : String(err));
187
281
  }
188
282
  return true;
189
283
  }
284
+ async handleHistory(chatId, userId, arg) {
285
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
286
+ const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
287
+ if (sessions.length === 0) {
288
+ await this.replySender().sendTextReply(chatId, '暂无会话记录。');
289
+ return true;
290
+ }
291
+ let targetSession = sessions[0]; // 默认当前/最近会话
292
+ if (arg) {
293
+ const index = parseInt(arg, 10);
294
+ if (isNaN(index) || index < 1 || index > sessions.length) {
295
+ await this.replySender().sendTextReply(chatId, `序号无效,共 ${sessions.length} 个会话。`);
296
+ return true;
297
+ }
298
+ targetSession = sessions[index - 1];
299
+ }
300
+ const messages = await ClaudeSDKAdapter.getSessionMessagesForId(targetSession.sessionId, workDir, 30);
301
+ if (messages.length === 0) {
302
+ await this.replySender().sendTextReply(chatId, '该会话暂无对话记录。');
303
+ return true;
304
+ }
305
+ const preview = truncateSummary(targetSession);
306
+ const lines = [`📜 会话记录: ${preview}`, ''];
307
+ for (const msg of messages) {
308
+ if (msg.type === 'system')
309
+ continue;
310
+ const m = msg.message;
311
+ let text = '';
312
+ if (typeof m === 'string') {
313
+ text = m;
314
+ }
315
+ else if (m && typeof m === 'object') {
316
+ const content = m.content;
317
+ if (Array.isArray(content) && content[0]?.text) {
318
+ text = content[0].text;
319
+ }
320
+ else if (typeof content === 'string') {
321
+ text = content;
322
+ }
323
+ }
324
+ if (!text)
325
+ continue;
326
+ const prefix = msg.type === 'user' ? '👤' : '🤖';
327
+ lines.push(`${prefix} ${text.slice(0, 200)}`);
328
+ }
329
+ await this.replySender().sendTextReply(chatId, lines.join('\n'));
330
+ return true;
331
+ }
332
+ async handleDelete(chatId, userId, arg) {
333
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
334
+ const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
335
+ const index = parseInt(arg, 10);
336
+ if (isNaN(index) || index < 1 || index > sessions.length) {
337
+ await this.replySender().sendTextReply(chatId, `用法: /delete <序号>\n共 ${sessions.length} 个会话。`);
338
+ return true;
339
+ }
340
+ const session = sessions[index - 1];
341
+ const preview = truncateSummary(session);
342
+ const ok = await ClaudeSDKAdapter.deleteSessionById(session.sessionId, workDir);
343
+ await this.replySender().sendTextReply(chatId, ok ? `✅ 已删除会话: ${preview}` : `❌ 删除失败`);
344
+ return true;
345
+ }
346
+ async handleRename(chatId, userId, title) {
347
+ if (!title) {
348
+ await this.replySender().sendTextReply(chatId, '用法: /rename <新标题>');
349
+ return true;
350
+ }
351
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
352
+ const convId = this.deps.sessionManager.getConvId(userId);
353
+ const aiCommand = 'claude';
354
+ const sessionId = this.deps.sessionManager.getSessionIdForConv(userId, convId, aiCommand);
355
+ if (!sessionId) {
356
+ await this.replySender().sendTextReply(chatId, '当前没有活动会话。');
357
+ return true;
358
+ }
359
+ const ok = await ClaudeSDKAdapter.renameSessionById(sessionId, title, workDir);
360
+ await this.replySender().sendTextReply(chatId, ok ? `✅ 会话已重命名为: ${title}` : '❌ 重命名失败');
361
+ return true;
362
+ }
363
+ async handleFork(chatId, userId, arg) {
364
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
365
+ const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
366
+ if (sessions.length === 0) {
367
+ await this.replySender().sendTextReply(chatId, '暂无会话可分支。');
368
+ return true;
369
+ }
370
+ let targetSession = sessions[0];
371
+ if (arg) {
372
+ const index = parseInt(arg, 10);
373
+ if (isNaN(index) || index < 1 || index > sessions.length) {
374
+ await this.replySender().sendTextReply(chatId, `序号无效,共 ${sessions.length} 个会话。`);
375
+ return true;
376
+ }
377
+ targetSession = sessions[index - 1];
378
+ }
379
+ const newSessionId = await ClaudeSDKAdapter.forkSessionById(targetSession.sessionId, workDir);
380
+ if (newSessionId) {
381
+ this.deps.sessionManager.setActiveSessionId(userId, newSessionId);
382
+ const preview = truncateSummary(targetSession);
383
+ await this.replySender().sendTextReply(chatId, `✅ 已分支会话: ${preview}\n新会话 ID: ${newSessionId.slice(0, 8)}...\n继续发消息即可。`);
384
+ }
385
+ else {
386
+ await this.replySender().sendTextReply(chatId, '❌ 分支失败');
387
+ }
388
+ return true;
389
+ }
390
+ async handleModels(chatId, userId, _platform) {
391
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
392
+ const models = await ClaudeSDKAdapter.getSupportedModels(workDir);
393
+ if (models.length === 0) {
394
+ await this.replySender().sendTextReply(chatId, '暂无可用模型信息。');
395
+ return true;
396
+ }
397
+ const lines = ['🤖 可用模型:', ''];
398
+ for (const model of models) {
399
+ const name = model.displayName || model.value;
400
+ const desc = model.description ? ` - ${model.description.slice(0, 60)}` : '';
401
+ lines.push(`• ${name}${desc}`);
402
+ }
403
+ await this.replySender().sendTextReply(chatId, lines.join('\n'));
404
+ return true;
405
+ }
406
+ async handleContext(chatId, userId, _platform) {
407
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
408
+ const usage = await ClaudeSDKAdapter.getContextUsage(workDir);
409
+ if (!usage) {
410
+ await this.replySender().sendTextReply(chatId, '暂无上下文信息。');
411
+ return true;
412
+ }
413
+ const lines = ['📏 上下文窗口占用:', ''];
414
+ if (usage.model)
415
+ lines.push(`模型: ${usage.model}`);
416
+ if (usage.totalTokens)
417
+ lines.push(`已用: ${usage.totalTokens.toLocaleString()} tokens`);
418
+ if (usage.maxTokens)
419
+ lines.push(`上限: ${usage.maxTokens.toLocaleString()} tokens`);
420
+ if (usage.percentage != null)
421
+ lines.push(`使用率: ${usage.percentage}%`);
422
+ if (usage.categories?.length) {
423
+ lines.push('', '分类:');
424
+ for (const cat of usage.categories) {
425
+ if (cat.tokens > 0) {
426
+ lines.push(` ${cat.name}: ${cat.tokens.toLocaleString()}`);
427
+ }
428
+ }
429
+ }
430
+ await this.replySender().sendTextReply(chatId, lines.join('\n'));
431
+ return true;
432
+ }
190
433
  getAiVersion(aiCommand) {
191
434
  if (aiCommand === 'claude') {
192
435
  return Promise.resolve('SDK Mode');