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 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();
|
||||||
|
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();
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user