@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.
- package/README.md +76 -59
- package/README.zh-CN.md +75 -58
- package/dist/adapters/claude-sdk-adapter.d.ts +45 -19
- package/dist/adapters/claude-sdk-adapter.js +240 -414
- package/dist/adapters/registry.js +1 -2
- package/dist/adapters/tool-adapter.interface.d.ts +4 -0
- package/dist/clawbot/client.d.ts +1 -1
- package/dist/clawbot/client.js +71 -3
- package/dist/clawbot/event-handler.d.ts +1 -1
- package/dist/clawbot/event-handler.js +10 -3
- package/dist/commands/handler.d.ts +7 -0
- package/dist/commands/handler.js +270 -27
- package/dist/config-web-page-i18n.d.ts +96 -4
- package/dist/config-web-page-i18n.js +98 -4
- package/dist/config-web.js +15 -0
- package/dist/constants.js +0 -1
- package/dist/index.js +13 -4
- package/dist/logger.d.ts +5 -0
- package/dist/logger.js +21 -0
- package/dist/platform/handle-text-flow.d.ts +2 -0
- package/dist/platform/handle-text-flow.js +35 -2
- package/dist/session/session-manager.d.ts +9 -12
- package/dist/session/session-manager.js +31 -79
- package/dist/shared/ai-task.js +1 -0
- package/dist/shared/message-wal.d.ts +31 -0
- package/dist/shared/message-wal.js +91 -0
- package/package.json +4 -3
- package/web/dist/assets/index-B1c1xlbx.css +1 -0
- package/web/dist/assets/index-Dng7B2mE.js +57 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-7fl61tV1.css +0 -1
- package/web/dist/assets/index-BaLTMeeF.js +0 -57
|
@@ -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;
|
package/dist/clawbot/client.d.ts
CHANGED
|
@@ -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;
|
package/dist/clawbot/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/dist/commands/handler.js
CHANGED
|
@@ -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
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
203
|
+
await this.replySender().sendTextReply(chatId, '用法: /resume [序号]\n\n不带序号则恢复最近一条会话。');
|
|
118
204
|
return true;
|
|
119
205
|
}
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
210
|
+
const session = sessions[index - 1];
|
|
126
211
|
this.deps.requestQueue.cancelUser(userId);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
182
|
-
await this.replySender().sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(
|
|
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');
|