feat: /document command + intent detection в чат-боте

- /document list — список документов из файловой системы
- /document <тема> — генерация документа через фоновую задачу
- Intent detection: «создай документ ...» через @mention = /document
- Intent detection: «создай задачу ...» через @mention = /task
- Обновлён /help с секцией Documents

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-20 07:34:36 +00:00
parent 5cd67c06c6
commit 4a7b5004c4

View File

@@ -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 commands
bot.start(async (ctx) => { bot.start(async (ctx) => {
chatId = ctx.chat.id.toString(); chatId = ctx.chat.id.toString();
@@ -214,6 +231,9 @@ bot.command('help', async (ctx) => {
'`/task list` - Список задач\n' + '`/task list` - Список задач\n' +
'`/task N` - Статус задачи #N\n' + '`/task N` - Статус задачи #N\n' +
'`/task cancel N` - Отменить задачу\n\n' + '`/task cancel N` - Отменить задачу\n\n' +
'*Documents:*\n' +
'`/document list` - Список документов\n' +
'`/document <тема>` - Сгенерировать документ в панели суперадмина\n\n' +
'*Session Management:*\n' + '*Session Management:*\n' +
'`/sessions` - List active Claude sessions\n' + '`/sessions` - List active Claude sessions\n' +
'`/queue` - View queued messages\n\n' + '`/queue` - View queued messages\n\n' +
@@ -296,9 +316,17 @@ bot.command('context', async (ctx) => {
}); });
bot.command('task', 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(); 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 // /task list
if (text === 'list' || text === '') { if (text === 'list' || text === '') {
const tasks = await listTasks(); const tasks = await listTasks();
@@ -361,6 +389,66 @@ bot.command('task', async (ctx) => {
await ctx.reply(`📋 *Задача #${task.id} создана*\n\nВыполняется в фоне. Уведомлю по завершении.`, { parse_mode: 'Markdown' }); 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 <description> — create new document
const wrappedMessage = [
`Создай документ-страницу в панели суперадмина на тему: "${text}"`,
'',
'Инструкции:',
'1. Создай новый файл в apps/web-platform-admin/src/app/(dashboard)/document/<slug>/page.tsx',
' где <slug> — транслитерация названия документа (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/<slug>',
'',
'ВАЖНО: не трогай другие файлы. Только создай новую страницу документа.',
].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) => { bot.command('sessions', async (ctx) => {
const sessions = Array.from(activeSessions.values()); const sessions = Array.from(activeSessions.values());
@@ -602,11 +690,19 @@ bot.command('spawned', async (ctx) => {
// Listen for any text messages from user // Listen for any text messages from user
bot.on('text', async (ctx) => { bot.on('text', async (ctx) => {
const message = ctx.message.text; let message = ctx.message.text;
const from = ctx.from.username || ctx.from.first_name; const from = ctx.from.username || ctx.from.first_name;
console.log(`\n📨 Message from ${from}: "${message}"\n`); 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) // Record message in chat history buffer (for chatbot context)
addToChatHistory(ctx.chat.id.toString(), from, message); addToChatHistory(ctx.chat.id.toString(), from, message);
@@ -795,7 +891,66 @@ bot.on('text', async (ctx) => {
} }
if (!cleanMessage) cleanMessage = message; 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(); 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/<slug>/page.tsx',
' где <slug> — транслитерация названия документа (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/<slug>',
'',
'ВАЖНО: не трогай другие файлы. Только создай новую страницу документа.',
].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)}..."`); console.log(`🤖 Chatbot handling message from ${from}: "${cleanMessage.substring(0, 50)}..."`);
// Prepend chat history as context // Prepend chat history as context