INIT
This commit is contained in:
parent
3b0d281844
commit
ca13019afb
141
README.md
141
README.md
|
|
@ -0,0 +1,141 @@
|
||||||
|
Вот **аннотацию и инструкцию для README.md** для твоего проекта **BotRouting** — всё по делу, без воды и с пояснением структуры, установки и запуска.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# **BotRouting**
|
||||||
|
|
||||||
|
**BotRouting** — это гибкая система маршрутизации и управления Telegram-ботами с поддержкой:
|
||||||
|
|
||||||
|
* разделения прав пользователей по группам и внешним интерфейсам,
|
||||||
|
* гибкой маршрутизации сообщений (по ключевым словам, командам, LLM),
|
||||||
|
* современной админкой для управления пользователями, группами, ботами, командами,
|
||||||
|
* масштабируемой архитектуры на Node.js, Next.js и Prisma.
|
||||||
|
|
||||||
|
## **Структура репозитория**
|
||||||
|
|
||||||
|
```
|
||||||
|
BotRouting/
|
||||||
|
├── admin/ # Админ-панель (Next.js + React)
|
||||||
|
├── deamon/ # Демон-роутер (Node.js, Telegram, LLM, Prisma)
|
||||||
|
├── clientExample/# Пример клиента (необязательно)
|
||||||
|
├── node_modules/
|
||||||
|
├── package.json
|
||||||
|
├── pnpm-lock.yaml
|
||||||
|
├── pnpm-workspace.yaml
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
* **admin/** — современная админка для управления группами, пользователями, ботами, командами.
|
||||||
|
* **deamon/** — основной роутинг-сервис: интеграция Telegram API, LLM, Prisma.
|
||||||
|
* **clientExample/** — пример использования клиента (опционально, можешь удалить).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Установка**
|
||||||
|
|
||||||
|
### 1. **Клонируй репозиторий**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone <URL-репозитория>
|
||||||
|
cd BotRouting
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Установи зависимости**
|
||||||
|
|
||||||
|
> Можно использовать **pnpm** (предпочтительно) или **npm** — оба поддерживаются благодаря workspace-конфигу.
|
||||||
|
|
||||||
|
#### **Через pnpm:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Через npm:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Настрой переменные окружения**
|
||||||
|
|
||||||
|
Создай файл `.env` в корне каждого пакета (`admin/`, `deamon/`) и задай переменные:
|
||||||
|
|
||||||
|
#### **Пример для `deamon/.env`:**
|
||||||
|
|
||||||
|
```
|
||||||
|
DATABASE_URL="file:../deamon/dev.db"
|
||||||
|
TELEGRAM_TOKEN="...твой токен..."
|
||||||
|
OPENAI_API_KEY="...ключ openai..."
|
||||||
|
LM_MODEL="vikhrmodels-vikhr-nemo-12b-instruct-r-21-09-24"
|
||||||
|
LM_BASE_URL="http://localhost:1234/v1"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Пример для `admin/.env`:**
|
||||||
|
|
||||||
|
```
|
||||||
|
DATABASE_URL="file:../deamon/dev.db"
|
||||||
|
NEXT_PUBLIC_API_URL="http://localhost:3000/api"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Важно:** один и тот же файл БД может использоваться обеими частями (админка и демон), если база в SQLite и тестовая. Для production — смотри документацию Prisma!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **Инициализируй и мигрируй БД**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm -F deamon prisma migrate dev
|
||||||
|
```
|
||||||
|
|
||||||
|
или
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd deamon
|
||||||
|
npx prisma migrate dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Запуск**
|
||||||
|
|
||||||
|
### **Админ-панель**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm -F admin dev
|
||||||
|
```
|
||||||
|
|
||||||
|
или
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd admin
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Откроется на [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Демон-роутер (боты и LLM)**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm -F deamon start
|
||||||
|
```
|
||||||
|
|
||||||
|
или
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd deamon
|
||||||
|
node external-bot-daemon.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Основные возможности**
|
||||||
|
|
||||||
|
* **Управление доступом**: через группы, ботов и команды.
|
||||||
|
* **Гибкая маршрутизация сообщений**: по ключевым словам, командам или LLM (AI).
|
||||||
|
* **LLM-интеграция**: поддержка локальных или облачных моделей через OpenAI API (в т.ч. open-source локальные, если указан LM\_BASE\_URL).
|
||||||
|
* **Модульная архитектура**: легко добавлять новые типы ботов, логику, интерфейсы и расширять систему.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 27ef38e41bea642bcaa837082e77b6c8db14cb6b
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
// RmqTelegramBot.js
|
||||||
|
import TelegramBot from 'node-telegram-bot-api'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { getChannel } from './utils/rmq.js'
|
||||||
|
|
||||||
|
export class RmqTelegramBot {
|
||||||
|
constructor(botName, token, options = {}) {
|
||||||
|
this._bot = new TelegramBot(token, { ...options, polling: false, webHook: false, debug: true })
|
||||||
|
this._name = botName
|
||||||
|
this._channel = getChannel()
|
||||||
|
this._inQ = `InMessage${botName}`
|
||||||
|
this._outQ = `OutMessage${botName}`
|
||||||
|
this._sessionQ = `SessionQueue${botName}` // общая системная очередь для сессий
|
||||||
|
|
||||||
|
// убедимся, что очередь существует
|
||||||
|
this._channel.assertQueue(this._sessionQ, { durable: true })
|
||||||
|
|
||||||
|
// переопределяем HTTP‑запрос, чтобы шлём всё в OutMessage…
|
||||||
|
this._bot._request = (type, form) => {
|
||||||
|
const payload = { method: type, form }
|
||||||
|
this._channel.sendToQueue(
|
||||||
|
this._outQ,
|
||||||
|
Buffer.from(JSON.stringify(payload)),
|
||||||
|
{ persistent: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InMessage → processUpdate
|
||||||
|
this._channel.consume(this._inQ, msg => {
|
||||||
|
try {
|
||||||
|
const { update } = JSON.parse(msg.content.toString())
|
||||||
|
console.log('update',update);
|
||||||
|
this._bot.processUpdate(update)
|
||||||
|
this._channel.ack(msg)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to process update from RMQ', err)
|
||||||
|
this._channel.nack(msg, false, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Proxy для автоматической делегации любых методов в this._bot
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (prop in target) {
|
||||||
|
const v = target[prop]
|
||||||
|
return typeof v === 'function' ? v.bind(target) : v
|
||||||
|
}
|
||||||
|
if (prop in target._bot) {
|
||||||
|
const v = target._bot[prop]
|
||||||
|
return typeof v === 'function' ? v.bind(target._bot) : v
|
||||||
|
}
|
||||||
|
throw new Error(`No such method or property: ${String(prop)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——————— Сессионные методы ———————
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RPC‑запрос текущего занятия сессии
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns {Promise<object>} состояние из session‑сервиса
|
||||||
|
*/
|
||||||
|
async requestSessionState(userId) {
|
||||||
|
const correlationId = uuidv4()
|
||||||
|
// временная очередь-ответчик
|
||||||
|
const { queue: replyQ } = await this._channel.assertQueue('', { exclusive: true })
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._channel.consume(replyQ, msg => {
|
||||||
|
if (msg.properties.correlationId === correlationId) {
|
||||||
|
const state = JSON.parse(msg.content.toString())
|
||||||
|
resolve(state)
|
||||||
|
}
|
||||||
|
}, { noAck: true })
|
||||||
|
|
||||||
|
// запрос в общий SessionQueue
|
||||||
|
const payload = { action: 'get', userId }
|
||||||
|
this._channel.sendToQueue(
|
||||||
|
this._sessionQ,
|
||||||
|
Buffer.from(JSON.stringify(payload)),
|
||||||
|
{ correlationId, replyTo: replyQ }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Захватить внимание этого бота за пользователем
|
||||||
|
* @param {string} userId
|
||||||
|
*/
|
||||||
|
lockSession(userId) {
|
||||||
|
const payload = {
|
||||||
|
action: 'lock',
|
||||||
|
userId,
|
||||||
|
externalBotId: this._name
|
||||||
|
}
|
||||||
|
this._channel.sendToQueue(
|
||||||
|
this._sessionQ,
|
||||||
|
Buffer.from(JSON.stringify(payload)),
|
||||||
|
{ persistent: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отпустить внимание (снять блокировку)
|
||||||
|
* @param {string} userId
|
||||||
|
*/
|
||||||
|
unlockSession(userId) {
|
||||||
|
const payload = { action: 'unlock', userId }
|
||||||
|
this._channel.sendToQueue(
|
||||||
|
this._sessionQ,
|
||||||
|
Buffer.from(JSON.stringify(payload)),
|
||||||
|
{ persistent: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——————— Событийные обёртки ———————
|
||||||
|
|
||||||
|
on(event, listener) {
|
||||||
|
this._bot.on(event, listener)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
once(event, listener) {
|
||||||
|
this._bot.once(event, listener)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
removeListener(event, listener) {
|
||||||
|
this._bot.removeListener(event, listener)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { RmqTelegramBot } from './RmqTelegramBot.js'
|
||||||
|
// Предположим, что ваш ext.id и имя бота — 'BusinessTrips'
|
||||||
|
const botName = 'BusinessTrips'
|
||||||
|
const botToken = 'неважен_здесь'; // нужен лишь для инициализации, но API не вызывается
|
||||||
|
|
||||||
|
import { initRabbit, getChannel } from './utils/rmq.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const channel = await initRabbit(process.env.RABBIT_URL || 'amqp://telegram_main:PASSWORD@rabbit.vidi.one:5672/telegram_main');
|
||||||
|
|
||||||
|
const bot = new RmqTelegramBot(botName, botToken)
|
||||||
|
|
||||||
|
// регистрируем обработчик апдейтов «как обычно»
|
||||||
|
bot.on('message', async msg => {
|
||||||
|
|
||||||
|
console.log('message',msg);
|
||||||
|
|
||||||
|
// const resp = await bot.requestSessionState(msg.innerUserId);
|
||||||
|
// console.log('resp',resp);
|
||||||
|
// bot.lockSession(msg.innerUserId);
|
||||||
|
bot.sendMessage(
|
||||||
|
msg.chat.id,
|
||||||
|
'Тест окей'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ваш сервис не вызывает bot.launch() и не запускает polling/webhook
|
||||||
|
console.log(`🚀 RMQ‑bot for ${botName} ready, listening InMessage${botName}`)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "clientexample",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"author": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"amqplib": "^0.10.8",
|
||||||
|
"node-telegram-bot-api": "^0.66.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
Ты выступаешь в роли маршрутизатора сообщений между различными подботами в Telegram.
|
||||||
|
|
||||||
|
На входе ты получаешь только текст сообщения от пользователя.
|
||||||
|
|
||||||
|
Твоя задача — определить, какому из подботов следует передать сообщение. Ответ строго в формате JSON:
|
||||||
|
|
||||||
|
{
|
||||||
|
"bot": "название_бота",
|
||||||
|
"reason": "короткое объяснение маршрута"
|
||||||
|
}
|
||||||
|
|
||||||
|
Вот доступные подботы и их специализация:
|
||||||
|
|
||||||
|
- "trip_bot": Вопросы о командировках, поездках, бронировании отелей.
|
||||||
|
- "weather_bot": Прогноз погоды, текущая температура, осадки и т.п.
|
||||||
|
- "news_bot": Новости мира, страны, региона.
|
||||||
|
- "joke_bot": Пошути, расскажи анекдот, интересный факт.
|
||||||
|
- "support_bot": Техническая поддержка, вопросы о работе системы.
|
||||||
|
- "finance_bot": Курс валют, инвестиции, финансы, банки.
|
||||||
|
|
||||||
|
Если сообщение не удаётся однозначно отнести ни к одному боту, укажи "bot": "unknown".
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
|
||||||
|
Вход: "Какая погода завтра в Белграде?"
|
||||||
|
Ответ:
|
||||||
|
{
|
||||||
|
"bot": "weather_bot",
|
||||||
|
"reason": "Запрос прогноза погоды на завтра в конкретном городе"
|
||||||
|
}
|
||||||
|
|
||||||
|
Вход: "Забронируй мне командировку в Москву с понедельника"
|
||||||
|
Ответ:
|
||||||
|
{
|
||||||
|
"bot": "trip_bot",
|
||||||
|
"reason": "Пользователь оформляет командировку"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* Генерирует системный промт для LLM-роутинга
|
||||||
|
* @param {Array} bots - [{ name: 'trip_bot', description: '...' }, ...]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function buildRouterPrompt(bots) {
|
||||||
|
console.log('bots',bots);
|
||||||
|
// Преобразуем список ботов в текст с описаниями
|
||||||
|
const botsBlock = bots.map(
|
||||||
|
b => `- "${b.name}": ${b.description || 'Без описания.'}`
|
||||||
|
).join('\n')
|
||||||
|
|
||||||
|
// Примеры (можно вынести отдельно)
|
||||||
|
const examples = `
|
||||||
|
Вход: "Какая погода завтра в Белграде?"
|
||||||
|
Ответ:
|
||||||
|
{
|
||||||
|
"bot": "weather_bot",
|
||||||
|
"reason": "Запрос прогноза погоды на завтра в конкретном городе"
|
||||||
|
}
|
||||||
|
|
||||||
|
Вход: "Забронируй мне командировку в Москву с понедельника"
|
||||||
|
Ответ:
|
||||||
|
{
|
||||||
|
"bot": "trip_bot",
|
||||||
|
"reason": "Пользователь оформляет командировку"
|
||||||
|
}`.trim()
|
||||||
|
|
||||||
|
return `
|
||||||
|
Ты выступаешь в роли маршрутизатора сообщений между различными подботами в Telegram.
|
||||||
|
|
||||||
|
На входе ты получаешь только текст сообщения от пользователя.
|
||||||
|
|
||||||
|
Твоя задача — определить, какому из подботов следует передать сообщение. Ответ строго в формате JSON:
|
||||||
|
|
||||||
|
{
|
||||||
|
"bot": "название_бота",
|
||||||
|
"reason": "короткое объяснение маршрута"
|
||||||
|
}
|
||||||
|
|
||||||
|
Вот доступные подботы и их специализация в формаьте (- {name}: {description}):
|
||||||
|
|
||||||
|
${botsBlock}
|
||||||
|
|
||||||
|
Если сообщение не удаётся однозначно отнести ни к одному боту или нет бота способного обработать это исключение, укажи "bot": "unknown".
|
||||||
|
|
||||||
|
|
||||||
|
`.trim()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// rmq.js
|
||||||
|
import amqplib from 'amqplib';
|
||||||
|
|
||||||
|
let channel;
|
||||||
|
export async function initRabbit(url) {
|
||||||
|
const conn = await amqplib.connect(url);
|
||||||
|
channel = await conn.createChannel();
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChannel() {
|
||||||
|
if (!channel) throw new Error('RabbitMQ channel not initialized');
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// external-bot-daemon.js
|
||||||
|
import TelegramBot from 'node-telegram-bot-api'
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
// ...твой код ниже
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
// Мапа для хранения всех активных клиентов
|
||||||
|
const bots = {}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Получаем все внешние интерфейсы из базы
|
||||||
|
const externalBots = await prisma.externalBot.findMany({
|
||||||
|
include: { bots: { include: { commands: { include: { groupIds: true } }, groups: true } } }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Для каждого интерфейса запускаем TelegramBot
|
||||||
|
for (const ext of externalBots) {
|
||||||
|
if (!ext.token) continue
|
||||||
|
// Если уже создан, не пересоздаем
|
||||||
|
if (bots[ext.token]) continue
|
||||||
|
console.log('ext.token',ext.token);
|
||||||
|
const bot = new TelegramBot(ext.token, { polling: true })
|
||||||
|
bots[ext.token] = bot
|
||||||
|
|
||||||
|
bot.on('message', async msg => {
|
||||||
|
// user id
|
||||||
|
const userId = msg.from.id.toString()
|
||||||
|
// Можно искать User по platform/ID (если надо)
|
||||||
|
|
||||||
|
// Получаем все боты и команды для этого интерфейса
|
||||||
|
const freshExt = await prisma.externalBot.findUnique({
|
||||||
|
where: { id: ext.id },
|
||||||
|
include: { bots: { include: { commands: { include: { groupIds: true } }, groups: true } } }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Можно получить все команды и группы
|
||||||
|
const routing = freshExt.bots.map(b => ({
|
||||||
|
name: b.name,
|
||||||
|
commands: b.commands.map(cmd => ({
|
||||||
|
command: cmd.command,
|
||||||
|
description: cmd.description,
|
||||||
|
groups: cmd.groupIds.map(g => g.name)
|
||||||
|
})),
|
||||||
|
groups: b.groups.map(g => g.name)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Для теста просто отправь список команд в ответ
|
||||||
|
const text =
|
||||||
|
routing.map(b =>
|
||||||
|
`Бот: ${b.name}\nКоманды:\n` +
|
||||||
|
b.commands.map(c =>
|
||||||
|
` /${c.command} (${c.groups.join(', ')})${c.description ? ': ' + c.description : ''}`
|
||||||
|
).join('\n')
|
||||||
|
).join('\n\n')
|
||||||
|
bot.sendMessage(msg.chat.id, text || 'Нет доступных команд')
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Started polling for ExternalBot "${ext.name}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import TelegramBot from 'node-telegram-bot-api'
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { handleIncomingMessage } from './router/index.js'
|
||||||
|
import { initRabbit, getChannel } from './utils/rmq.js';
|
||||||
|
import { getSessionState,lockSessionToBot,unlockSession } from './router/session.js'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
const bots = {}
|
||||||
|
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const channel = await initRabbit(process.env.RABBIT_URL || 'amqp://telegram_main:PASSWORD@rabbit.vidi.one:5672/telegram_main');
|
||||||
|
|
||||||
|
const externalBots = await prisma.externalBot.findMany({
|
||||||
|
include: { bots: { include: { commands: { include: { groupIds: true } }, groups: true } } }
|
||||||
|
})
|
||||||
|
|
||||||
|
// сначала ассерт очередей и потребитель OutMessage…
|
||||||
|
for (const ext of externalBots) {
|
||||||
|
for( const bot of ext.bots){
|
||||||
|
const inQ = `InMessage${bot.name}`;
|
||||||
|
const outQ = `OutMessage${bot.name}`;
|
||||||
|
const sessionQ = `SessionQueue${bot.name}`;
|
||||||
|
await channel.assertQueue(inQ, { durable: true });
|
||||||
|
await channel.assertQueue(outQ, { durable: true });
|
||||||
|
await channel.assertQueue(sessionQ, { durable: true });
|
||||||
|
|
||||||
|
|
||||||
|
channel.consume(outQ, msg => {
|
||||||
|
const { method,form } = JSON.parse(msg.content.toString());
|
||||||
|
|
||||||
|
// Низкоуровневый вызов HTTP-запроса к Telegram API
|
||||||
|
bots[ext.token]._request(method,form);
|
||||||
|
|
||||||
|
channel.ack(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
channel.consume(sessionQ, async reqMsg => {
|
||||||
|
try {
|
||||||
|
const { action, userId } = JSON.parse(reqMsg.content.toString());
|
||||||
|
// внешний бот, привязанный к этой очереди
|
||||||
|
const externalBotId = ext.id;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'get': {
|
||||||
|
// узнаём, захватил ли кто-то сессию
|
||||||
|
const target = await getSessionState(userId, externalBotId);
|
||||||
|
const resp = JSON.stringify({ externalBotId: target });
|
||||||
|
channel.sendToQueue(
|
||||||
|
reqMsg.properties.replyTo,
|
||||||
|
Buffer.from(resp),
|
||||||
|
{ correlationId: reqMsg.properties.correlationId }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'lock': {
|
||||||
|
// этот бот берёт сессию
|
||||||
|
await lockSessionToBot(userId, externalBotId, bot.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'unlock': {
|
||||||
|
// этот бот снимает свою блокировку
|
||||||
|
await unlockSession(userId, externalBotId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown session action: ${action}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.ack(reqMsg);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing sessionQ message', err);
|
||||||
|
// не возвращаем в очередь
|
||||||
|
channel.nack(reqMsg, false, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// теперь создаём и запускаем Telegram‑ботов
|
||||||
|
for (const ext of externalBots) {
|
||||||
|
if (!ext.token || bots[ext.token]) continue;
|
||||||
|
const bot = new TelegramBot(ext.token, { polling: true, debug: true });
|
||||||
|
bots[ext.token] = bot;
|
||||||
|
bot.processUpdate = async (update) => {
|
||||||
|
const ctx = { update, ext, bot, prisma, allBots: externalBots };
|
||||||
|
console.log('handleIncomingMessage');
|
||||||
|
await handleIncomingMessage(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bot..on('message', async msg => {
|
||||||
|
//
|
||||||
|
// await handleIncomingMessage(ctx);
|
||||||
|
// });
|
||||||
|
console.log(`Started polling for ExternalBot "${ext.name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// groupFilter.js
|
||||||
|
|
||||||
|
// Есть ли хотя бы одна общая группа (userGroups и otherGroups — массивы объектов с .id)
|
||||||
|
export function hasCommonGroup(userGroups, otherGroups) {
|
||||||
|
if (!Array.isArray(userGroups) || !Array.isArray(otherGroups)) return false
|
||||||
|
const userGroupIds = userGroups.map(g => g.id)
|
||||||
|
return otherGroups.some(g => userGroupIds.includes(g.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Строго фильтрует: возвращает только ботов, у которых есть общая группа с userGroups,
|
||||||
|
// и только те команды внутри бота, которые разрешены этим группам.
|
||||||
|
export function filterBotsAndCommandsByUserGroups(allBots, userGroups) {
|
||||||
|
console.log('allBots');
|
||||||
|
console.dir(allBots, { depth: null, colors: true });
|
||||||
|
|
||||||
|
return allBots
|
||||||
|
.filter(bot => {
|
||||||
|
const botGroups = bot.groups || []
|
||||||
|
// Строго: если у бота есть группы — только с пересечением, иначе вообще не показываем
|
||||||
|
return botGroups.length > 0 && hasCommonGroup(userGroups, botGroups)
|
||||||
|
})
|
||||||
|
.map(bot => {
|
||||||
|
// Оставляем только доступные команды для этих групп
|
||||||
|
const filteredCommands = (bot.commands || []).filter(cmd =>
|
||||||
|
cmd.groupIds?.length > 0 && hasCommonGroup(userGroups, cmd.groupIds)
|
||||||
|
)
|
||||||
|
return { ...bot, commands: filteredCommands }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "deamon",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"author": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "external-bot-daemon.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^6.12.0",
|
||||||
|
"amqplib": "^0.10.8",
|
||||||
|
"node-telegram-bot-api": "^0.66.0",
|
||||||
|
"openai": "^5.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// commandMatcher.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ищет команду в начале текста и возвращает объект с ботом и командой
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Array<{ name: string, commands: Array<{ command: string }> }>} bots
|
||||||
|
* @returns {{ botName: string, commandName: string } | null}
|
||||||
|
*/
|
||||||
|
export function findCommandTargetBot(text, bots) {
|
||||||
|
if (!text) return null
|
||||||
|
|
||||||
|
// первое слово, без ведущего "/"
|
||||||
|
const [firstWord] = text.trim().split(/\s+/)
|
||||||
|
const normalized = firstWord.replace(/^\//, '').toLowerCase()
|
||||||
|
|
||||||
|
for (const b of bots) {
|
||||||
|
for (const cmd of b.commands || []) {
|
||||||
|
if (cmd.command.toLowerCase() === normalized) {
|
||||||
|
return {
|
||||||
|
botName: b.name,
|
||||||
|
commandName: cmd.command
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
import { getLockingBot } from './session.js'
|
||||||
|
import { findCommandTargetBot } from './commandMatcher.js'
|
||||||
|
import { routeByLLM } from './llmRouter.js'
|
||||||
|
import { getOrCreateUserAndGroups } from '../userService.js'
|
||||||
|
import { filterBotsAndCommandsByUserGroups } from '../groupFilter.js'
|
||||||
|
import { checkUserBotRights, checkUserCommandRights } from './rights.js'
|
||||||
|
import { getChannel } from '../utils/rmq.js' // <-- импорт канала
|
||||||
|
|
||||||
|
|
||||||
|
const updateKeys = [
|
||||||
|
'message',
|
||||||
|
'edited_message',
|
||||||
|
'channel_post',
|
||||||
|
'edited_channel_post',
|
||||||
|
'business_connection',
|
||||||
|
'business_message',
|
||||||
|
'edited_business_message',
|
||||||
|
'deleted_business_messages',
|
||||||
|
'message_reaction',
|
||||||
|
'message_reaction_count',
|
||||||
|
'inline_query',
|
||||||
|
'chosen_inline_result',
|
||||||
|
'callback_query',
|
||||||
|
'shipping_query',
|
||||||
|
'pre_checkout_query',
|
||||||
|
'purchased_paid_media',
|
||||||
|
'poll',
|
||||||
|
'poll_answer',
|
||||||
|
'my_chat_member',
|
||||||
|
'chat_member',
|
||||||
|
'chat_join_request',
|
||||||
|
'chat_boost',
|
||||||
|
'removed_chat_boost'
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function handleIncomingMessage(ctx) {
|
||||||
|
|
||||||
|
//console.log('ctx',ctx);
|
||||||
|
const { update, ext, prisma } = ctx
|
||||||
|
|
||||||
|
const foundKey = updateKeys.find(key => update[key] !== undefined);
|
||||||
|
|
||||||
|
const msg = update[foundKey];
|
||||||
|
|
||||||
|
const channel = getChannel()
|
||||||
|
|
||||||
|
// 1) Пользователь и группы
|
||||||
|
const { user, userId, groups } = await getOrCreateUserAndGroups(ctx.prisma, msg.from)
|
||||||
|
|
||||||
|
msg['innerUserId'] = userId;
|
||||||
|
|
||||||
|
console.log('ext',ext);
|
||||||
|
|
||||||
|
// 0) Подтягиваем актуальный externalBot вместе с bots, commands и groups
|
||||||
|
const fullExt = await prisma.externalBot.findUnique({
|
||||||
|
where: { id: ext.id },
|
||||||
|
include: {
|
||||||
|
bots: {
|
||||||
|
include: {
|
||||||
|
commands: { include: { groupIds: true } },
|
||||||
|
groups: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('fullExt',fullExt);
|
||||||
|
|
||||||
|
if (!fullExt) {
|
||||||
|
return ctx.bot.sendMessage(msg.chat.id, 'Internal error: external bot not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Доступные боты
|
||||||
|
const availableBots = filterBotsAndCommandsByUserGroups(fullExt.bots, groups)
|
||||||
|
.filter(b => b.isActive)
|
||||||
|
|
||||||
|
console.log('availableBots',availableBots);
|
||||||
|
|
||||||
|
// 3) Захват внимания в сессии
|
||||||
|
const sessionBotID = await getLockingBot(userId,ext.id);
|
||||||
|
console.log('sessionBotID',sessionBotID);
|
||||||
|
if (sessionBotID) {
|
||||||
|
const sessionBotName = (availableBots.find(b => b.id === sessionBotID) || {}).name || null
|
||||||
|
if(sessionBotName){
|
||||||
|
if (!checkUserBotRights(availableBots, sessionBotName)) {
|
||||||
|
return ctx.bot.sendMessage(msg.chat.id, 'Нет доступа к боту.')
|
||||||
|
}
|
||||||
|
// Публикуем в очередь
|
||||||
|
const inQ = `InMessage${sessionBotName}`
|
||||||
|
channel.sendToQueue(inQ, Buffer.from(JSON.stringify({
|
||||||
|
update,
|
||||||
|
userId,
|
||||||
|
reason: 'session_capture'
|
||||||
|
})), { persistent: true })
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
await unlockSession(userId,ext.id)
|
||||||
|
return ctx.bot.sendMessage(msg.chat.id, 'Нет доступа к выбранному боту.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Чёткая команда
|
||||||
|
if(msg?.text){
|
||||||
|
const commandMatch = findCommandTargetBot(msg.text, availableBots)
|
||||||
|
if (commandMatch) {
|
||||||
|
const { botName, commandName } = commandMatch
|
||||||
|
if (!checkUserCommandRights(availableBots, botName, commandName)) {
|
||||||
|
return ctx.bot.sendMessage(msg.chat.id, 'Нет доступа к команде.')
|
||||||
|
}
|
||||||
|
const inQ = `InMessage${botName}`
|
||||||
|
channel.sendToQueue(inQ, Buffer.from(JSON.stringify({
|
||||||
|
update,
|
||||||
|
userId,
|
||||||
|
command: commandName,
|
||||||
|
reason: 'explicit_command'
|
||||||
|
})), { persistent: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) LLM‑роутинг
|
||||||
|
const llmInput = availableBots.map(b => ({
|
||||||
|
name: b.name,
|
||||||
|
description: b.description || 'Без описания'
|
||||||
|
}))
|
||||||
|
const llmResult = await routeByLLM(msg.text, llmInput)
|
||||||
|
const targetBot = llmResult.bot
|
||||||
|
|
||||||
|
if (targetBot && targetBot !== 'unknown') {
|
||||||
|
if (!checkUserBotRights(availableBots, targetBot)) {
|
||||||
|
return ctx.bot.sendMessage(msg.chat.id, 'Нет доступа к выбранному боту.')
|
||||||
|
}
|
||||||
|
const inQ = `InMessage${targetBot}`
|
||||||
|
const ok = channel.sendToQueue(inQ, Buffer.from(JSON.stringify({
|
||||||
|
update,
|
||||||
|
userId,
|
||||||
|
reason: llmResult.reason
|
||||||
|
})), { persistent: true })
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
// буфер переполнен, подождём drain
|
||||||
|
channel.once('drain', () => {
|
||||||
|
console.log('Канал освободился, можно отправлять дальше')
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
console.log('Отправили в ',inQ)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Не смогли определить бота — уведомляем сразу
|
||||||
|
return ctx.bot.sendMessage(
|
||||||
|
msg.chat.id,
|
||||||
|
'Не удалось определить, какому боту передать сообщение.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || 'test';
|
||||||
|
const LM_BASE_URL = process.env.LM_BASE_URL || 'http://localhost:1234/v1';
|
||||||
|
|
||||||
|
export const openai = new OpenAI({
|
||||||
|
baseURL: LM_BASE_URL,
|
||||||
|
apiKey: OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
import {buildRouterPrompt} from '../utils/llmUtils.js'
|
||||||
|
/**
|
||||||
|
* Маршрутизация через LLM: возвращает { bot, reason }
|
||||||
|
* @param {string} userText
|
||||||
|
* @param {Array} bots - [{ name, description }]
|
||||||
|
* @returns {Promise<{bot: string, reason: string}>}
|
||||||
|
*/
|
||||||
|
export async function routeByLLM(userText, bots) {
|
||||||
|
const prompt = buildRouterPrompt(bots);
|
||||||
|
|
||||||
|
console.log('prompt',prompt);
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{ role: 'system', content: prompt },
|
||||||
|
{ role: 'user', content: userText }
|
||||||
|
]
|
||||||
|
|
||||||
|
console.log('messages',messages[1]);
|
||||||
|
const data = {
|
||||||
|
model: process.env.LM_MODEL || 'vikhrmodels-vikhr-nemo-12b-instruct-r-21-09-24',
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
const response = await openai.chat.completions.create(data)
|
||||||
|
const content = response.choices[0]?.message?.content || '{}'
|
||||||
|
|
||||||
|
console.log('content',content);
|
||||||
|
let jsonString = content
|
||||||
|
.replace(/```(?:json)?\s*/g, '') // убираем ``` или ```json и любые пробелы/переводы строки после
|
||||||
|
.replace(/```/g, '') // убираем оставшиеся ```
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(jsonString)
|
||||||
|
// result: { bot, reason }
|
||||||
|
return result
|
||||||
|
} catch (e) {
|
||||||
|
return { bot: 'unknown', reason: 'Ошибка парсинга ответа' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// rights.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка, есть ли у пользователя доступ к боту по его имени
|
||||||
|
* @param {Array<Object>} availableBots — массив объектов ботов { name, commands, … }
|
||||||
|
* @param {string} botName — название бота для проверки
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function checkUserBotRights(availableBots, botName) {
|
||||||
|
return availableBots.some(b => b.name === botName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка, есть ли у пользователя доступ к конкретной команде бота
|
||||||
|
* @param {Array<Object>} availableBots — массив объектов ботов { name, commands, … }
|
||||||
|
* @param {string} botName — название бота
|
||||||
|
* @param {string} commandName — команда (поле `command`) для проверки
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function checkUserCommandRights(availableBots, botName, commandName) {
|
||||||
|
const bot = availableBots.find(b => b.name === botName);
|
||||||
|
return !!(bot && bot.commands.some(cmd => cmd.command === commandName));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// rmq.js
|
||||||
|
import amqplib from 'amqplib';
|
||||||
|
|
||||||
|
let channel;
|
||||||
|
export async function initRabbit(url) {
|
||||||
|
const conn = await amqplib.connect(url);
|
||||||
|
channel = await conn.createChannel();
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChannel() {
|
||||||
|
if (!channel) throw new Error('RabbitMQ channel not initialized');
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
// session.js
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить запись ChatState для пары (userId, externalBotId).
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} externalBotId
|
||||||
|
* @returns {Promise<import('@prisma/client').ChatState|null>}
|
||||||
|
*/
|
||||||
|
export async function getSessionState(userId, externalBotId) {
|
||||||
|
return prisma.chatState.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_externalBotId: { userId, externalBotId }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Узнать, какой внутренний бот сейчас держит сессию.
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} externalBotId
|
||||||
|
* @returns {Promise<string|null>} — возвращает botId или null
|
||||||
|
*/
|
||||||
|
export async function getLockingBot(userId, externalBotId) {
|
||||||
|
const state = await getSessionState(userId, externalBotId)
|
||||||
|
return state ? state.lockedByBot : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Захватить внимание пользователя этим внутренним ботом.
|
||||||
|
* Запишет в lockedByBot = botId.
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} externalBotId
|
||||||
|
* @param {string} botId — id внутреннего Bot
|
||||||
|
*/
|
||||||
|
export async function lockSessionToBot(userId, externalBotId, botId) {
|
||||||
|
await prisma.chatState.upsert({
|
||||||
|
where: {
|
||||||
|
userId_externalBotId: { userId, externalBotId }
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
lockedByBot: botId,
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
externalBotId,
|
||||||
|
stateJson: {},
|
||||||
|
lockedByBot: botId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Освободить сессию у пользователя для данного внешнего бота.
|
||||||
|
* Запишет lockedByBot = null.
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} externalBotId
|
||||||
|
*/
|
||||||
|
export async function unlockSession(userId, externalBotId) {
|
||||||
|
await prisma.chatState.updateMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
externalBotId,
|
||||||
|
// только если была блокировка
|
||||||
|
lockedByBot: { not: null }
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
lockedByBot: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить, захватил ли сессию конкретный внутренний бот.
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} externalBotId
|
||||||
|
* @param {string} botId
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export async function isSessionLockedByBot(userId, externalBotId, botId) {
|
||||||
|
const state = await getSessionState(userId, externalBotId)
|
||||||
|
return !!state && state.lockedByBot === botId
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
// userService.js
|
||||||
|
|
||||||
|
export async function getOrCreateUserAndGroups(prisma, fromInfo) {
|
||||||
|
const platform = 'telegram'
|
||||||
|
const originalPlatformId = fromInfo.id.toString()
|
||||||
|
const username = fromInfo.username || ''
|
||||||
|
|
||||||
|
// 1. Пытаемся найти пользователя по уникальному ключу (originalPlatformId + platform)
|
||||||
|
let user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
platform_originalPlatformId: { platform, originalPlatformId }
|
||||||
|
},
|
||||||
|
include: { groups: true } // <-- many-to-many!
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. Если не найден — создаём
|
||||||
|
if (!user) {
|
||||||
|
user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
platform,
|
||||||
|
originalPlatformId
|
||||||
|
},
|
||||||
|
include: { groups: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Получаем id пользователя
|
||||||
|
const userId = user.id
|
||||||
|
|
||||||
|
// 4. Массив групп пользователя
|
||||||
|
const groups = user.groups ?? []
|
||||||
|
|
||||||
|
// 5. Возвращаем user, userId, groups
|
||||||
|
return { user, userId, groups }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
Ты выступаешь в роли маршрутизатора сообщений между различными подботами в Telegram.
|
||||||
|
|
||||||
|
На входе ты получаешь только текст сообщения от пользователя.
|
||||||
|
|
||||||
|
Твоя задача — определить, какому из подботов следует передать сообщение. Ответ строго в формате JSON:
|
||||||
|
|
||||||
|
{
|
||||||
|
"bot": "название_бота",
|
||||||
|
"reason": "короткое объяснение маршрута"
|
||||||
|
}
|
||||||
|
|
||||||
|
Вот доступные подботы и их специализация:
|
||||||
|
|
||||||
|
- "trip_bot": Вопросы о командировках, поездках, бронировании отелей.
|
||||||
|
- "weather_bot": Прогноз погоды, текущая температура, осадки и т.п.
|
||||||
|
- "news_bot": Новости мира, страны, региона.
|
||||||
|
- "joke_bot": Пошути, расскажи анекдот, интересный факт.
|
||||||
|
- "support_bot": Техническая поддержка, вопросы о работе системы.
|
||||||
|
- "finance_bot": Курс валют, инвестиции, финансы, банки.
|
||||||
|
|
||||||
|
Если сообщение не удаётся однозначно отнести ни к одному боту, укажи "bot": "unknown".
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
|
||||||
|
Вход: "Какая погода завтра в Белграде?"
|
||||||
|
Ответ:
|
||||||
|
{
|
||||||
|
"bot": "weather_bot",
|
||||||
|
"reason": "Запрос прогноза погоды на завтра в конкретном городе"
|
||||||
|
}
|
||||||
|
|
||||||
|
Вход: "Забронируй мне командировку в Москву с понедельника"
|
||||||
|
Ответ:
|
||||||
|
{
|
||||||
|
"bot": "trip_bot",
|
||||||
|
"reason": "Пользователь оформляет командировку"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* Генерирует системный промт для LLM-роутинга
|
||||||
|
* @param {Array} bots - [{ name: 'trip_bot', description: '...' }, ...]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function buildRouterPrompt(bots) {
|
||||||
|
console.log('bots',bots);
|
||||||
|
// Преобразуем список ботов в текст с описаниями
|
||||||
|
const botsBlock = bots.map(
|
||||||
|
b => `- "${b.name}": ${b.description || 'Без описания.'}`
|
||||||
|
).join('\n')
|
||||||
|
|
||||||
|
// Примеры (можно вынести отдельно)
|
||||||
|
const examples = `
|
||||||
|
Вход: "Какая погода завтра в Белграде?"
|
||||||
|
Ответ:
|
||||||
|
{
|
||||||
|
"bot": "weather_bot",
|
||||||
|
"reason": "Запрос прогноза погоды на завтра в конкретном городе"
|
||||||
|
}
|
||||||
|
|
||||||
|
Вход: "Забронируй мне командировку в Москву с понедельника"
|
||||||
|
Ответ:
|
||||||
|
{
|
||||||
|
"bot": "trip_bot",
|
||||||
|
"reason": "Пользователь оформляет командировку"
|
||||||
|
}`.trim()
|
||||||
|
|
||||||
|
return `
|
||||||
|
Ты выступаешь в роли маршрутизатора сообщений между различными подботами в Telegram.
|
||||||
|
|
||||||
|
На входе ты получаешь только текст сообщения от пользователя.
|
||||||
|
|
||||||
|
Твоя задача — определить, какому из подботов следует передать сообщение. Ответ строго в формате JSON:
|
||||||
|
|
||||||
|
{
|
||||||
|
"bot": "название_бота",
|
||||||
|
"reason": "короткое объяснение маршрута"
|
||||||
|
}
|
||||||
|
|
||||||
|
Вот доступные подботы и их специализация в формаьте (- {name}: {description}):
|
||||||
|
|
||||||
|
${botsBlock}
|
||||||
|
|
||||||
|
Если сообщение не удаётся однозначно отнести ни к одному боту или нет бота способного обработать это исключение, укажи "bot": "unknown".
|
||||||
|
|
||||||
|
|
||||||
|
`.trim()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// rmq.js
|
||||||
|
import amqplib from 'amqplib';
|
||||||
|
|
||||||
|
let channel;
|
||||||
|
export async function initRabbit(url) {
|
||||||
|
const conn = await amqplib.connect(url);
|
||||||
|
channel = await conn.createChannel();
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChannel() {
|
||||||
|
if (!channel) throw new Error('RabbitMQ channel not initialized');
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"admin",
|
||||||
|
"deamon"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"amqplib": "^0.10.8",
|
||||||
|
"dotenv": "^17.2.0",
|
||||||
|
"node-telegram-bot-api": "^0.66.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,4 @@
|
||||||
|
packages:
|
||||||
|
- 'admin'
|
||||||
|
- 'deamon'
|
||||||
|
|
||||||
Loading…
Reference in New Issue