@wu529778790/open-im 0.1.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/.env.example +16 -0
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/access/access-control.d.ts +5 -0
- package/dist/access/access-control.js +11 -0
- package/dist/adapters/claude-adapter.d.ts +7 -0
- package/dist/adapters/claude-adapter.js +17 -0
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +11 -0
- package/dist/adapters/tool-adapter.interface.d.ts +35 -0
- package/dist/adapters/tool-adapter.interface.js +4 -0
- package/dist/claude/cli-runner.d.ts +28 -0
- package/dist/claude/cli-runner.js +166 -0
- package/dist/claude/stream-parser.d.ts +17 -0
- package/dist/claude/stream-parser.js +50 -0
- package/dist/claude/types.d.ts +54 -0
- package/dist/claude/types.js +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6 -0
- package/dist/commands/handler.d.ts +30 -0
- package/dist/commands/handler.js +122 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +88 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +15 -0
- package/dist/hook/permission-server.d.ts +4 -0
- package/dist/hook/permission-server.js +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +76 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +65 -0
- package/dist/queue/request-queue.d.ts +6 -0
- package/dist/queue/request-queue.js +43 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +11 -0
- package/dist/session/session-manager.d.ts +26 -0
- package/dist/session/session-manager.js +207 -0
- package/dist/setup.d.ts +5 -0
- package/dist/setup.js +97 -0
- package/dist/shared/active-chats.d.ts +4 -0
- package/dist/shared/active-chats.js +55 -0
- package/dist/shared/ai-task.d.ts +42 -0
- package/dist/shared/ai-task.js +167 -0
- package/dist/shared/message-dedup.d.ts +4 -0
- package/dist/shared/message-dedup.js +25 -0
- package/dist/shared/task-cleanup.d.ts +2 -0
- package/dist/shared/task-cleanup.js +19 -0
- package/dist/shared/types.d.ts +9 -0
- package/dist/shared/types.js +1 -0
- package/dist/shared/utils.d.ts +7 -0
- package/dist/shared/utils.js +72 -0
- package/dist/telegram/client.d.ts +6 -0
- package/dist/telegram/client.js +27 -0
- package/dist/telegram/event-handler.d.ts +8 -0
- package/dist/telegram/event-handler.js +174 -0
- package/dist/telegram/message-sender.d.ts +7 -0
- package/dist/telegram/message-sender.js +102 -0
- package/package.json +52 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const TOOL_EMOJIS = {
|
|
2
|
+
Read: 'š', Write: 'āļø', Edit: 'š', Bash: 'š»', Glob: 'š', Grep: 'š',
|
|
3
|
+
WebFetch: 'š', WebSearch: 'š', Task: 'š', TodoRead: 'š', TodoWrite: 'ā
',
|
|
4
|
+
};
|
|
5
|
+
function getToolEmoji(name) {
|
|
6
|
+
return TOOL_EMOJIS[name] ?? 'š§';
|
|
7
|
+
}
|
|
8
|
+
export function truncateText(text, maxLen) {
|
|
9
|
+
if (text.length <= maxLen)
|
|
10
|
+
return text;
|
|
11
|
+
const keepLen = maxLen - 20;
|
|
12
|
+
const tail = text.slice(text.length - keepLen);
|
|
13
|
+
const lineBreak = tail.indexOf('\n');
|
|
14
|
+
const clean = lineBreak > 0 && lineBreak < 200 ? tail.slice(lineBreak + 1) : tail;
|
|
15
|
+
return `...(åęå·²ēē„)...\n${clean}`;
|
|
16
|
+
}
|
|
17
|
+
export function splitLongContent(text, maxLen) {
|
|
18
|
+
if (text.length <= maxLen)
|
|
19
|
+
return [text];
|
|
20
|
+
const parts = [];
|
|
21
|
+
let start = 0;
|
|
22
|
+
while (start < text.length) {
|
|
23
|
+
let end = start + maxLen;
|
|
24
|
+
if (end >= text.length) {
|
|
25
|
+
parts.push(text.slice(start));
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
const lastNewline = text.lastIndexOf('\n', end);
|
|
29
|
+
if (lastNewline > start)
|
|
30
|
+
end = lastNewline + 1;
|
|
31
|
+
parts.push(text.slice(start, end));
|
|
32
|
+
start = end;
|
|
33
|
+
}
|
|
34
|
+
return parts;
|
|
35
|
+
}
|
|
36
|
+
export function formatToolStats(toolStats, numTurns) {
|
|
37
|
+
const total = Object.values(toolStats).reduce((a, b) => a + b, 0);
|
|
38
|
+
if (total === 0)
|
|
39
|
+
return '';
|
|
40
|
+
const parts = Object.entries(toolStats)
|
|
41
|
+
.sort((a, b) => b[1] - a[1])
|
|
42
|
+
.map(([name, count]) => `${getToolEmoji(name)}${name}Ć${count}`)
|
|
43
|
+
.join(' ');
|
|
44
|
+
return `${numTurns > 0 ? numTurns + ' č½® ' : ''}${total} 欔巄å
·ļ¼${parts}ļ¼`;
|
|
45
|
+
}
|
|
46
|
+
export function formatToolCallNotification(toolName, toolInput) {
|
|
47
|
+
const emoji = getToolEmoji(toolName);
|
|
48
|
+
if (!toolInput)
|
|
49
|
+
return `${emoji} ${toolName}`;
|
|
50
|
+
let detail = '';
|
|
51
|
+
if (toolName === 'Bash' && toolInput.command)
|
|
52
|
+
detail = ` ā ${String(toolInput.command).slice(0, 60)}`;
|
|
53
|
+
if (toolName === 'Read' && toolInput.file_path)
|
|
54
|
+
detail = ` ā ${toolInput.file_path}`;
|
|
55
|
+
if (toolName === 'Write' && toolInput.file_path)
|
|
56
|
+
detail = ` ā ${toolInput.file_path}`;
|
|
57
|
+
return `${emoji} ${toolName}${detail}`;
|
|
58
|
+
}
|
|
59
|
+
export function trackCost(userCosts, userId, cost, durationMs) {
|
|
60
|
+
const r = userCosts.get(userId) ?? { totalCost: 0, totalDurationMs: 0, requestCount: 0 };
|
|
61
|
+
r.totalCost += cost;
|
|
62
|
+
r.totalDurationMs += durationMs;
|
|
63
|
+
r.requestCount += 1;
|
|
64
|
+
userCosts.set(userId, r);
|
|
65
|
+
}
|
|
66
|
+
export function getContextWarning(totalTurns) {
|
|
67
|
+
if (totalTurns >= 12)
|
|
68
|
+
return 'ā ļø äøäøęč¾éæļ¼å»ŗč®® /new å¼å§ę°ä¼čÆ';
|
|
69
|
+
if (totalTurns >= 8)
|
|
70
|
+
return `š” 对čÆå·² ${totalTurns} č½®ļ¼åÆēØ /compact å缩`;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Telegraf } from 'telegraf';
|
|
2
|
+
import type { Config } from '../config.js';
|
|
3
|
+
export declare function getBot(): Telegraf;
|
|
4
|
+
export declare function getBotUsername(): string | undefined;
|
|
5
|
+
export declare function initTelegram(config: Config, setupHandlers: (bot: Telegraf) => void): Promise<void>;
|
|
6
|
+
export declare function stopTelegram(): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Telegraf } from 'telegraf';
|
|
2
|
+
import { createLogger } from '../logger.js';
|
|
3
|
+
const log = createLogger('Telegram');
|
|
4
|
+
let bot;
|
|
5
|
+
let botUsername;
|
|
6
|
+
export function getBot() {
|
|
7
|
+
if (!bot)
|
|
8
|
+
throw new Error('Telegram bot not initialized');
|
|
9
|
+
return bot;
|
|
10
|
+
}
|
|
11
|
+
export function getBotUsername() {
|
|
12
|
+
return botUsername;
|
|
13
|
+
}
|
|
14
|
+
export async function initTelegram(config, setupHandlers) {
|
|
15
|
+
bot = new Telegraf(config.telegramBotToken);
|
|
16
|
+
setupHandlers(bot);
|
|
17
|
+
const me = (await bot.telegram.getMe());
|
|
18
|
+
botUsername = me.username;
|
|
19
|
+
bot.launch().catch((err) => {
|
|
20
|
+
log.error('Telegram polling error:', err);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
log.info('Telegram bot launched');
|
|
24
|
+
}
|
|
25
|
+
export function stopTelegram() {
|
|
26
|
+
bot?.stop('SIGTERM');
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Telegraf } from 'telegraf';
|
|
2
|
+
import type { Config } from '../config.js';
|
|
3
|
+
import type { SessionManager } from '../session/session-manager.js';
|
|
4
|
+
export interface TelegramEventHandlerHandle {
|
|
5
|
+
stop: () => void;
|
|
6
|
+
getRunningTaskCount: () => number;
|
|
7
|
+
}
|
|
8
|
+
export declare function setupTelegramHandlers(bot: Telegraf, config: Config, sessionManager: SessionManager): TelegramEventHandlerHandle;
|
|
@@ -0,0 +1,174 @@
|
|
|
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');
|
|
17
|
+
async function downloadTelegramPhoto(bot, fileId) {
|
|
18
|
+
await mkdir(IMAGE_DIR, { recursive: true });
|
|
19
|
+
const fileLink = await bot.telegram.getFileLink(fileId);
|
|
20
|
+
const res = await fetch(fileLink.href, { signal: AbortSignal.timeout(30000) });
|
|
21
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
22
|
+
const safeId = fileId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
23
|
+
const imagePath = join(IMAGE_DIR, `${Date.now()}-${safeId.slice(-8)}.jpg`);
|
|
24
|
+
await writeFile(imagePath, buffer);
|
|
25
|
+
return imagePath;
|
|
26
|
+
}
|
|
27
|
+
export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
28
|
+
const accessControl = new AccessControl(config.allowedUserIds);
|
|
29
|
+
const requestQueue = new RequestQueue();
|
|
30
|
+
const userCosts = new Map();
|
|
31
|
+
const runningTasks = new Map();
|
|
32
|
+
const stopTaskCleanup = startTaskCleanup(runningTasks);
|
|
33
|
+
const dedup = new MessageDedup();
|
|
34
|
+
const commandHandler = new CommandHandler({
|
|
35
|
+
config,
|
|
36
|
+
sessionManager,
|
|
37
|
+
requestQueue,
|
|
38
|
+
sender: { sendTextReply },
|
|
39
|
+
userCosts,
|
|
40
|
+
getRunningTasksSize: () => runningTasks.size,
|
|
41
|
+
});
|
|
42
|
+
registerPermissionSender('telegram', {});
|
|
43
|
+
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
44
|
+
const toolAdapter = getAdapter(config.aiCommand);
|
|
45
|
+
if (!toolAdapter) {
|
|
46
|
+
await sendTextReply(chatId, `ęŖé
ē½® AI å·„å
·: ${config.aiCommand}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId) : undefined;
|
|
50
|
+
log.info(`Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
51
|
+
const toolId = config.aiCommand;
|
|
52
|
+
let msgId;
|
|
53
|
+
try {
|
|
54
|
+
msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
log.error('Failed to send thinking message:', err);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const stopTyping = startTypingLoop(chatId);
|
|
61
|
+
const taskKey = `${userId}:${msgId}`;
|
|
62
|
+
await runAITask({ config, sessionManager, userCosts }, { userId, chatId, workDir, sessionId, convId, platform: 'telegram', taskKey }, prompt, toolAdapter, {
|
|
63
|
+
throttleMs: THROTTLE_MS,
|
|
64
|
+
streamUpdate: (content, toolNote) => {
|
|
65
|
+
const note = toolNote ? 'č¾åŗäø...\n' + toolNote : 'č¾åŗäø...';
|
|
66
|
+
updateMessage(chatId, msgId, content, 'streaming', note, toolId).catch(() => { });
|
|
67
|
+
},
|
|
68
|
+
sendComplete: async (content, note) => {
|
|
69
|
+
await sendFinalMessages(chatId, msgId, content, note, toolId);
|
|
70
|
+
},
|
|
71
|
+
sendError: async (error) => {
|
|
72
|
+
await updateMessage(chatId, msgId, `é误ļ¼${error}`, 'error', 'ę§č”失蓄', toolId);
|
|
73
|
+
},
|
|
74
|
+
extraCleanup: () => {
|
|
75
|
+
stopTyping();
|
|
76
|
+
runningTasks.delete(taskKey);
|
|
77
|
+
},
|
|
78
|
+
onTaskReady: (state) => {
|
|
79
|
+
runningTasks.set(taskKey, state);
|
|
80
|
+
},
|
|
81
|
+
sendImage: (path) => sendImageReply(chatId, path),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
bot.on('callback_query', async (ctx) => {
|
|
85
|
+
const query = ctx.callbackQuery;
|
|
86
|
+
if (!('data' in query))
|
|
87
|
+
return;
|
|
88
|
+
const userId = String(ctx.from?.id ?? '');
|
|
89
|
+
const data = query.data;
|
|
90
|
+
if (data.startsWith('stop_')) {
|
|
91
|
+
const messageId = data.replace('stop_', '');
|
|
92
|
+
const taskKey = `${userId}:${messageId}`;
|
|
93
|
+
const taskInfo = runningTasks.get(taskKey);
|
|
94
|
+
if (taskInfo) {
|
|
95
|
+
runningTasks.delete(taskKey);
|
|
96
|
+
taskInfo.settle();
|
|
97
|
+
taskInfo.handle.abort();
|
|
98
|
+
const chatId = String(ctx.chat?.id ?? '');
|
|
99
|
+
await updateMessage(chatId, messageId, taskInfo.latestContent || 'å·²åę¢', 'error', 'ā¹ļø å·²åę¢', config.aiCommand);
|
|
100
|
+
await ctx.answerCbQuery('å·²åę¢ę§č”');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await ctx.answerCbQuery('ä»»å”å·²å®ęęäøååØ');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (data.startsWith('perm_allow_') || data.startsWith('perm_deny_')) {
|
|
107
|
+
const isAllow = data.startsWith('perm_allow_');
|
|
108
|
+
const requestId = data.replace(/^perm_(allow|deny)_/, '');
|
|
109
|
+
const decision = isAllow ? 'allow' : 'deny';
|
|
110
|
+
resolvePermissionById(requestId, decision);
|
|
111
|
+
await ctx.answerCbQuery(isAllow ? 'ā
å·²å
许' : 'ā å·²ęē»');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
bot.on(message('text'), async (ctx) => {
|
|
115
|
+
const chatId = String(ctx.chat.id);
|
|
116
|
+
const userId = String(ctx.from.id);
|
|
117
|
+
const messageId = String(ctx.message.message_id);
|
|
118
|
+
let text = ctx.message.text.trim();
|
|
119
|
+
if (dedup.isDuplicate(`${chatId}:${messageId}`))
|
|
120
|
+
return;
|
|
121
|
+
if (!accessControl.isAllowed(userId)) {
|
|
122
|
+
await sendTextReply(chatId, 'ę±ęļ¼ęØę²”ę访é®ęéć\nęØē ID: ' + userId);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setActiveChatId('telegram', chatId);
|
|
126
|
+
if (await commandHandler.dispatch(text, chatId, userId, 'telegram', handleAIRequest)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const workDir = sessionManager.getWorkDir(userId);
|
|
130
|
+
const convId = sessionManager.getConvId(userId);
|
|
131
|
+
const enqueueResult = requestQueue.enqueue(userId, convId, text, async (prompt) => {
|
|
132
|
+
await handleAIRequest(userId, chatId, prompt, workDir, convId, undefined, messageId);
|
|
133
|
+
});
|
|
134
|
+
if (enqueueResult === 'rejected') {
|
|
135
|
+
await sendTextReply(chatId, '请ę±éå已滔ļ¼čÆ·ēØååčÆć');
|
|
136
|
+
}
|
|
137
|
+
else if (enqueueResult === 'queued') {
|
|
138
|
+
await sendTextReply(chatId, 'ęØē请ę±å·²ęéēå¾
ć');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
bot.on(message('photo'), async (ctx) => {
|
|
142
|
+
const chatId = String(ctx.chat.id);
|
|
143
|
+
const userId = String(ctx.from.id);
|
|
144
|
+
const caption = ctx.message.caption?.trim() || '';
|
|
145
|
+
if (dedup.isDuplicate(`${chatId}:${ctx.message.message_id}`))
|
|
146
|
+
return;
|
|
147
|
+
if (!accessControl.isAllowed(userId))
|
|
148
|
+
return;
|
|
149
|
+
setActiveChatId('telegram', chatId);
|
|
150
|
+
const photos = ctx.message.photo;
|
|
151
|
+
const largest = photos[photos.length - 1];
|
|
152
|
+
let imagePath;
|
|
153
|
+
try {
|
|
154
|
+
imagePath = await downloadTelegramPhoto(bot, largest.file_id);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
log.error('Failed to download photo:', err);
|
|
158
|
+
await sendTextReply(chatId, 'å¾ēäøč½½å¤±č“„ć');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const prompt = caption
|
|
162
|
+
? `ēØę·åéäŗäøå¼ å¾ēļ¼éčØļ¼${caption}ļ¼ļ¼å·²äæåå° ${imagePath}ćčÆ·ēØ Read å·„å
·ę„ēå¹¶åęć`
|
|
163
|
+
: `ēØę·åéäŗäøå¼ å¾ēļ¼å·²äæåå° ${imagePath}ćčÆ·ēØ Read å·„å
·ę„ēå¹¶åęć`;
|
|
164
|
+
const workDir = sessionManager.getWorkDir(userId);
|
|
165
|
+
const convId = sessionManager.getConvId(userId);
|
|
166
|
+
requestQueue.enqueue(userId, convId, prompt, async (p) => {
|
|
167
|
+
await handleAIRequest(userId, chatId, p, workDir, convId);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
stop: () => stopTaskCleanup(),
|
|
172
|
+
getRunningTaskCount: () => runningTasks.size,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type MessageStatus = 'thinking' | 'streaming' | 'done' | 'error';
|
|
2
|
+
export declare function sendThinkingMessage(chatId: string, replyToMessageId?: string, toolId?: string): Promise<string>;
|
|
3
|
+
export declare function updateMessage(chatId: string, messageId: string, content: string, status: MessageStatus, note?: string, toolId?: string): Promise<void>;
|
|
4
|
+
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
5
|
+
export declare function sendTextReply(chatId: string, text: string): Promise<void>;
|
|
6
|
+
export declare function sendImageReply(chatId: string, imagePath: string): Promise<void>;
|
|
7
|
+
export declare function startTypingLoop(chatId: string): () => void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getBot } from './client.js';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { createLogger } from '../logger.js';
|
|
4
|
+
import { splitLongContent, truncateText } from '../shared/utils.js';
|
|
5
|
+
import { MAX_TELEGRAM_MESSAGE_LENGTH } from '../constants.js';
|
|
6
|
+
const log = createLogger('TgSender');
|
|
7
|
+
const STATUS_ICONS = {
|
|
8
|
+
thinking: 'šµ',
|
|
9
|
+
streaming: 'šµ',
|
|
10
|
+
done: 'š¢',
|
|
11
|
+
error: 'š“',
|
|
12
|
+
};
|
|
13
|
+
const TOOL_DISPLAY_NAMES = {
|
|
14
|
+
claude: 'claude-code',
|
|
15
|
+
codex: 'codex',
|
|
16
|
+
cursor: 'cursor',
|
|
17
|
+
};
|
|
18
|
+
function getToolTitle(toolId, status) {
|
|
19
|
+
const name = TOOL_DISPLAY_NAMES[toolId] ?? toolId;
|
|
20
|
+
if (status === 'thinking')
|
|
21
|
+
return `${name} - ęčäø...`;
|
|
22
|
+
if (status === 'error')
|
|
23
|
+
return `${name} - é误`;
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
function formatMessage(content, status, note, toolId = 'claude') {
|
|
27
|
+
const icon = STATUS_ICONS[status];
|
|
28
|
+
const title = getToolTitle(toolId, status);
|
|
29
|
+
const text = truncateText(content, MAX_TELEGRAM_MESSAGE_LENGTH);
|
|
30
|
+
let out = `${icon} ${title}\n\n${text}`;
|
|
31
|
+
if (note)
|
|
32
|
+
out += `\n\nāāāāāāāāā\n${note}`;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function buildStopKeyboard(messageId) {
|
|
36
|
+
return {
|
|
37
|
+
inline_keyboard: [[{ text: 'ā¹ļø åę¢', callback_data: `stop_${messageId}` }]],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function sendThinkingMessage(chatId, replyToMessageId, toolId = 'claude') {
|
|
41
|
+
const bot = getBot();
|
|
42
|
+
const extra = {};
|
|
43
|
+
if (replyToMessageId) {
|
|
44
|
+
extra.reply_parameters = {
|
|
45
|
+
message_id: Number(replyToMessageId),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const msg = await bot.telegram.sendMessage(Number(chatId), formatMessage('ę£åØęč...', 'thinking', '请ēØå', toolId), { ...extra, parse_mode: 'Markdown' });
|
|
49
|
+
await bot.telegram.editMessageText(Number(chatId), msg.message_id, undefined, formatMessage('ę£åØęč...', 'thinking', '请ēØå', toolId), { reply_markup: buildStopKeyboard(msg.message_id), parse_mode: 'Markdown' });
|
|
50
|
+
return String(msg.message_id);
|
|
51
|
+
}
|
|
52
|
+
export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
|
|
53
|
+
const bot = getBot();
|
|
54
|
+
const opts = {};
|
|
55
|
+
if (status === 'thinking' || status === 'streaming') {
|
|
56
|
+
opts.reply_markup = buildStopKeyboard(Number(messageId));
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await bot.telegram.editMessageText(Number(chatId), Number(messageId), undefined, formatMessage(content, status, note, toolId), { ...opts, parse_mode: 'Markdown' });
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err && typeof err === 'object' && 'message' in err && String(err.message).includes('not modified')) {
|
|
63
|
+
/* ignore */
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
log.error('Failed to update message:', err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function sendFinalMessages(chatId, messageId, fullContent, note, toolId = 'claude') {
|
|
71
|
+
const parts = splitLongContent(fullContent, MAX_TELEGRAM_MESSAGE_LENGTH);
|
|
72
|
+
await updateMessage(chatId, messageId, parts[0], 'done', note, toolId);
|
|
73
|
+
const bot = getBot();
|
|
74
|
+
for (let i = 1; i < parts.length; i++) {
|
|
75
|
+
try {
|
|
76
|
+
await bot.telegram.sendMessage(Number(chatId), formatMessage(parts[i], 'done', `(ē» ${i + 1}/${parts.length}) ${note}`, toolId), { parse_mode: 'Markdown' });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
log.error('Failed to send continuation:', err);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function sendTextReply(chatId, text) {
|
|
84
|
+
const bot = getBot();
|
|
85
|
+
try {
|
|
86
|
+
await bot.telegram.sendMessage(Number(chatId), text, { parse_mode: 'Markdown' });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
log.error('Failed to send text:', err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function sendImageReply(chatId, imagePath) {
|
|
93
|
+
const bot = getBot();
|
|
94
|
+
await bot.telegram.sendPhoto(Number(chatId), { source: createReadStream(imagePath) });
|
|
95
|
+
}
|
|
96
|
+
export function startTypingLoop(chatId) {
|
|
97
|
+
const bot = getBot();
|
|
98
|
+
const interval = setInterval(() => {
|
|
99
|
+
bot.telegram.sendChatAction(Number(chatId), 'typing').catch(() => { });
|
|
100
|
+
}, 4000);
|
|
101
|
+
return () => clearInterval(interval);
|
|
102
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wu529778790/open-im",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"open-im": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
".env.example"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"setup": "node dist/index.js --setup-only",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"telegram",
|
|
23
|
+
"feishu",
|
|
24
|
+
"claude",
|
|
25
|
+
"claude-code",
|
|
26
|
+
"bot",
|
|
27
|
+
"bridge"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/wu529778790/open-im.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/wu529778790/open-im#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/wu529778790/open-im/issues"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"prompts": "^2.4.2",
|
|
40
|
+
"telegraf": "^4.16.3"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.0.0",
|
|
44
|
+
"@types/prompts": "^2.4.9",
|
|
45
|
+
"dotenv": "^16.0.0",
|
|
46
|
+
"tsx": "^4.0.0",
|
|
47
|
+
"typescript": "^5.0.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
}
|
|
52
|
+
}
|