@wu529778790/open-im 0.3.12 → 1.0.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.
- package/README.md +117 -78
- package/dist/access/access-control.js +9 -2
- package/dist/claude/cli-runner.js +79 -28
- package/dist/cli.js +102 -183
- package/dist/config.d.ts +16 -2
- package/dist/config.js +70 -7
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/feishu/client.d.ts +5 -0
- package/dist/feishu/client.js +69 -0
- package/dist/feishu/event-handler.d.ts +8 -0
- package/dist/feishu/event-handler.js +255 -0
- package/dist/feishu/message-sender.d.ts +7 -0
- package/dist/feishu/message-sender.js +253 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +85 -69
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +164 -66
- package/dist/telegram/client.d.ts +2 -2
- package/dist/telegram/client.js +11 -22
- package/dist/telegram/event-handler.d.ts +3 -3
- package/dist/telegram/event-handler.js +84 -71
- package/dist/telegram/message-sender.d.ts +1 -1
- package/dist/telegram/message-sender.js +72 -89
- package/package.json +3 -3
- package/.env.example +0 -16
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { mkdir, writeFile } from
|
|
2
|
-
import { join } from
|
|
3
|
-
import { message } from
|
|
4
|
-
import { AccessControl } from
|
|
5
|
-
import { RequestQueue } from
|
|
6
|
-
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, startTypingLoop, sendImageReply, } from
|
|
7
|
-
import { registerPermissionSender, resolvePermissionById } from
|
|
8
|
-
import { CommandHandler } from
|
|
9
|
-
import { getAdapter } from
|
|
10
|
-
import { runAITask } from
|
|
11
|
-
import { startTaskCleanup } from
|
|
12
|
-
import { MessageDedup } from
|
|
13
|
-
import {
|
|
14
|
-
import { setActiveChatId } from
|
|
15
|
-
import { createLogger } from
|
|
16
|
-
const log = createLogger(
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { message } from "telegraf/filters";
|
|
4
|
+
import { AccessControl } from "../access/access-control.js";
|
|
5
|
+
import { RequestQueue } from "../queue/request-queue.js";
|
|
6
|
+
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, startTypingLoop, sendImageReply, } from "./message-sender.js";
|
|
7
|
+
import { registerPermissionSender, resolvePermissionById, } from "../hook/permission-server.js";
|
|
8
|
+
import { CommandHandler } from "../commands/handler.js";
|
|
9
|
+
import { getAdapter } from "../adapters/registry.js";
|
|
10
|
+
import { runAITask } from "../shared/ai-task.js";
|
|
11
|
+
import { startTaskCleanup } from "../shared/task-cleanup.js";
|
|
12
|
+
import { MessageDedup } from "../shared/message-dedup.js";
|
|
13
|
+
import { TELEGRAM_THROTTLE_MS, IMAGE_DIR } from "../constants.js";
|
|
14
|
+
import { setActiveChatId } from "../shared/active-chats.js";
|
|
15
|
+
import { createLogger } from "../logger.js";
|
|
16
|
+
const log = createLogger("TgHandler");
|
|
17
17
|
// 动态节流器类 - 根据内容长度和更新频率调整间隔
|
|
18
18
|
class DynamicThrottle {
|
|
19
19
|
lastUpdate = 0;
|
|
20
20
|
lastContentLength = 0;
|
|
21
21
|
consecutiveErrors = 0;
|
|
22
|
-
baseInterval =
|
|
22
|
+
baseInterval = TELEGRAM_THROTTLE_MS;
|
|
23
23
|
getNextDelay(contentLength) {
|
|
24
24
|
const now = Date.now();
|
|
25
25
|
const timeSinceLastUpdate = now - this.lastUpdate;
|
|
@@ -57,15 +57,17 @@ class DynamicThrottle {
|
|
|
57
57
|
async function downloadTelegramPhoto(bot, fileId) {
|
|
58
58
|
await mkdir(IMAGE_DIR, { recursive: true });
|
|
59
59
|
const fileLink = await bot.telegram.getFileLink(fileId);
|
|
60
|
-
const res = await fetch(fileLink.href, {
|
|
60
|
+
const res = await fetch(fileLink.href, {
|
|
61
|
+
signal: AbortSignal.timeout(30000),
|
|
62
|
+
});
|
|
61
63
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
62
|
-
const safeId = fileId.replace(/[^a-zA-Z0-9_-]/g,
|
|
64
|
+
const safeId = fileId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
63
65
|
const imagePath = join(IMAGE_DIR, `${Date.now()}-${safeId.slice(-8)}.jpg`);
|
|
64
66
|
await writeFile(imagePath, buffer);
|
|
65
67
|
return imagePath;
|
|
66
68
|
}
|
|
67
69
|
export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
68
|
-
const accessControl = new AccessControl(config.
|
|
70
|
+
const accessControl = new AccessControl(config.telegramAllowedUserIds);
|
|
69
71
|
const requestQueue = new RequestQueue();
|
|
70
72
|
const runningTasks = new Map();
|
|
71
73
|
const stopTaskCleanup = startTaskCleanup(runningTasks);
|
|
@@ -77,7 +79,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
77
79
|
sender: { sendTextReply },
|
|
78
80
|
getRunningTasksSize: () => runningTasks.size,
|
|
79
81
|
});
|
|
80
|
-
registerPermissionSender(
|
|
82
|
+
registerPermissionSender("telegram", {});
|
|
81
83
|
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
82
84
|
// 在用户每次发送消息时就累加计数,确保提示能轮换显示
|
|
83
85
|
const currentTurns = sessionManager.addTurns(userId, 1);
|
|
@@ -87,15 +89,17 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
87
89
|
await sendTextReply(chatId, `未配置 AI 工具: ${config.aiCommand}`);
|
|
88
90
|
return;
|
|
89
91
|
}
|
|
90
|
-
const sessionId = convId
|
|
91
|
-
|
|
92
|
+
const sessionId = convId
|
|
93
|
+
? sessionManager.getSessionIdForConv(userId, convId)
|
|
94
|
+
: undefined;
|
|
95
|
+
log.info(`Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? "new"}`);
|
|
92
96
|
const toolId = config.aiCommand;
|
|
93
97
|
let msgId;
|
|
94
98
|
try {
|
|
95
99
|
msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId);
|
|
96
100
|
}
|
|
97
101
|
catch (err) {
|
|
98
|
-
log.error(
|
|
102
|
+
log.error("Failed to send thinking message:", err);
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
105
|
const stopTyping = startTypingLoop(chatId);
|
|
@@ -103,13 +107,13 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
103
107
|
// 创建动态节流器
|
|
104
108
|
const throttle = new DynamicThrottle();
|
|
105
109
|
// 不保存思考内容,只显示最终结果
|
|
106
|
-
let savedThinkingText =
|
|
110
|
+
let savedThinkingText = "";
|
|
107
111
|
let hasThinkingContent = false; // 始终为 false,不显示思考内容
|
|
108
112
|
// 创建包装的流式更新函数(带串行化、智能跳过和防抖)
|
|
109
113
|
const createStreamUpdateWrapper = () => {
|
|
110
114
|
let lastUpdateTime = 0;
|
|
111
115
|
let lastContentLength = 0;
|
|
112
|
-
let lastContent =
|
|
116
|
+
let lastContent = "";
|
|
113
117
|
let pendingUpdate = null;
|
|
114
118
|
let updateInProgress = false; // 串行化锁
|
|
115
119
|
let scheduledContent = null; // 待更新内容
|
|
@@ -127,7 +131,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
127
131
|
}
|
|
128
132
|
// 如果有更新正在进行,等待它完成
|
|
129
133
|
while (updateInProgress) {
|
|
130
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
131
135
|
}
|
|
132
136
|
// 重置状态,确保完成更新能立即执行
|
|
133
137
|
updateInProgress = false;
|
|
@@ -147,7 +151,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
147
151
|
if (hasThinkingContent && savedThinkingText) {
|
|
148
152
|
// 思考内容使用引用格式,用分隔线区分
|
|
149
153
|
const thinkingFormatted = `💭 思考过程:\n${savedThinkingText}`;
|
|
150
|
-
const separator =
|
|
154
|
+
const separator = "\n\n─────────\n\n";
|
|
151
155
|
// 组合内容
|
|
152
156
|
const combined = thinkingFormatted + separator + content;
|
|
153
157
|
// 如果组合后超过预览长度,截取最后部分(但保留思考内容)
|
|
@@ -172,12 +176,13 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
172
176
|
}
|
|
173
177
|
else {
|
|
174
178
|
// 没有思考内容,直接显示(如果超过预览长度则截取)
|
|
175
|
-
displayContent =
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
displayContent =
|
|
180
|
+
content.length > STREAM_PREVIEW_LENGTH
|
|
181
|
+
? `...\n\n${content.slice(-STREAM_PREVIEW_LENGTH)}`
|
|
182
|
+
: content;
|
|
178
183
|
}
|
|
179
|
-
const note = toolNote ?
|
|
180
|
-
await updateMessage(chatId, msgId, displayContent,
|
|
184
|
+
const note = toolNote ? "输出中...\n" + toolNote : "输出中...";
|
|
185
|
+
await updateMessage(chatId, msgId, displayContent, "streaming", note, toolId);
|
|
181
186
|
throttle.recordSuccess();
|
|
182
187
|
lastUpdateTime = Date.now();
|
|
183
188
|
}
|
|
@@ -201,7 +206,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
201
206
|
let debounceTimer = null;
|
|
202
207
|
return (content, toolNote) => {
|
|
203
208
|
// 检测是否是思考内容,如果则跳过不显示
|
|
204
|
-
if (content.startsWith(
|
|
209
|
+
if (content.startsWith("💭 **思考中...**")) {
|
|
205
210
|
// 不保存思考内容,直接返回,不触发更新
|
|
206
211
|
return;
|
|
207
212
|
}
|
|
@@ -232,12 +237,19 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
232
237
|
};
|
|
233
238
|
};
|
|
234
239
|
const streamUpdateWrapper = createStreamUpdateWrapper();
|
|
235
|
-
await runAITask({ config, sessionManager }, {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
await runAITask({ config, sessionManager }, {
|
|
241
|
+
userId,
|
|
242
|
+
chatId,
|
|
243
|
+
workDir,
|
|
244
|
+
sessionId,
|
|
245
|
+
convId,
|
|
246
|
+
platform: "telegram",
|
|
247
|
+
taskKey,
|
|
248
|
+
}, prompt, toolAdapter, {
|
|
249
|
+
throttleMs: TELEGRAM_THROTTLE_MS,
|
|
250
|
+
streamUpdate: (content, toolNote) => {
|
|
251
|
+
const note = toolNote ? "输出中...\n" + toolNote : "输出中...";
|
|
252
|
+
updateMessage(chatId, msgId, content, "streaming", note, toolId).catch(() => { });
|
|
241
253
|
},
|
|
242
254
|
sendComplete: async (content, note) => {
|
|
243
255
|
throttle.reset();
|
|
@@ -247,19 +259,19 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
247
259
|
await sendFinalMessages(chatId, msgId, content, note, toolId);
|
|
248
260
|
}
|
|
249
261
|
catch (err) {
|
|
250
|
-
log.error(
|
|
262
|
+
log.error("Failed to send complete message:", err);
|
|
251
263
|
// 如果发送失败,至少尝试更新状态
|
|
252
|
-
await updateMessage(chatId, msgId, content,
|
|
264
|
+
await updateMessage(chatId, msgId, content, "done", note, toolId);
|
|
253
265
|
}
|
|
254
266
|
},
|
|
255
267
|
sendError: async (error) => {
|
|
256
268
|
throttle.reset();
|
|
257
|
-
await updateMessage(chatId, msgId, `错误:${error}`,
|
|
269
|
+
await updateMessage(chatId, msgId, `错误:${error}`, "error", "执行失败", toolId);
|
|
258
270
|
},
|
|
259
271
|
extraCleanup: () => {
|
|
260
272
|
throttle.reset();
|
|
261
273
|
// 清理思考内容
|
|
262
|
-
savedThinkingText =
|
|
274
|
+
savedThinkingText = "";
|
|
263
275
|
hasThinkingContent = false;
|
|
264
276
|
stopTyping();
|
|
265
277
|
runningTasks.delete(taskKey);
|
|
@@ -270,37 +282,38 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
270
282
|
sendImage: (path) => sendImageReply(chatId, path),
|
|
271
283
|
});
|
|
272
284
|
}
|
|
273
|
-
bot.on(
|
|
285
|
+
bot.on("callback_query", async (ctx) => {
|
|
274
286
|
const query = ctx.callbackQuery;
|
|
275
|
-
if (!(
|
|
287
|
+
if (!("data" in query))
|
|
276
288
|
return;
|
|
277
|
-
const userId = String(ctx.from?.id ??
|
|
289
|
+
const userId = String(ctx.from?.id ?? "");
|
|
278
290
|
const data = query.data;
|
|
279
|
-
if (data.startsWith(
|
|
280
|
-
const messageId = data.replace(
|
|
291
|
+
if (data.startsWith("stop_")) {
|
|
292
|
+
const messageId = data.replace("stop_", "");
|
|
281
293
|
const taskKey = `${userId}:${messageId}`;
|
|
282
294
|
const taskInfo = runningTasks.get(taskKey);
|
|
283
295
|
if (taskInfo) {
|
|
284
296
|
runningTasks.delete(taskKey);
|
|
285
297
|
taskInfo.settle();
|
|
286
298
|
taskInfo.handle.abort();
|
|
287
|
-
const chatId = String(ctx.chat?.id ??
|
|
288
|
-
await updateMessage(chatId, messageId, taskInfo.latestContent ||
|
|
289
|
-
await ctx.answerCbQuery(
|
|
299
|
+
const chatId = String(ctx.chat?.id ?? "");
|
|
300
|
+
await updateMessage(chatId, messageId, taskInfo.latestContent || "已停止", "error", "⏹️ 已停止", config.aiCommand);
|
|
301
|
+
await ctx.answerCbQuery("已停止执行");
|
|
290
302
|
}
|
|
291
303
|
else {
|
|
292
|
-
await ctx.answerCbQuery(
|
|
304
|
+
await ctx.answerCbQuery("任务已完成或不存在");
|
|
293
305
|
}
|
|
294
306
|
}
|
|
295
|
-
else if (data.startsWith(
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
const
|
|
307
|
+
else if (data.startsWith("perm_allow_") ||
|
|
308
|
+
data.startsWith("perm_deny_")) {
|
|
309
|
+
const isAllow = data.startsWith("perm_allow_");
|
|
310
|
+
const requestId = data.replace(/^perm_(allow|deny)_/, "");
|
|
311
|
+
const decision = isAllow ? "allow" : "deny";
|
|
299
312
|
resolvePermissionById(requestId, decision);
|
|
300
|
-
await ctx.answerCbQuery(isAllow ?
|
|
313
|
+
await ctx.answerCbQuery(isAllow ? "✅ 已允许" : "❌ 已拒绝");
|
|
301
314
|
}
|
|
302
315
|
});
|
|
303
|
-
bot.on(message(
|
|
316
|
+
bot.on(message("text"), async (ctx) => {
|
|
304
317
|
const chatId = String(ctx.chat.id);
|
|
305
318
|
const userId = String(ctx.from.id);
|
|
306
319
|
const messageId = String(ctx.message.message_id);
|
|
@@ -308,11 +321,11 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
308
321
|
if (dedup.isDuplicate(`${chatId}:${messageId}`))
|
|
309
322
|
return;
|
|
310
323
|
if (!accessControl.isAllowed(userId)) {
|
|
311
|
-
await sendTextReply(chatId,
|
|
324
|
+
await sendTextReply(chatId, "抱歉,您没有访问权限。\n您的 ID: " + userId);
|
|
312
325
|
return;
|
|
313
326
|
}
|
|
314
|
-
setActiveChatId(
|
|
315
|
-
if (await commandHandler.dispatch(text, chatId, userId,
|
|
327
|
+
setActiveChatId("telegram", chatId);
|
|
328
|
+
if (await commandHandler.dispatch(text, chatId, userId, "telegram", handleAIRequest)) {
|
|
316
329
|
return;
|
|
317
330
|
}
|
|
318
331
|
const workDir = sessionManager.getWorkDir(userId);
|
|
@@ -320,22 +333,22 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
320
333
|
const enqueueResult = requestQueue.enqueue(userId, convId, text, async (prompt) => {
|
|
321
334
|
await handleAIRequest(userId, chatId, prompt, workDir, convId, undefined, messageId);
|
|
322
335
|
});
|
|
323
|
-
if (enqueueResult ===
|
|
324
|
-
await sendTextReply(chatId,
|
|
336
|
+
if (enqueueResult === "rejected") {
|
|
337
|
+
await sendTextReply(chatId, "请求队列已满,请稍后再试。");
|
|
325
338
|
}
|
|
326
|
-
else if (enqueueResult ===
|
|
327
|
-
await sendTextReply(chatId,
|
|
339
|
+
else if (enqueueResult === "queued") {
|
|
340
|
+
await sendTextReply(chatId, "您的请求已排队等待。");
|
|
328
341
|
}
|
|
329
342
|
});
|
|
330
|
-
bot.on(message(
|
|
343
|
+
bot.on(message("photo"), async (ctx) => {
|
|
331
344
|
const chatId = String(ctx.chat.id);
|
|
332
345
|
const userId = String(ctx.from.id);
|
|
333
|
-
const caption = ctx.message.caption?.trim() ||
|
|
346
|
+
const caption = ctx.message.caption?.trim() || "";
|
|
334
347
|
if (dedup.isDuplicate(`${chatId}:${ctx.message.message_id}`))
|
|
335
348
|
return;
|
|
336
349
|
if (!accessControl.isAllowed(userId))
|
|
337
350
|
return;
|
|
338
|
-
setActiveChatId(
|
|
351
|
+
setActiveChatId("telegram", chatId);
|
|
339
352
|
const photos = ctx.message.photo;
|
|
340
353
|
const largest = photos[photos.length - 1];
|
|
341
354
|
let imagePath;
|
|
@@ -343,8 +356,8 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
343
356
|
imagePath = await downloadTelegramPhoto(bot, largest.file_id);
|
|
344
357
|
}
|
|
345
358
|
catch (err) {
|
|
346
|
-
log.error(
|
|
347
|
-
await sendTextReply(chatId,
|
|
359
|
+
log.error("Failed to download photo:", err);
|
|
360
|
+
await sendTextReply(chatId, "图片下载失败。");
|
|
348
361
|
return;
|
|
349
362
|
}
|
|
350
363
|
const prompt = caption
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type MessageStatus =
|
|
1
|
+
export type MessageStatus = "thinking" | "streaming" | "done" | "error";
|
|
2
2
|
export declare function sendThinkingMessage(chatId: string, replyToMessageId?: string, toolId?: string): Promise<string>;
|
|
3
3
|
export declare function updateMessage(chatId: string, messageId: string, content: string, status: MessageStatus, note?: string, toolId?: string): Promise<void>;
|
|
4
4
|
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
import { getBot } from
|
|
2
|
-
import { createReadStream } from
|
|
3
|
-
import { basename } from
|
|
4
|
-
import { createLogger } from
|
|
5
|
-
import { splitLongContent, truncateText, preprocessMarkdownForTelegram } from
|
|
6
|
-
import { MAX_TELEGRAM_MESSAGE_LENGTH } from
|
|
7
|
-
import { listDirectories, buildDirectoryKeyboard } from
|
|
8
|
-
const log = createLogger(
|
|
1
|
+
import { getBot } from "./client.js";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { createLogger } from "../logger.js";
|
|
5
|
+
import { splitLongContent, truncateText, preprocessMarkdownForTelegram, } from "../shared/utils.js";
|
|
6
|
+
import { MAX_TELEGRAM_MESSAGE_LENGTH } from "../constants.js";
|
|
7
|
+
import { listDirectories, buildDirectoryKeyboard, } from "../commands/handler.js";
|
|
8
|
+
const log = createLogger("TgSender");
|
|
9
|
+
const lastSentByMsg = new Map();
|
|
9
10
|
const STATUS_ICONS = {
|
|
10
|
-
thinking:
|
|
11
|
-
streaming:
|
|
12
|
-
done:
|
|
13
|
-
error:
|
|
11
|
+
thinking: "🔵",
|
|
12
|
+
streaming: "🔵",
|
|
13
|
+
done: "🟢",
|
|
14
|
+
error: "🔴",
|
|
14
15
|
};
|
|
15
16
|
const TOOL_DISPLAY_NAMES = {
|
|
16
|
-
claude:
|
|
17
|
-
codex:
|
|
18
|
-
cursor:
|
|
17
|
+
claude: "claude-code",
|
|
18
|
+
codex: "codex",
|
|
19
|
+
cursor: "cursor",
|
|
19
20
|
};
|
|
20
21
|
function getToolTitle(toolId, status) {
|
|
21
22
|
const name = TOOL_DISPLAY_NAMES[toolId] ?? toolId;
|
|
22
|
-
if (status ===
|
|
23
|
+
if (status === "thinking")
|
|
23
24
|
return `${name} - 思考中...`;
|
|
24
|
-
if (status ===
|
|
25
|
+
if (status === "error")
|
|
25
26
|
return `${name} - 错误`;
|
|
26
27
|
return name;
|
|
27
28
|
}
|
|
@@ -29,12 +30,12 @@ function getToolTitle(toolId, status) {
|
|
|
29
30
|
const TG_MAX_LENGTH = 4096;
|
|
30
31
|
// 预留给 header 和 note 的空间
|
|
31
32
|
const RESERVED_LENGTH = 150;
|
|
32
|
-
function formatMessage(content, status, note, toolId =
|
|
33
|
+
function formatMessage(content, status, note, toolId = "claude") {
|
|
33
34
|
const icon = STATUS_ICONS[status];
|
|
34
35
|
const title = getToolTitle(toolId, status);
|
|
35
36
|
// 在应用 Markdown 格式时,预处理内容以兼容 Telegram
|
|
36
37
|
let processedContent = content;
|
|
37
|
-
if (status ===
|
|
38
|
+
if (status === "done" || status === "error") {
|
|
38
39
|
processedContent = preprocessMarkdownForTelegram(content);
|
|
39
40
|
}
|
|
40
41
|
// 计算可用内容长度(预留 header 和 note 空间)
|
|
@@ -50,7 +51,7 @@ function formatMessage(content, status, note, toolId = 'claude') {
|
|
|
50
51
|
if (out.length > TG_MAX_LENGTH) {
|
|
51
52
|
const keepLen = TG_MAX_LENGTH - 50;
|
|
52
53
|
const tail = text.slice(text.length - keepLen);
|
|
53
|
-
const lineBreak = tail.indexOf(
|
|
54
|
+
const lineBreak = tail.indexOf("\n");
|
|
54
55
|
const clean = lineBreak > 0 && lineBreak < 200 ? tail.slice(lineBreak + 1) : tail;
|
|
55
56
|
out = `${icon} ${title}\n\n...(前文已省略)...\n${clean}`;
|
|
56
57
|
if (note)
|
|
@@ -60,127 +61,109 @@ function formatMessage(content, status, note, toolId = 'claude') {
|
|
|
60
61
|
}
|
|
61
62
|
function buildStopKeyboard(messageId) {
|
|
62
63
|
return {
|
|
63
|
-
inline_keyboard: [
|
|
64
|
+
inline_keyboard: [
|
|
65
|
+
[{ text: "⏹️ 停止", callback_data: `stop_${messageId}` }],
|
|
66
|
+
],
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
|
-
export async function sendThinkingMessage(chatId, replyToMessageId, toolId =
|
|
69
|
+
export async function sendThinkingMessage(chatId, replyToMessageId, toolId = "claude") {
|
|
67
70
|
const bot = getBot();
|
|
68
71
|
const extra = {};
|
|
69
72
|
if (replyToMessageId) {
|
|
70
|
-
extra.reply_parameters =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
extra.reply_parameters =
|
|
74
|
+
{
|
|
75
|
+
message_id: Number(replyToMessageId),
|
|
76
|
+
};
|
|
73
77
|
}
|
|
74
|
-
|
|
75
|
-
const msg = await bot.telegram.sendMessage(Number(chatId),
|
|
76
|
-
|
|
78
|
+
const text = formatMessage("正在思考...", "thinking", "请稍候", toolId);
|
|
79
|
+
const msg = await bot.telegram.sendMessage(Number(chatId), text, {
|
|
80
|
+
...extra,
|
|
81
|
+
parse_mode: "Markdown",
|
|
82
|
+
});
|
|
83
|
+
await bot.telegram.editMessageReplyMarkup(Number(chatId), msg.message_id, undefined, buildStopKeyboard(msg.message_id));
|
|
77
84
|
return String(msg.message_id);
|
|
78
85
|
}
|
|
79
86
|
// 检查错误是否可忽略(只忽略真正无害的错误)
|
|
80
87
|
function isIgnorableError(err) {
|
|
81
|
-
if (err && typeof err ===
|
|
88
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
82
89
|
const msg = String(err.message);
|
|
83
90
|
// 只忽略 "not modified" 类错误(内容没有变化)
|
|
84
|
-
return msg.includes(
|
|
91
|
+
return (msg.includes("not modified") || msg.includes("message is not modified"));
|
|
85
92
|
}
|
|
86
93
|
return false;
|
|
87
94
|
}
|
|
88
95
|
// 提取重试延迟时间(秒)
|
|
89
96
|
function extractRetryAfter(err) {
|
|
90
|
-
if (err && typeof err ===
|
|
97
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
91
98
|
const msg = String(err.message);
|
|
92
99
|
const match = msg.match(/retry after (\d+)/);
|
|
93
100
|
if (match)
|
|
94
101
|
return parseInt(match[1], 10);
|
|
95
102
|
}
|
|
96
|
-
if (err && typeof err ===
|
|
103
|
+
if (err && typeof err === "object" && "parameters" in err) {
|
|
97
104
|
const params = err.parameters;
|
|
98
105
|
if (params?.retry_after)
|
|
99
106
|
return params.retry_after;
|
|
100
107
|
}
|
|
101
108
|
return null;
|
|
102
109
|
}
|
|
103
|
-
export async function updateMessage(chatId, messageId, content, status, note, toolId =
|
|
110
|
+
export async function updateMessage(chatId, messageId, content, status, note, toolId = "claude") {
|
|
111
|
+
const formatted = formatMessage(content, status, note, toolId);
|
|
112
|
+
if (lastSentByMsg.get(messageId) === formatted)
|
|
113
|
+
return;
|
|
114
|
+
lastSentByMsg.set(messageId, formatted);
|
|
104
115
|
const bot = getBot();
|
|
105
116
|
const opts = {};
|
|
106
|
-
if (status ===
|
|
117
|
+
if (status === "thinking" || status === "streaming") {
|
|
107
118
|
opts.reply_markup = buildStopKeyboard(Number(messageId));
|
|
108
119
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
opts.reply_markup = { inline_keyboard: [] };
|
|
112
|
-
// 添加日志,帮助诊断
|
|
113
|
-
log.info(`Updating message to ${status} status (removing stop button) for ${chatId}:${messageId}, content length: ${content.length}`);
|
|
120
|
+
try {
|
|
121
|
+
await bot.telegram.editMessageText(Number(chatId), Number(messageId), undefined, formatted, { ...opts, parse_mode: "Markdown" });
|
|
114
122
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
while (retries <= maxRetries) {
|
|
122
|
-
try {
|
|
123
|
-
await bot.telegram.editMessageText(Number(chatId), Number(messageId), undefined, formatMessage(content, status, note, toolId), { ...opts, parse_mode: shouldParseMarkdown ? 'Markdown' : undefined });
|
|
124
|
-
return;
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (err &&
|
|
125
|
+
typeof err === "object" &&
|
|
126
|
+
"message" in err &&
|
|
127
|
+
String(err.message).includes("not modified")) {
|
|
128
|
+
/* ignore */
|
|
125
129
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// 忽略这些错误,不需要重试
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const retryAfter = extractRetryAfter(err);
|
|
132
|
-
if (retryAfter !== null && retries < maxRetries) {
|
|
133
|
-
// 429 错误,使用 Telegram 返回的实际等待时间
|
|
134
|
-
// 添加额外的缓冲时间(10%),确保不会立即再次触发限制
|
|
135
|
-
const delayMs = Math.ceil(retryAfter * 1000 * 1.1);
|
|
136
|
-
log.warn(`Rate limited, waiting ${delayMs}ms (${retryAfter}s + 10% buffer) before retry (${retries + 1}/${maxRetries})`);
|
|
137
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
138
|
-
retries++;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
// 对于非 429 错误,使用指数退避
|
|
142
|
-
if (retries < maxRetries) {
|
|
143
|
-
const delayMs = Math.pow(2, retries) * 1000; // 1s, 2s, 4s
|
|
144
|
-
log.warn(`Temporary error, waiting ${delayMs}ms before retry (${retries + 1}/${maxRetries}):`, err);
|
|
145
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
146
|
-
retries++;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (retries >= maxRetries) {
|
|
150
|
-
log.error('Failed to update message after retries:', err);
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
log.error('Failed to update message:', err);
|
|
154
|
-
}
|
|
155
|
-
return;
|
|
130
|
+
else {
|
|
131
|
+
log.error("Failed to update message:", err);
|
|
156
132
|
}
|
|
157
133
|
}
|
|
134
|
+
if (status === "done" || status === "error") {
|
|
135
|
+
lastSentByMsg.delete(messageId);
|
|
136
|
+
}
|
|
158
137
|
}
|
|
159
|
-
export async function sendFinalMessages(chatId, messageId, fullContent, note, toolId =
|
|
138
|
+
export async function sendFinalMessages(chatId, messageId, fullContent, note, toolId = "claude") {
|
|
160
139
|
const parts = splitLongContent(fullContent, MAX_TELEGRAM_MESSAGE_LENGTH);
|
|
161
|
-
await updateMessage(chatId, messageId, parts[0],
|
|
140
|
+
await updateMessage(chatId, messageId, parts[0], "done", note, toolId);
|
|
162
141
|
const bot = getBot();
|
|
163
142
|
for (let i = 1; i < parts.length; i++) {
|
|
164
143
|
try {
|
|
165
|
-
await bot.telegram.sendMessage(Number(chatId), formatMessage(parts[i],
|
|
144
|
+
await bot.telegram.sendMessage(Number(chatId), formatMessage(parts[i], "done", `(续 ${i + 1}/${parts.length}) ${note}`, toolId), { parse_mode: "Markdown" });
|
|
166
145
|
}
|
|
167
146
|
catch (err) {
|
|
168
|
-
log.error(
|
|
147
|
+
log.error("Failed to send continuation:", err);
|
|
169
148
|
}
|
|
170
149
|
}
|
|
171
150
|
}
|
|
172
151
|
export async function sendTextReply(chatId, text) {
|
|
173
152
|
const bot = getBot();
|
|
174
153
|
try {
|
|
175
|
-
await bot.telegram.sendMessage(Number(chatId), text, {
|
|
154
|
+
await bot.telegram.sendMessage(Number(chatId), text, {
|
|
155
|
+
parse_mode: "Markdown",
|
|
156
|
+
});
|
|
176
157
|
}
|
|
177
158
|
catch (err) {
|
|
178
|
-
log.error(
|
|
159
|
+
log.error("Failed to send text:", err);
|
|
179
160
|
}
|
|
180
161
|
}
|
|
181
162
|
export async function sendImageReply(chatId, imagePath) {
|
|
182
163
|
const bot = getBot();
|
|
183
|
-
await bot.telegram.sendPhoto(Number(chatId), {
|
|
164
|
+
await bot.telegram.sendPhoto(Number(chatId), {
|
|
165
|
+
source: createReadStream(imagePath),
|
|
166
|
+
});
|
|
184
167
|
}
|
|
185
168
|
/**
|
|
186
169
|
* 发送目录选择界面
|
|
@@ -189,20 +172,20 @@ export async function sendDirectorySelection(chatId, currentDir, userId) {
|
|
|
189
172
|
const bot = getBot();
|
|
190
173
|
const directories = listDirectories(currentDir);
|
|
191
174
|
if (directories.length === 0) {
|
|
192
|
-
await bot.telegram.sendMessage(Number(chatId), `📁 当前目录: \`${currentDir}\`\n\n没有可访问的子目录`, { parse_mode:
|
|
175
|
+
await bot.telegram.sendMessage(Number(chatId), `📁 当前目录: \`${currentDir}\`\n\n没有可访问的子目录`, { parse_mode: "Markdown" });
|
|
193
176
|
return;
|
|
194
177
|
}
|
|
195
178
|
const keyboard = buildDirectoryKeyboard(directories, userId);
|
|
196
179
|
const dirName = basename(currentDir) || currentDir;
|
|
197
180
|
await bot.telegram.sendMessage(Number(chatId), `📁 当前目录: \`${dirName}\`\n\n选择要切换到的目录:`, {
|
|
198
|
-
parse_mode:
|
|
181
|
+
parse_mode: "Markdown",
|
|
199
182
|
reply_markup: keyboard,
|
|
200
183
|
});
|
|
201
184
|
}
|
|
202
185
|
export function startTypingLoop(chatId) {
|
|
203
186
|
const bot = getBot();
|
|
204
187
|
const interval = setInterval(() => {
|
|
205
|
-
bot.telegram.sendChatAction(Number(chatId),
|
|
188
|
+
bot.telegram.sendChatAction(Number(chatId), "typing").catch(() => { });
|
|
206
189
|
}, 4000);
|
|
207
190
|
return () => clearInterval(interval);
|
|
208
191
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wu529778790/open-im",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
18
|
"dev": "tsx src/index.ts",
|
|
19
|
-
"run": "node dist/index.js",
|
|
20
19
|
"start": "node dist/cli.js start",
|
|
21
20
|
"stop": "node dist/cli.js stop",
|
|
21
|
+
"foreground": "node dist/cli.js dev",
|
|
22
22
|
"setup": "node dist/index.js --setup-only",
|
|
23
23
|
"test": "npm run build",
|
|
24
24
|
"prepublishOnly": "npm run build"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"url": "https://github.com/wu529778790/open-im/issues"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"
|
|
44
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
45
45
|
"prompts": "^2.4.2",
|
|
46
46
|
"telegraf": "^4.16.3"
|
|
47
47
|
},
|
package/.env.example
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Telegram Bot Token (from @BotFather)
|
|
2
|
-
TELEGRAM_BOT_TOKEN=
|
|
3
|
-
|
|
4
|
-
# Allowed user IDs (comma-separated, empty = allow all in dev)
|
|
5
|
-
ALLOWED_USER_IDS=
|
|
6
|
-
|
|
7
|
-
# AI tool: claude | codex | cursor
|
|
8
|
-
AI_COMMAND=claude
|
|
9
|
-
|
|
10
|
-
# Claude CLI (when AI_COMMAND=claude)
|
|
11
|
-
CLAUDE_CLI_PATH=claude
|
|
12
|
-
CLAUDE_WORK_DIR=.
|
|
13
|
-
CLAUDE_SKIP_PERMISSIONS=true
|
|
14
|
-
CLAUDE_TIMEOUT_MS=600000
|
|
15
|
-
CLAUDE_MODEL=
|
|
16
|
-
ALLOWED_BASE_DIRS=
|