@wu529778790/open-im 0.2.12 → 0.3.1
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 < 50 && timeSinceLastUpdate < 500) {
|
|
35
|
+
this.lastUpdate = now;
|
|
36
|
+
return 500; // 内容增长缓慢时,每 500ms 更新一次
|
|
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,151 @@ 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
|
+
let savedThinkingText = '';
|
|
104
|
+
let hasThinkingContent = false; // 是否包含思考内容
|
|
105
|
+
// 创建包装的流式更新函数(带串行化、智能跳过和防抖)
|
|
106
|
+
const createStreamUpdateWrapper = () => {
|
|
107
|
+
let lastUpdateTime = 0;
|
|
108
|
+
let lastContentLength = 0;
|
|
109
|
+
let lastContent = '';
|
|
110
|
+
let pendingUpdate = null;
|
|
111
|
+
let updateInProgress = false; // 串行化锁
|
|
112
|
+
let scheduledContent = null; // 待更新内容
|
|
113
|
+
let scheduledToolNote;
|
|
114
|
+
// 流式输出时显示最后 N 个字符(增加到 1500 以提高流畅度)
|
|
115
|
+
const STREAM_PREVIEW_LENGTH = 1500;
|
|
116
|
+
// 执行更新(串行化)
|
|
117
|
+
const performUpdate = async (content, toolNote) => {
|
|
118
|
+
if (updateInProgress) {
|
|
119
|
+
// 如果有更新正在进行,保存当前内容待更新
|
|
120
|
+
scheduledContent = content;
|
|
121
|
+
scheduledToolNote = toolNote;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
updateInProgress = true;
|
|
125
|
+
try {
|
|
126
|
+
let displayContent = content;
|
|
127
|
+
// 如果有思考内容,将其格式化后添加到前面
|
|
128
|
+
if (hasThinkingContent && savedThinkingText) {
|
|
129
|
+
// 思考内容使用引用格式,用分隔线区分
|
|
130
|
+
const thinkingFormatted = `💭 思考过程:\n${savedThinkingText}`;
|
|
131
|
+
const separator = '\n\n─────────\n\n';
|
|
132
|
+
// 组合内容
|
|
133
|
+
const combined = thinkingFormatted + separator + content;
|
|
134
|
+
// 如果组合后超过预览长度,截取最后部分(但保留思考内容)
|
|
135
|
+
if (combined.length > STREAM_PREVIEW_LENGTH) {
|
|
136
|
+
// 如果思考内容本身就很长,截取思考内容
|
|
137
|
+
const maxThinkingLength = 800;
|
|
138
|
+
const truncatedThinking = savedThinkingText.length > maxThinkingLength
|
|
139
|
+
? `...(已省略 ${savedThinkingText.length - maxThinkingLength} 字符)...\n\n${savedThinkingText.slice(-maxThinkingLength)}`
|
|
140
|
+
: savedThinkingText;
|
|
141
|
+
displayContent = `💭 思考过程:\n${truncatedThinking}\n\n─────────\n\n`;
|
|
142
|
+
// 添加输出内容的最后部分
|
|
143
|
+
if (content.length > 800) {
|
|
144
|
+
displayContent += `...\n\n${content.slice(-800)}`;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
displayContent += content;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
displayContent = combined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// 没有思考内容,直接显示(如果超过预览长度则截取)
|
|
156
|
+
displayContent = content.length > STREAM_PREVIEW_LENGTH
|
|
157
|
+
? `...\n\n${content.slice(-STREAM_PREVIEW_LENGTH)}`
|
|
158
|
+
: content;
|
|
159
|
+
}
|
|
160
|
+
const note = toolNote ? '输出中...\n' + toolNote : '输出中...';
|
|
161
|
+
await updateMessage(chatId, msgId, displayContent, 'streaming', note, toolId);
|
|
162
|
+
throttle.recordSuccess();
|
|
163
|
+
lastUpdateTime = Date.now();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
throttle.recordError();
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
updateInProgress = false;
|
|
170
|
+
// 如果有待更新的内容,立即更新
|
|
171
|
+
if (scheduledContent !== null) {
|
|
172
|
+
const nextContent = scheduledContent;
|
|
173
|
+
const nextNote = scheduledToolNote;
|
|
174
|
+
scheduledContent = null;
|
|
175
|
+
scheduledToolNote = undefined;
|
|
176
|
+
await performUpdate(nextContent, nextNote);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
// 防抖延迟(毫秒)- 降低到 150ms 提高响应速度
|
|
181
|
+
const DEBOUNCE_MS = 150;
|
|
182
|
+
let debounceTimer = null;
|
|
183
|
+
return (content, toolNote) => {
|
|
184
|
+
// 检测是否是思考内容
|
|
185
|
+
if (content.startsWith('💭 **思考中...**')) {
|
|
186
|
+
// 保存思考内容(去掉前缀)
|
|
187
|
+
savedThinkingText = content.replace('💭 **思考中...**\n\n', '');
|
|
188
|
+
hasThinkingContent = true;
|
|
189
|
+
}
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
const elapsed = now - lastUpdateTime;
|
|
192
|
+
// 智能跳过:内容增长小于 30 字符且距离上次更新不足 500ms(降低阈值,提高流畅度)
|
|
193
|
+
const contentGrowth = content.length - lastContentLength;
|
|
194
|
+
if (contentGrowth < 30 && elapsed < 500 && lastContentLength > 0) {
|
|
195
|
+
// 跳过此次更新,但更新长度记录
|
|
196
|
+
lastContentLength = content.length;
|
|
197
|
+
lastContent = content;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// 更新记录
|
|
201
|
+
lastContentLength = content.length;
|
|
202
|
+
lastContent = content;
|
|
203
|
+
// 使用动态节流器计算基础延迟
|
|
204
|
+
const baseDelay = throttle.getNextDelay(content.length);
|
|
205
|
+
// 清除之前的防抖定时器
|
|
206
|
+
if (debounceTimer) {
|
|
207
|
+
clearTimeout(debounceTimer);
|
|
208
|
+
}
|
|
209
|
+
// 设置防抖定时器
|
|
210
|
+
debounceTimer = setTimeout(() => {
|
|
211
|
+
debounceTimer = null;
|
|
212
|
+
performUpdate(content, toolNote);
|
|
213
|
+
}, Math.max(DEBOUNCE_MS, baseDelay));
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const streamUpdateWrapper = createStreamUpdateWrapper();
|
|
60
217
|
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'telegram', taskKey }, prompt, toolAdapter, {
|
|
61
218
|
throttleMs: THROTTLE_MS,
|
|
62
|
-
streamUpdate:
|
|
63
|
-
|
|
64
|
-
|
|
219
|
+
streamUpdate: streamUpdateWrapper,
|
|
220
|
+
onThinkingToText: (content) => {
|
|
221
|
+
// 从思考转到文本输出时,标记有思考内容
|
|
222
|
+
// 注意:此时不保存文本内容,因为后续会通过 streamUpdate 持续更新
|
|
65
223
|
},
|
|
66
224
|
sendComplete: async (content, note) => {
|
|
67
|
-
|
|
225
|
+
throttle.reset();
|
|
226
|
+
// 完成时,如果有思考内容,将其包含在最终消息中
|
|
227
|
+
if (savedThinkingText && hasThinkingContent) {
|
|
228
|
+
const thinkingFormatted = `💭 思考过程:\n${savedThinkingText}\n\n─────────\n\n`;
|
|
229
|
+
const combined = thinkingFormatted + content;
|
|
230
|
+
await sendFinalMessages(chatId, msgId, combined, note, toolId);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
await sendFinalMessages(chatId, msgId, content, note, toolId);
|
|
234
|
+
}
|
|
68
235
|
},
|
|
69
236
|
sendError: async (error) => {
|
|
237
|
+
throttle.reset();
|
|
70
238
|
await updateMessage(chatId, msgId, `错误:${error}`, 'error', '执行失败', toolId);
|
|
71
239
|
},
|
|
72
240
|
extraCleanup: () => {
|
|
241
|
+
throttle.reset();
|
|
242
|
+
// 清理思考内容
|
|
243
|
+
savedThinkingText = '';
|
|
244
|
+
hasThinkingContent = false;
|
|
73
245
|
stopTyping();
|
|
74
246
|
runningTasks.delete(taskKey);
|
|
75
247
|
},
|
|
@@ -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(() => {
|