Telegram-бот Boltun AI на grammY + Mastra

- Отвечает только администраторам из ADMIN_IDS
- Личности на чат: встроенные и пользовательские (/personas, /persona, /persona_add, /persona_del)
- Вопросы через /ask, упоминание, ответ боту или личку
- Сохраняет сообщения чата в SQLite; агент по просьбе читает их инструментом read_chat_messages
- Модели через OpenRouter (по умолчанию DeepSeek)
- Слои: libs, repositories, services, bot/handlers
- Dockerfile для деплоя в Dokploy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yunogasai
2026-09-24 18:31:44 +02:00
commit 99c4660da9
32 changed files with 3173 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import type { Db } from "../libs/db.js";
import type { ChatMessage } from "../types.js";
interface MessageRow {
chat_id: number;
message_id: number;
date: number;
author: string;
text: string;
reply_to_message_id: number | null;
}
const toMessage = (row: MessageRow): ChatMessage => ({
chatId: row.chat_id,
messageId: row.message_id,
date: row.date,
author: row.author,
text: row.text,
replyToMessageId: row.reply_to_message_id,
});
export const createMessageRepository = (db: Db) => {
const statements = {
upsert: db.prepare(`
INSERT INTO messages (chat_id, message_id, date, author, text, reply_to_message_id)
VALUES (@chatId, @messageId, @date, @author, @text, @replyToMessageId)
ON CONFLICT(chat_id, message_id) DO UPDATE SET text = excluded.text
`),
pruneChat: db.prepare(`
DELETE FROM messages WHERE chat_id = ? AND message_id <= (
SELECT message_id FROM messages WHERE chat_id = ?
ORDER BY message_id DESC LIMIT 1 OFFSET ?
)
`),
latest: db.prepare("SELECT * FROM messages WHERE chat_id = ? ORDER BY message_id DESC LIMIT ?"),
findById: db.prepare("SELECT * FROM messages WHERE chat_id = ? AND message_id = ?"),
};
/** Сохраняет сообщение (при повторе — обновляет текст) и оставляет в чате не больше `keep` последних. */
const upsertAndPrune = db.transaction((message: ChatMessage, keep: number) => {
statements.upsert.run(message);
statements.pruneChat.run(message.chatId, message.chatId, keep);
});
/** Последние `limit` сообщений чата в хронологическом порядке. */
const latest = (chatId: number, limit: number): ChatMessage[] =>
(statements.latest.all(chatId, limit) as MessageRow[]).reverse().map(toMessage);
const findById = (chatId: number, messageId: number): ChatMessage | undefined => {
const row = statements.findById.get(chatId, messageId) as MessageRow | undefined;
return row && toMessage(row);
};
return { upsertAndPrune, latest, findById };
};
export type MessageRepository = ReturnType<typeof createMessageRepository>;