@wu529778790/open-im 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,6 +5,7 @@ import type { ThreadContext } from '../shared/types.js';
|
|
|
5
5
|
export type { ThreadContext };
|
|
6
6
|
export interface MessageSender {
|
|
7
7
|
sendTextReply(chatId: string, text: string, threadCtx?: ThreadContext): Promise<void>;
|
|
8
|
+
sendDirectorySelection?(chatId: string, currentDir: string, userId: string): Promise<void>;
|
|
8
9
|
}
|
|
9
10
|
export interface CommandHandlerDeps {
|
|
10
11
|
config: Config;
|
|
@@ -27,3 +28,24 @@ export declare class CommandHandler {
|
|
|
27
28
|
private handleDeny;
|
|
28
29
|
private getClaudeVersion;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* 列出目录并返回目录信息
|
|
33
|
+
*/
|
|
34
|
+
export declare function listDirectories(basePath: string): {
|
|
35
|
+
name: string;
|
|
36
|
+
fullPath: string;
|
|
37
|
+
isParent: boolean;
|
|
38
|
+
}[];
|
|
39
|
+
/**
|
|
40
|
+
* 生成目录选择的按钮布局
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildDirectoryKeyboard(directories: {
|
|
43
|
+
name: string;
|
|
44
|
+
fullPath: string;
|
|
45
|
+
isParent: boolean;
|
|
46
|
+
}[], userId: string): {
|
|
47
|
+
inline_keyboard: Array<Array<{
|
|
48
|
+
text: string;
|
|
49
|
+
callback_data: string;
|
|
50
|
+
}>>;
|
|
51
|
+
};
|
package/dist/commands/handler.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolveLatestPermission, getPendingCount } from '../hook/permission-server.js';
|
|
2
2
|
import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
|
+
import { readdirSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
4
6
|
export class CommandHandler {
|
|
5
7
|
deps;
|
|
6
8
|
constructor(deps) {
|
|
@@ -81,8 +83,15 @@ export class CommandHandler {
|
|
|
81
83
|
return true;
|
|
82
84
|
}
|
|
83
85
|
async handleCd(chatId, userId, dir) {
|
|
86
|
+
// 如果 dir 为空,显示目录选择界面
|
|
84
87
|
if (!dir) {
|
|
85
|
-
|
|
88
|
+
const currentDir = this.deps.sessionManager.getWorkDir(userId);
|
|
89
|
+
if (this.deps.sender.sendDirectorySelection) {
|
|
90
|
+
await this.deps.sender.sendDirectorySelection(chatId, currentDir, userId);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await this.deps.sender.sendTextReply(chatId, `当前目录: ${currentDir}\n使用 /cd <路径> 切换`);
|
|
94
|
+
}
|
|
86
95
|
return true;
|
|
87
96
|
}
|
|
88
97
|
try {
|
|
@@ -125,3 +134,54 @@ export class CommandHandler {
|
|
|
125
134
|
});
|
|
126
135
|
}
|
|
127
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* 列出目录并返回目录信息
|
|
139
|
+
*/
|
|
140
|
+
export function listDirectories(basePath) {
|
|
141
|
+
const dirs = [];
|
|
142
|
+
try {
|
|
143
|
+
// 添加返回上级目录选项(如果不是根目录)
|
|
144
|
+
const parent = dirname(basePath);
|
|
145
|
+
if (parent !== basePath) {
|
|
146
|
+
dirs.push({ name: '🔙 返回上级', fullPath: parent, isParent: true });
|
|
147
|
+
}
|
|
148
|
+
// 读取子目录
|
|
149
|
+
const entries = readdirSync(basePath, { withFileTypes: true });
|
|
150
|
+
const subDirs = entries
|
|
151
|
+
.filter((entry) => entry.isDirectory())
|
|
152
|
+
.filter((entry) => !entry.name.startsWith('.')) // 过滤隐藏目录
|
|
153
|
+
.map((entry) => ({
|
|
154
|
+
name: entry.name,
|
|
155
|
+
fullPath: join(basePath, entry.name),
|
|
156
|
+
isParent: false,
|
|
157
|
+
}))
|
|
158
|
+
.sort((a, b) => a.name.localeCompare(b.name)); // 按名称排序
|
|
159
|
+
dirs.push(...subDirs);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// 忽略错误
|
|
163
|
+
}
|
|
164
|
+
return dirs;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 生成目录选择的按钮布局
|
|
168
|
+
*/
|
|
169
|
+
export function buildDirectoryKeyboard(directories, userId) {
|
|
170
|
+
const buttons = [];
|
|
171
|
+
// 每行 2 个按钮
|
|
172
|
+
for (let i = 0; i < directories.length; i += 2) {
|
|
173
|
+
const row = [];
|
|
174
|
+
row.push({
|
|
175
|
+
text: directories[i].name,
|
|
176
|
+
callback_data: `cd:${userId}:${encodeURIComponent(directories[i].fullPath)}`,
|
|
177
|
+
});
|
|
178
|
+
if (i + 1 < directories.length) {
|
|
179
|
+
row.push({
|
|
180
|
+
text: directories[i + 1].name,
|
|
181
|
+
callback_data: `cd:${userId}:${encodeURIComponent(directories[i + 1].fullPath)}`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
buttons.push(row);
|
|
185
|
+
}
|
|
186
|
+
return { inline_keyboard: buttons };
|
|
187
|
+
}
|
|
@@ -14,6 +14,46 @@ import { THROTTLE_MS, IMAGE_DIR } from '../constants.js';
|
|
|
14
14
|
import { setActiveChatId } from '../shared/active-chats.js';
|
|
15
15
|
import { createLogger } from '../logger.js';
|
|
16
16
|
const log = createLogger('TgHandler');
|
|
17
|
+
// 动态节流器类 - 根据内容长度和更新频率调整间隔
|
|
18
|
+
class DynamicThrottle {
|
|
19
|
+
lastUpdate = 0;
|
|
20
|
+
lastContentLength = 0;
|
|
21
|
+
consecutiveErrors = 0;
|
|
22
|
+
baseInterval = THROTTLE_MS;
|
|
23
|
+
getNextDelay(contentLength) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const timeSinceLastUpdate = now - this.lastUpdate;
|
|
26
|
+
// 如果最近有错误,增加延迟
|
|
27
|
+
if (this.consecutiveErrors > 0) {
|
|
28
|
+
const errorDelay = this.baseInterval * (1 + this.consecutiveErrors * 2);
|
|
29
|
+
this.lastUpdate = now;
|
|
30
|
+
return errorDelay;
|
|
31
|
+
}
|
|
32
|
+
// 内容增长较小时,增加延迟
|
|
33
|
+
const contentGrowth = contentLength - this.lastContentLength;
|
|
34
|
+
if (contentGrowth < 100 && timeSinceLastUpdate < 1000) {
|
|
35
|
+
this.lastUpdate = now;
|
|
36
|
+
return 1000; // 内容增长缓慢,每秒更新一次
|
|
37
|
+
}
|
|
38
|
+
// 内容增长较快,使用基础间隔
|
|
39
|
+
this.lastUpdate = now;
|
|
40
|
+
this.lastContentLength = contentLength;
|
|
41
|
+
return this.baseInterval;
|
|
42
|
+
}
|
|
43
|
+
recordError() {
|
|
44
|
+
this.consecutiveErrors++;
|
|
45
|
+
// 重置时间,确保下次使用延迟
|
|
46
|
+
this.lastUpdate = Date.now();
|
|
47
|
+
}
|
|
48
|
+
recordSuccess() {
|
|
49
|
+
this.consecutiveErrors = 0;
|
|
50
|
+
}
|
|
51
|
+
reset() {
|
|
52
|
+
this.lastUpdate = 0;
|
|
53
|
+
this.lastContentLength = 0;
|
|
54
|
+
this.consecutiveErrors = 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
17
57
|
async function downloadTelegramPhoto(bot, fileId) {
|
|
18
58
|
await mkdir(IMAGE_DIR, { recursive: true });
|
|
19
59
|
const fileLink = await bot.telegram.getFileLink(fileId);
|
|
@@ -57,19 +97,97 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
57
97
|
}
|
|
58
98
|
const stopTyping = startTypingLoop(chatId);
|
|
59
99
|
const taskKey = `${userId}:${msgId}`;
|
|
100
|
+
// 创建动态节流器
|
|
101
|
+
const throttle = new DynamicThrottle();
|
|
102
|
+
// 创建包装的流式更新函数(带串行化、智能跳过和防抖)
|
|
103
|
+
const createStreamUpdateWrapper = () => {
|
|
104
|
+
let lastUpdateTime = 0;
|
|
105
|
+
let lastContentLength = 0;
|
|
106
|
+
let lastContent = '';
|
|
107
|
+
let pendingUpdate = null;
|
|
108
|
+
let updateInProgress = false; // 串行化锁
|
|
109
|
+
let scheduledContent = null; // 待更新内容
|
|
110
|
+
let scheduledToolNote;
|
|
111
|
+
// 流式输出时只显示最后 N 个字符,避免消息过大
|
|
112
|
+
const STREAM_PREVIEW_LENGTH = 500;
|
|
113
|
+
// 执行更新(串行化)
|
|
114
|
+
const performUpdate = async (content, toolNote) => {
|
|
115
|
+
if (updateInProgress) {
|
|
116
|
+
// 如果有更新正在进行,保存当前内容待更新
|
|
117
|
+
scheduledContent = content;
|
|
118
|
+
scheduledToolNote = toolNote;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
updateInProgress = true;
|
|
122
|
+
try {
|
|
123
|
+
// 流式输出时只显示最后部分内容,避免触发速率限制
|
|
124
|
+
const displayContent = content.length > STREAM_PREVIEW_LENGTH
|
|
125
|
+
? `...(已输出 ${content.length} 字符,显示最后 ${STREAM_PREVIEW_LENGTH} 字符)...\n\n${content.slice(-STREAM_PREVIEW_LENGTH)}`
|
|
126
|
+
: content;
|
|
127
|
+
const note = toolNote ? '输出中...\n' + toolNote : '输出中...';
|
|
128
|
+
await updateMessage(chatId, msgId, displayContent, 'streaming', note, toolId);
|
|
129
|
+
throttle.recordSuccess();
|
|
130
|
+
lastUpdateTime = Date.now();
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throttle.recordError();
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
updateInProgress = false;
|
|
137
|
+
// 如果有待更新的内容,立即更新
|
|
138
|
+
if (scheduledContent !== null) {
|
|
139
|
+
const nextContent = scheduledContent;
|
|
140
|
+
const nextNote = scheduledToolNote;
|
|
141
|
+
scheduledContent = null;
|
|
142
|
+
scheduledToolNote = undefined;
|
|
143
|
+
await performUpdate(nextContent, nextNote);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
// 防抖延迟(毫秒)
|
|
148
|
+
const DEBOUNCE_MS = 200;
|
|
149
|
+
let debounceTimer = null;
|
|
150
|
+
return (content, toolNote) => {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const elapsed = now - lastUpdateTime;
|
|
153
|
+
// 智能跳过:内容增长小于 50 字符且距离上次更新不足 1 秒
|
|
154
|
+
const contentGrowth = content.length - lastContentLength;
|
|
155
|
+
if (contentGrowth < 50 && elapsed < 1000 && lastContentLength > 0) {
|
|
156
|
+
// 跳过此次更新,但更新长度记录
|
|
157
|
+
lastContentLength = content.length;
|
|
158
|
+
lastContent = content;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// 更新记录
|
|
162
|
+
lastContentLength = content.length;
|
|
163
|
+
lastContent = content;
|
|
164
|
+
// 使用动态节流器计算基础延迟
|
|
165
|
+
const baseDelay = throttle.getNextDelay(content.length);
|
|
166
|
+
// 清除之前的防抖定时器
|
|
167
|
+
if (debounceTimer) {
|
|
168
|
+
clearTimeout(debounceTimer);
|
|
169
|
+
}
|
|
170
|
+
// 设置防抖定时器
|
|
171
|
+
debounceTimer = setTimeout(() => {
|
|
172
|
+
debounceTimer = null;
|
|
173
|
+
performUpdate(content, toolNote);
|
|
174
|
+
}, Math.max(DEBOUNCE_MS, baseDelay));
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const streamUpdateWrapper = createStreamUpdateWrapper();
|
|
60
178
|
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'telegram', taskKey }, prompt, toolAdapter, {
|
|
61
179
|
throttleMs: THROTTLE_MS,
|
|
62
|
-
streamUpdate:
|
|
63
|
-
const note = toolNote ? '输出中...\n' + toolNote : '输出中...';
|
|
64
|
-
updateMessage(chatId, msgId, content, 'streaming', note, toolId).catch(() => { });
|
|
65
|
-
},
|
|
180
|
+
streamUpdate: streamUpdateWrapper,
|
|
66
181
|
sendComplete: async (content, note) => {
|
|
182
|
+
throttle.reset();
|
|
67
183
|
await sendFinalMessages(chatId, msgId, content, note, toolId);
|
|
68
184
|
},
|
|
69
185
|
sendError: async (error) => {
|
|
186
|
+
throttle.reset();
|
|
70
187
|
await updateMessage(chatId, msgId, `错误:${error}`, 'error', '执行失败', toolId);
|
|
71
188
|
},
|
|
72
189
|
extraCleanup: () => {
|
|
190
|
+
throttle.reset();
|
|
73
191
|
stopTyping();
|
|
74
192
|
runningTasks.delete(taskKey);
|
|
75
193
|
},
|
|
@@ -4,4 +4,8 @@ export declare function updateMessage(chatId: string, messageId: string, content
|
|
|
4
4
|
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
5
5
|
export declare function sendTextReply(chatId: string, text: string): Promise<void>;
|
|
6
6
|
export declare function sendImageReply(chatId: string, imagePath: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* 发送目录选择界面
|
|
9
|
+
*/
|
|
10
|
+
export declare function sendDirectorySelection(chatId: string, currentDir: string, userId: string): Promise<void>;
|
|
7
11
|
export declare function startTypingLoop(chatId: string): () => void;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getBot } from './client.js';
|
|
2
2
|
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { basename } from 'node:path';
|
|
3
4
|
import { createLogger } from '../logger.js';
|
|
4
5
|
import { splitLongContent, truncateText } from '../shared/utils.js';
|
|
5
6
|
import { MAX_TELEGRAM_MESSAGE_LENGTH } from '../constants.js';
|
|
7
|
+
import { listDirectories, buildDirectoryKeyboard } from '../commands/handler.js';
|
|
6
8
|
const log = createLogger('TgSender');
|
|
7
9
|
const STATUS_ICONS = {
|
|
8
10
|
thinking: '🔵',
|
|
@@ -23,13 +25,32 @@ function getToolTitle(toolId, status) {
|
|
|
23
25
|
return `${name} - 错误`;
|
|
24
26
|
return name;
|
|
25
27
|
}
|
|
28
|
+
// Telegram 实际消息长度限制(4096 字符)
|
|
29
|
+
const TG_MAX_LENGTH = 4096;
|
|
30
|
+
// 预留给 header 和 note 的空间
|
|
31
|
+
const RESERVED_LENGTH = 150;
|
|
26
32
|
function formatMessage(content, status, note, toolId = 'claude') {
|
|
27
33
|
const icon = STATUS_ICONS[status];
|
|
28
34
|
const title = getToolTitle(toolId, status);
|
|
29
|
-
|
|
35
|
+
// 计算可用内容长度(预留 header 和 note 空间)
|
|
36
|
+
const headerLength = `${icon} ${title}\n\n`.length;
|
|
37
|
+
const noteLength = note ? `\n\n─────────\n${note}`.length : 0;
|
|
38
|
+
const maxContentLength = TG_MAX_LENGTH - headerLength - noteLength - RESERVED_LENGTH;
|
|
39
|
+
// 确保内容长度不超过限制
|
|
40
|
+
const text = truncateText(content, Math.max(100, maxContentLength));
|
|
30
41
|
let out = `${icon} ${title}\n\n${text}`;
|
|
31
42
|
if (note)
|
|
32
43
|
out += `\n\n─────────\n${note}`;
|
|
44
|
+
// 最终安全检查:如果还是太长,强制截断
|
|
45
|
+
if (out.length > TG_MAX_LENGTH) {
|
|
46
|
+
const keepLen = TG_MAX_LENGTH - 50;
|
|
47
|
+
const tail = text.slice(text.length - keepLen);
|
|
48
|
+
const lineBreak = tail.indexOf('\n');
|
|
49
|
+
const clean = lineBreak > 0 && lineBreak < 200 ? tail.slice(lineBreak + 1) : tail;
|
|
50
|
+
out = `${icon} ${title}\n\n...(前文已省略)...\n${clean}`;
|
|
51
|
+
if (note)
|
|
52
|
+
out += `\n\n─────────\n${note}`;
|
|
53
|
+
}
|
|
33
54
|
return out;
|
|
34
55
|
}
|
|
35
56
|
function buildStopKeyboard(messageId) {
|
|
@@ -50,24 +71,83 @@ export async function sendThinkingMessage(chatId, replyToMessageId, toolId = 'cl
|
|
|
50
71
|
await bot.telegram.editMessageText(Number(chatId), msg.message_id, undefined, formatMessage('正在思考...', 'thinking', '请稍候', toolId), { reply_markup: buildStopKeyboard(msg.message_id) });
|
|
51
72
|
return String(msg.message_id);
|
|
52
73
|
}
|
|
74
|
+
// 检查错误是否可忽略
|
|
75
|
+
function isIgnorableError(err) {
|
|
76
|
+
if (err && typeof err === 'object' && 'message' in err) {
|
|
77
|
+
const msg = String(err.message);
|
|
78
|
+
return (msg.includes('not modified') ||
|
|
79
|
+
msg.includes('MESSAGE_TOO_LONG') ||
|
|
80
|
+
msg.includes('can\'t parse entities') ||
|
|
81
|
+
msg.includes('message to edit not found') ||
|
|
82
|
+
msg.includes('message is not modified'));
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
// 提取重试延迟时间(秒)
|
|
87
|
+
function extractRetryAfter(err) {
|
|
88
|
+
if (err && typeof err === 'object' && 'message' in err) {
|
|
89
|
+
const msg = String(err.message);
|
|
90
|
+
const match = msg.match(/retry after (\d+)/);
|
|
91
|
+
if (match)
|
|
92
|
+
return parseInt(match[1], 10);
|
|
93
|
+
}
|
|
94
|
+
if (err && typeof err === 'object' && 'parameters' in err) {
|
|
95
|
+
const params = err.parameters;
|
|
96
|
+
if (params?.retry_after)
|
|
97
|
+
return params.retry_after;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
53
101
|
export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
|
|
54
102
|
const bot = getBot();
|
|
55
103
|
const opts = {};
|
|
56
104
|
if (status === 'thinking' || status === 'streaming') {
|
|
57
105
|
opts.reply_markup = buildStopKeyboard(Number(messageId));
|
|
58
106
|
}
|
|
107
|
+
else if (status === 'done' || status === 'error') {
|
|
108
|
+
// 完成或错误时,显式移除停止按钮
|
|
109
|
+
opts.reply_markup = {};
|
|
110
|
+
}
|
|
59
111
|
// 流式输出时使用纯文本,避免 Markdown 解析导致内容减少
|
|
60
112
|
// 只在完成时应用 Markdown 格式
|
|
61
113
|
const shouldParseMarkdown = status === 'done' || status === 'error';
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
114
|
+
let retries = 0;
|
|
115
|
+
const maxRetries = 2; // 减少重试次数,但增加等待时间
|
|
116
|
+
while (retries <= maxRetries) {
|
|
117
|
+
try {
|
|
118
|
+
await bot.telegram.editMessageText(Number(chatId), Number(messageId), undefined, formatMessage(content, status, note, toolId), { ...opts, parse_mode: shouldParseMarkdown ? 'Markdown' : undefined });
|
|
119
|
+
return;
|
|
68
120
|
}
|
|
69
|
-
|
|
70
|
-
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (isIgnorableError(err)) {
|
|
123
|
+
// 忽略这些错误,不需要重试
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const retryAfter = extractRetryAfter(err);
|
|
127
|
+
if (retryAfter !== null && retries < maxRetries) {
|
|
128
|
+
// 429 错误,使用 Telegram 返回的实际等待时间
|
|
129
|
+
// 添加额外的缓冲时间(10%),确保不会立即再次触发限制
|
|
130
|
+
const delayMs = Math.ceil(retryAfter * 1000 * 1.1);
|
|
131
|
+
log.warn(`Rate limited, waiting ${delayMs}ms (${retryAfter}s + 10% buffer) before retry (${retries + 1}/${maxRetries})`);
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
133
|
+
retries++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// 对于非 429 错误,使用指数退避
|
|
137
|
+
if (retries < maxRetries) {
|
|
138
|
+
const delayMs = Math.pow(2, retries) * 1000; // 1s, 2s, 4s
|
|
139
|
+
log.warn(`Temporary error, waiting ${delayMs}ms before retry (${retries + 1}/${maxRetries}):`, err);
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
141
|
+
retries++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (retries >= maxRetries) {
|
|
145
|
+
log.error('Failed to update message after retries:', err);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
log.error('Failed to update message:', err);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
71
151
|
}
|
|
72
152
|
}
|
|
73
153
|
}
|
|
@@ -97,6 +177,23 @@ export async function sendImageReply(chatId, imagePath) {
|
|
|
97
177
|
const bot = getBot();
|
|
98
178
|
await bot.telegram.sendPhoto(Number(chatId), { source: createReadStream(imagePath) });
|
|
99
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* 发送目录选择界面
|
|
182
|
+
*/
|
|
183
|
+
export async function sendDirectorySelection(chatId, currentDir, userId) {
|
|
184
|
+
const bot = getBot();
|
|
185
|
+
const directories = listDirectories(currentDir);
|
|
186
|
+
if (directories.length === 0) {
|
|
187
|
+
await bot.telegram.sendMessage(Number(chatId), `📁 当前目录: \`${currentDir}\`\n\n没有可访问的子目录`, { parse_mode: 'Markdown' });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const keyboard = buildDirectoryKeyboard(directories, userId);
|
|
191
|
+
const dirName = basename(currentDir) || currentDir;
|
|
192
|
+
await bot.telegram.sendMessage(Number(chatId), `📁 当前目录: \`${dirName}\`\n\n选择要切换到的目录:`, {
|
|
193
|
+
parse_mode: 'Markdown',
|
|
194
|
+
reply_markup: keyboard,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
100
197
|
export function startTypingLoop(chatId) {
|
|
101
198
|
const bot = getBot();
|
|
102
199
|
const interval = setInterval(() => {
|