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:
159
src/index.ts
159
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 <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) => {
|
||||
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/<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)}..."`);
|
||||
|
||||
// Prepend chat history as context
|
||||
|
||||
Reference in New Issue
Block a user