Files
boltun-ai/src/repositories/message-repository.ts
yunogasai b8f09227b0 Контекст диалога отдельно от архива чата
- Новая таблица dialog_messages: последние DIALOG_LIMIT пар «вопрос — ответ», подкладываются в каждый запрос
- /clear сбрасывает только контекст диалога, архив (включая импорт) не трогает; без подтверждения
- /context показывает контекст и архив по отдельности

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:42:14 +02:00

91 lines
4.1 KiB
TypeScript

import type { Db } from "../libs/db.js";
import type { ChatMessage } from "../types.js";
interface MessageRow {
chat_id: number;
message_id: number;
user_id: number | null;
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,
userId: row.user_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, user_id, date, author, text, reply_to_message_id)
VALUES (@chatId, @messageId, @userId, @date, @author, @text, @replyToMessageId)
ON CONFLICT(chat_id, message_id) DO UPDATE SET text = excluded.text
`),
insertIfMissing: db.prepare(`
INSERT OR IGNORE INTO messages (chat_id, message_id, user_id, date, author, text, reply_to_message_id)
VALUES (@chatId, @messageId, @userId, @date, @author, @text, @replyToMessageId)
`),
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 ?"),
latestByUser: db.prepare(
"SELECT * FROM messages WHERE chat_id = ? AND user_id = ? ORDER BY message_id DESC LIMIT ?",
),
findById: db.prepare("SELECT * FROM messages WHERE chat_id = ? AND message_id = ?"),
statsByChat: db.prepare("SELECT COUNT(*) AS count, MIN(date) AS oldest FROM messages WHERE chat_id = ?"),
};
const toChronological = (rows: unknown[]): ChatMessage[] => (rows as MessageRow[]).reverse().map(toMessage);
/** Сохраняет сообщение (при повторе — обновляет текст) и оставляет в чате не больше `keep` последних. */
const upsertAndPrune = db.transaction((message: ChatMessage, keep: number) => {
statements.upsert.run(message);
statements.pruneChat.run(message.chatId, message.chatId, keep);
});
/**
* Массовая загрузка (импорт): уже сохранённые сообщения не трогаем — живые данные точнее экспорта.
* Возвращает, сколько сообщений добавлено; затем историю чата обрезаем до `keep` последних.
*/
const insertManyAndPrune = db.transaction((chatId: number, messages: ChatMessage[], keep: number): number => {
const inserted = messages.reduce((sum, message) => sum + statements.insertIfMissing.run(message).changes, 0);
statements.pruneChat.run(chatId, chatId, keep);
return inserted;
});
/** Последние `limit` сообщений чата в хронологическом порядке. */
const latest = (chatId: number, limit: number): ChatMessage[] =>
toChronological(statements.latest.all(chatId, limit));
/** Последние `limit` сообщений конкретного участника в хронологическом порядке. */
const latestByUser = (chatId: number, userId: number, limit: number): ChatMessage[] =>
toChronological(statements.latestByUser.all(chatId, userId, limit));
const findById = (chatId: number, messageId: number): ChatMessage | undefined => {
const row = statements.findById.get(chatId, messageId) as MessageRow | undefined;
return row && toMessage(row);
};
/** Сколько сообщений чата сохранено и дата самого старого (null — если пусто). */
const statsByChat = (chatId: number): { count: number; oldest: number | null } =>
statements.statsByChat.get(chatId) as { count: number; oldest: number | null };
return { upsertAndPrune, insertManyAndPrune, latest, latestByUser, findById, statsByChat };
};
export type MessageRepository = ReturnType<typeof createMessageRepository>;