feat: chatbot mode — Claude CLI отвечает на сообщения в Telegram

Добавлен режим чат-бота: каждое текстовое сообщение в Telegram
обрабатывается через `claude -p` CLI с контекстом проекта (CLAUDE.md).
Поддержка продолжения диалога через --resume session_id.

Новое:
- src/chatbot.ts: модуль чат-бота (spawn, сессии, retry, split)
- Команды /chatbot (статус) и /chatreset (сброс диалога)
- Конфиг через CHATBOT_* переменные в .env
- Typing-индикатор, блокировка конкурентных запросов
- Безопасная отправка Markdown с fallback на plain text

Изменения в index.ts:
- Интеграция chatbot в bot.on('text') fallback
- handlerTimeout увеличен до 5 мин
- Очистка stale-сессий при отсутствии запущенного Claude

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-19 13:43:53 +00:00
parent e26d94dcc2
commit dc92c7fcf6
2 changed files with 388 additions and 14 deletions

View File

@@ -23,10 +23,19 @@ import {
loadProjects,
validateProjectPath
} from './project-registry.js';
import {
handleChatbotMessage,
initChatbot,
isChatbotEnabled,
resetChatbotSession,
getChatbotStatus
} from './chatbot.js';
dotenv.config();
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!, {
handlerTimeout: 300_000, // 5 min for chatbot responses
});
const app = express();
const PORT = parseInt(process.env.PORT || '3456');
const HOST = process.env.HOST || 'localhost';
@@ -78,6 +87,9 @@ setInterval(() => {
}
}, 5 * 60 * 1000); // Check every 5 minutes
// Initialize chatbot module
initChatbot();
app.use(express.json());
// Save chat ID to .env file
@@ -125,6 +137,10 @@ bot.command('status', async (ctx) => {
bot.command('help', async (ctx) => {
await ctx.reply(
'*Claude Telegram Bridge - Commands*\n\n' +
'*Chatbot:*\n' +
'• Send any message — Claude ответит как чат-бот\n' +
'`/chatbot` - Статус чат-бота\n' +
'`/chatreset` - Сброс диалога\n\n' +
'*Session Management:*\n' +
'`/sessions` - List active Claude sessions\n' +
'`/queue` - View queued messages\n\n' +
@@ -139,12 +155,11 @@ bot.command('help', async (ctx) => {
'`/status` - Check bridge status\n' +
'`/test` - Send test notification\n\n' +
'*How it works:*\n' +
'• Send any message - forwards to active Claude\n' +
'• Send any message — chatbot responds via Claude CLI\n' +
'• Target specific project: `ProjectName: message`\n' +
'• Messages show context: 📁 ProjectName [#abc1234]\n' +
'• Register projects for remote spawning\n' +
'• Messages queue when projects are offline\n\n' +
'More info: See README in bridge folder',
'• Messages queue when projects are offline',
{ parse_mode: 'Markdown' }
);
});
@@ -153,6 +168,31 @@ bot.command('test', async (ctx) => {
await ctx.reply('✅ Test notification received! Bridge is working.');
});
bot.command('chatbot', async (ctx) => {
const status = getChatbotStatus(ctx.chat.id.toString());
const enabledText = status.enabled ? '✅ Включен' : '⛔ Выключен';
const sessionText = status.hasSession
? `🗣 Активная сессия: ${status.messageCount} сообщений, последнее ${status.sessionAge} мин назад`
: '📭 Нет активной сессии';
await ctx.reply(
`*Chatbot Status*\n\n` +
`${enabledText}\n` +
`${sessionText}\n\n` +
`Сброс диалога: /chatreset`,
{ parse_mode: 'Markdown' }
);
});
bot.command('chatreset', async (ctx) => {
const deleted = resetChatbotSession(ctx.chat.id.toString());
if (deleted) {
await ctx.reply('🔄 Диалог сброшен. Следующее сообщение начнёт новую сессию.');
} else {
await ctx.reply('📭 Нет активной сессии для сброса.');
}
});
bot.command('sessions', async (ctx) => {
const sessions = Array.from(activeSessions.values());
@@ -533,24 +573,52 @@ bot.on('text', async (ctx) => {
// No project specified - check if we should auto-route to last session
if (lastMessageSession && activeSessions.has(lastMessageSession)) {
const session = activeSessions.get(lastMessageSession)!;
messageQueue.push({
from,
const claudeRunning = isClaudeRunning(session.projectName);
if (claudeRunning) {
messageQueue.push({
from,
message,
timestamp: new Date(),
read: false,
sessionId: lastMessageSession
});
await ctx.reply(`💬 Auto-routed to: 📁 *${session.projectName}* [#${lastMessageSession.substring(0, 7)}]`, { parse_mode: 'Markdown' });
console.log(`📥 Auto-routed to ${session.projectName}`);
return;
}
// Claude not actually running — clean up stale session
console.log(`[CLEANUP] Removing stale lastMessageSession for ${session.projectName}`);
activeSessions.delete(lastMessageSession);
lastMessageSession = null;
}
// No active project session — use chatbot if enabled
if (isChatbotEnabled()) {
const userChatId = ctx.chat.id.toString();
console.log(`🤖 Chatbot handling message from ${from}: "${message.substring(0, 50)}..."`);
// Fire-and-forget: don't block Telegraf's handler
handleChatbotMessage(
userChatId,
message,
timestamp: new Date(),
read: false,
sessionId: lastMessageSession
});
await ctx.reply(`💬 Auto-routed to: 📁 *${session.projectName}* [#${lastMessageSession.substring(0, 7)}]`, { parse_mode: 'Markdown' });
console.log(`📥 Auto-routed to ${session.projectName}`);
async () => {
await ctx.sendChatAction('typing');
},
async (text: string, parseMode?: string) => {
await ctx.reply(text, parseMode ? { parse_mode: parseMode as any } : {});
}
).catch(err => console.error('[Chatbot] Async error:', err));
} else {
// No recent session - add to general message queue
// Fallback: queue the message (original behavior)
messageQueue.push({
from,
message,
timestamp: new Date(),
read: false
});
await ctx.reply('💬 Message received - responding...');
await ctx.reply('💬 Message received - queued for processing.');
console.log('📥 Queued for Claude to process');
}
});