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:
96
src/index.ts
96
src/index.ts
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user