diff --git a/src/index.ts b/src/index.ts index 25f193f..5e3724c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -173,6 +173,23 @@ async function saveChatId(id: string) { } } +// Middleware: only respond in the configured group chat or to allowed users in DMs +const ALLOWED_DM_USERS = ['skosolapov']; + +bot.use(async (ctx, next) => { + const messageChatId = ctx.chat?.id?.toString(); + const username = ctx.from?.username?.toLowerCase(); + const isGroupChat = messageChatId === chatId; + const isAllowedDM = ctx.chat?.type === 'private' && username && ALLOWED_DM_USERS.includes(username); + + if (!isGroupChat && !isAllowedDM) { + console.log(`🚫 Ignored message from chat ${messageChatId}, user ${username}`); + return; + } + + return next(); +}); + // Bot commands bot.start(async (ctx) => { chatId = ctx.chat.id.toString(); @@ -214,6 +231,9 @@ bot.command('help', async (ctx) => { '`/task list` - Список задач\n' + '`/task N` - Статус задачи #N\n' + '`/task cancel N` - Отменить задачу\n\n' + + '*Documents:*\n' + + '`/document list` - Список документов\n' + + '`/document <тема>` - Сгенерировать документ в панели суперадмина\n\n' + '*Session Management:*\n' + '`/sessions` - List active Claude sessions\n' + '`/queue` - View queued messages\n\n' + @@ -296,9 +316,17 @@ bot.command('context', async (ctx) => { }); bot.command('task', async (ctx) => { - const text = ctx.message.text.replace(/^\/task\s*/, '').trim(); + let text = ctx.message.text.replace(/^\/task\s*/, '').trim(); const userChatId = ctx.chat.id.toString(); + // Extract quoted message text if /task is sent as a reply + const replyToMessage = ctx.message.reply_to_message; + if (replyToMessage && 'text' in replyToMessage && replyToMessage.text && text) { + const quotedText = replyToMessage.text; + console.log(`💬 /task reply contains quoted text: "${quotedText.substring(0, 100)}..."`); + text = `[Цитата сообщения на которое отвечает пользователь]:\n${quotedText}\n\n[Ответ пользователя]:\n${text}`; + } + // /task list if (text === 'list' || text === '') { const tasks = await listTasks(); @@ -361,6 +389,66 @@ bot.command('task', async (ctx) => { await ctx.reply(`📋 *Задача #${task.id} создана*\n\nВыполняется в фоне. Уведомлю по завершении.`, { parse_mode: 'Markdown' }); }); +bot.command('document', async (ctx) => { + const text = ctx.message.text.replace(/^\/document\s*/, '').trim(); + const userChatId = ctx.chat.id.toString(); + + // /document list — list available documents + if (text === 'list' || text === '') { + try { + const docsDir = '/home/fitcrm/apps/web-platform-admin/src/app/(dashboard)/document'; + const dirEntries = await fs.readdir(docsDir, { withFileTypes: true }).catch(() => []); + const slugs = dirEntries + .filter((d: any) => d.isDirectory() && !d.name.startsWith('[')) + .map((d: any) => d.name); + + if (slugs.length === 0) { + await ctx.reply( + '*📄 Документы*\n\n📭 Нет документов.\n\nСоздать: `/document описание документа`', + { parse_mode: 'Markdown' }, + ); + } else { + const lines = slugs.map( + (s: string) => `• [${s}](https://platform.myfitcrm.ru/document/${s})`, + ); + await ctx.reply( + `*📄 Документы* (${slugs.length})\n\n${lines.join('\n')}\n\nСоздать новый: \`/document описание\``, + { parse_mode: 'Markdown', link_preview_options: { is_disabled: true } }, + ); + } + } catch { + await ctx.reply('❌ Не удалось прочитать список документов.'); + } + return; + } + + // /document — create new document + const wrappedMessage = [ + `Создай документ-страницу в панели суперадмина на тему: "${text}"`, + '', + 'Инструкции:', + '1. Создай новый файл в apps/web-platform-admin/src/app/(dashboard)/document//page.tsx', + ' где — транслитерация названия документа (kebab-case, латиница).', + '2. Используй как образец файл apps/web-platform-admin/src/app/(dashboard)/document/dev-status/page.tsx', + '3. Документ ОБЯЗАТЕЛЬНО должен иметь три кнопки: "Скачать PDF" (jspdf + html2canvas-pro), "Скачать Excel" (xlsx), "Печать" (window.print()).', + ' Все три библиотеки уже установлены: jspdf, html2canvas-pro, xlsx. Импортируй их динамически через import().', + '4. Контент страницы генерируй на основе данных из API или как статические данные — на твоё усмотрение по теме.', + '5. Используй компоненты: таблицы, карточки-метрики, списки, бейджи. Стилизация через Tailwind CSS.', + '6. После создания запусти: cd /home/fitcrm/apps/web-platform-admin && npx next build && pm2 restart fitcrm-web-platform-admin', + '7. После сборки отправь в Telegram (через MCP telegram_notify) ссылку на документ: https://platform.myfitcrm.ru/document/', + '', + 'ВАЖНО: не трогай другие файлы. Только создай новую страницу документа.', + ].join('\n'); + + const task = await createTask(userChatId, wrappedMessage); + await ctx.reply( + `📄 *Документ #${task.id} — генерация запущена*\n\n` + + `Тема: ${text}\n\n` + + `Документ будет создан и доступен в панели суперадмина. Уведомлю по завершении.`, + { parse_mode: 'Markdown' }, + ); +}); + bot.command('sessions', async (ctx) => { const sessions = Array.from(activeSessions.values()); @@ -602,11 +690,19 @@ bot.command('spawned', async (ctx) => { // Listen for any text messages from user bot.on('text', async (ctx) => { - const message = ctx.message.text; + let message = ctx.message.text; const from = ctx.from.username || ctx.from.first_name; console.log(`\n📨 Message from ${from}: "${message}"\n`); + // Extract quoted message text if this is a reply + const replyToMsg = ctx.message.reply_to_message; + if (replyToMsg && 'text' in replyToMsg && replyToMsg.text) { + const quotedText = replyToMsg.text; + console.log(`💬 Message is a reply, quoted text: "${quotedText.substring(0, 100)}..."`); + message = `[Цитата сообщения на которое отвечает пользователь]:\n${quotedText}\n\n[Ответ пользователя]:\n${message}`; + } + // Record message in chat history buffer (for chatbot context) addToChatHistory(ctx.chat.id.toString(), from, message); @@ -795,7 +891,66 @@ bot.on('text', async (ctx) => { } if (!cleanMessage) cleanMessage = message; + // Extract quoted message text if this is a reply + if (replyTo && 'text' in replyTo && replyTo.text) { + const quotedText = replyTo.text; + console.log(`💬 Reply contains quoted text: "${quotedText.substring(0, 100)}..."`); + cleanMessage = `[Цитата сообщения на которое отвечает пользователь]:\n${quotedText}\n\n[Ответ пользователя]:\n${cleanMessage}`; + + // If quoted message contains a task plan (📋 Задача #), route through task manager + // Task manager has more turns (50 vs 10) — suitable for executing plans + if (quotedText.includes('📋 Задача #') || quotedText.includes('Задача #')) { + const userChatId = ctx.chat.id.toString(); + console.log(`📋 Detected task plan in quoted message — routing through task manager`); + const task = await createTask(userChatId, cleanMessage); + await ctx.reply(`📋 *Задача #${task.id} создана*\n\nВыполняется в фоне. Уведомлю по завершении.`, { parse_mode: 'Markdown' }); + return; + } + } + const userChatId = ctx.chat.id.toString(); + + // Intent detection: route natural language to commands + const docMatch = cleanMessage.match(/^(?:создай|сгенерируй|сделай|подготовь|сформируй)\s+документ\s+(.+)/i); + if (docMatch) { + const docTopic = docMatch[1].replace(/^(?:на тему|о|об|по|про)\s+/i, '').trim(); + console.log(`📄 Intent detected: document creation — "${docTopic}"`); + const wrappedMessage = [ + `Создай документ-страницу в панели суперадмина на тему: "${docTopic}"`, + '', + 'Инструкции:', + '1. Создай новый файл в apps/web-platform-admin/src/app/(dashboard)/document//page.tsx', + ' где — транслитерация названия документа (kebab-case, латиница).', + '2. Используй как образец файл apps/web-platform-admin/src/app/(dashboard)/document/dev-status/page.tsx', + '3. Документ ОБЯЗАТЕЛЬНО должен иметь три кнопки: "Скачать PDF" (jspdf + html2canvas-pro), "Скачать Excel" (xlsx), "Печать" (window.print()).', + ' Все три библиотеки уже установлены: jspdf, html2canvas-pro, xlsx. Импортируй их динамически через import().', + '4. Контент страницы генерируй на основе данных из API или как статические данные — на твоё усмотрение по теме.', + '5. Используй компоненты: таблицы, карточки-метрики, списки, бейджи. Стилизация через Tailwind CSS.', + '6. После создания запусти: cd /home/fitcrm/apps/web-platform-admin && npx next build && pm2 restart fitcrm-web-platform-admin', + '7. После сборки отправь в Telegram (через MCP telegram_notify) ссылку на документ: https://platform.myfitcrm.ru/document/', + '', + 'ВАЖНО: не трогай другие файлы. Только создай новую страницу документа.', + ].join('\n'); + const task = await createTask(userChatId, wrappedMessage); + await ctx.reply( + `📄 *Документ #${task.id} — генерация запущена*\n\nТема: ${docTopic}\n\nУведомлю по завершении.`, + { parse_mode: 'Markdown' }, + ); + return; + } + + const taskMatch = cleanMessage.match(/^(?:создай|сделай|выполни|запусти)\s+(?:задачу|таск)\s+(.+)/i); + if (taskMatch) { + const taskMessage = taskMatch[1].trim(); + console.log(`📋 Intent detected: task creation — "${taskMessage}"`); + const task = await createTask(userChatId, taskMessage); + await ctx.reply( + `📋 *Задача #${task.id} создана*\n\nВыполняется в фоне. Уведомлю по завершении.`, + { parse_mode: 'Markdown' }, + ); + return; + } + console.log(`🤖 Chatbot handling message from ${from}: "${cleanMessage.substring(0, 50)}..."`); // Prepend chat history as context