@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.
@@ -1,25 +1,25 @@
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 { 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');
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 = THROTTLE_MS;
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, { signal: AbortSignal.timeout(30000) });
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.allowedUserIds);
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('telegram', {});
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 ? sessionManager.getSessionIdForConv(userId, convId) : undefined;
91
- log.info(`Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
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('Failed to send thinking message:', err);
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 = '\n\n─────────\n\n';
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 = content.length > STREAM_PREVIEW_LENGTH
176
- ? `...\n\n${content.slice(-STREAM_PREVIEW_LENGTH)}`
177
- : content;
179
+ displayContent =
180
+ content.length > STREAM_PREVIEW_LENGTH
181
+ ? `...\n\n${content.slice(-STREAM_PREVIEW_LENGTH)}`
182
+ : content;
178
183
  }
179
- const note = toolNote ? '输出中...\n' + toolNote : '输出中...';
180
- await updateMessage(chatId, msgId, displayContent, 'streaming', note, toolId);
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 }, { userId, chatId, workDir, sessionId, convId, platform: 'telegram', taskKey }, prompt, toolAdapter, {
236
- throttleMs: THROTTLE_MS,
237
- streamUpdate: streamUpdateWrapper,
238
- onThinkingToText: (content) => {
239
- // 从思考转到文本输出时,标记有思考内容
240
- // 注意:此时不保存文本内容,因为后续会通过 streamUpdate 持续更新
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('Failed to send complete message:', err);
262
+ log.error("Failed to send complete message:", err);
251
263
  // 如果发送失败,至少尝试更新状态
252
- await updateMessage(chatId, msgId, content, 'done', note, toolId);
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}`, 'error', '执行失败', toolId);
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('callback_query', async (ctx) => {
285
+ bot.on("callback_query", async (ctx) => {
274
286
  const query = ctx.callbackQuery;
275
- if (!('data' in query))
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('stop_')) {
280
- const messageId = data.replace('stop_', '');
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 || '已停止', 'error', '⏹️ 已停止', config.aiCommand);
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('perm_allow_') || data.startsWith('perm_deny_')) {
296
- const isAllow = data.startsWith('perm_allow_');
297
- const requestId = data.replace(/^perm_(allow|deny)_/, '');
298
- const decision = isAllow ? 'allow' : 'deny';
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('text'), async (ctx) => {
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, '抱歉,您没有访问权限。\n您的 ID: ' + userId);
324
+ await sendTextReply(chatId, "抱歉,您没有访问权限。\n您的 ID: " + userId);
312
325
  return;
313
326
  }
314
- setActiveChatId('telegram', chatId);
315
- if (await commandHandler.dispatch(text, chatId, userId, 'telegram', handleAIRequest)) {
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 === 'rejected') {
324
- await sendTextReply(chatId, '请求队列已满,请稍后再试。');
336
+ if (enqueueResult === "rejected") {
337
+ await sendTextReply(chatId, "请求队列已满,请稍后再试。");
325
338
  }
326
- else if (enqueueResult === 'queued') {
327
- await sendTextReply(chatId, '您的请求已排队等待。');
339
+ else if (enqueueResult === "queued") {
340
+ await sendTextReply(chatId, "您的请求已排队等待。");
328
341
  }
329
342
  });
330
- bot.on(message('photo'), async (ctx) => {
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('telegram', chatId);
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('Failed to download photo:', err);
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 = 'thinking' | 'streaming' | 'done' | 'error';
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 './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');
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: 'claude-code',
17
- codex: 'codex',
18
- cursor: '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 === 'thinking')
23
+ if (status === "thinking")
23
24
  return `${name} - 思考中...`;
24
- if (status === 'error')
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 = 'claude') {
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 === 'done' || status === 'error') {
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('\n');
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: [[{ text: '⏹️ 停止', callback_data: `stop_${messageId}` }]],
64
+ inline_keyboard: [
65
+ [{ text: "⏹️ 停止", callback_data: `stop_${messageId}` }],
66
+ ],
64
67
  };
65
68
  }
66
- export async function sendThinkingMessage(chatId, replyToMessageId, toolId = 'claude') {
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
- message_id: Number(replyToMessageId),
72
- };
73
+ extra.reply_parameters =
74
+ {
75
+ message_id: Number(replyToMessageId),
76
+ };
73
77
  }
74
- // 初始消息使用纯文本,避免 Markdown 解析问题
75
- const msg = await bot.telegram.sendMessage(Number(chatId), formatMessage('正在思考...', 'thinking', '请稍候', toolId), extra);
76
- await bot.telegram.editMessageText(Number(chatId), msg.message_id, undefined, formatMessage('正在思考...', 'thinking', '请稍候', toolId), { reply_markup: buildStopKeyboard(msg.message_id) });
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 === 'object' && 'message' in 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('not modified') || msg.includes('message is not modified');
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 === 'object' && 'message' in 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 === 'object' && 'parameters' in 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 = 'claude') {
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 === 'thinking' || status === 'streaming') {
117
+ if (status === "thinking" || status === "streaming") {
107
118
  opts.reply_markup = buildStopKeyboard(Number(messageId));
108
119
  }
109
- else if (status === 'done' || status === 'error') {
110
- // 完成或错误时,显式移除停止按钮(使用空的 inline_keyboard)
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
- // 流式输出时使用纯文本,避免 Markdown 解析导致内容减少
116
- // 完成时也暂时使用纯文本,避免 Markdown 解析错误
117
- // TODO: 等待 Markdown 预处理稳定后再启用 Markdown
118
- const shouldParseMarkdown = false; // 暂时禁用 Markdown 解析
119
- let retries = 0;
120
- const maxRetries = 2; // 减少重试次数,但增加等待时间
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
- catch (err) {
127
- if (isIgnorableError(err)) {
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 = 'claude') {
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], 'done', note, toolId);
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], 'done', `(续 ${i + 1}/${parts.length}) ${note}`, toolId), { parse_mode: 'Markdown' });
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('Failed to send continuation:', err);
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, { parse_mode: 'Markdown' });
154
+ await bot.telegram.sendMessage(Number(chatId), text, {
155
+ parse_mode: "Markdown",
156
+ });
176
157
  }
177
158
  catch (err) {
178
- log.error('Failed to send text:', err);
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), { source: createReadStream(imagePath) });
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: 'Markdown' });
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: 'Markdown',
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), 'typing').catch(() => { });
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.12",
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
- "https-proxy-agent": "^7.0.6",
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=