Files
boltun-ai/src/libs/telegram-format.ts
yunogasai 26da4bb5ee Участники чата, промпты в файлах, личности только из кода
- Бот запоминает участников (имя, фамилия, ник, активность) и ID автора каждого сообщения
- Упоминания в вопросе (@ник и по имени-ссылке) передаются модели с данными участника
- Новые инструменты агента: list_chat_members, read_member_messages (поиск по нику/имени)
- Бот обращается к людям по имени
- Промпты вынесены в prompts/system.md и prompts/personas/<id>.md
- Личности больше не редактируются из чата: скрипт seed синхронизирует их с БД при старте
- Оставлена одна личность boltun (Dirty D)
- Миграция старых баз: колонка messages.user_id

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

104 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Message, MessageEntity, User } from "grammy/types";
import type { ChatMessage, MemberProfile } from "../types.js";
const TELEGRAM_MESSAGE_LIMIT = 4096;
export const toMemberProfile = (user: User): MemberProfile => ({
userId: user.id,
username: user.username ?? null,
firstName: user.first_name,
lastName: user.last_name ?? null,
});
export const displayName = (user: User): string => {
const name = [user.first_name, user.last_name].filter(Boolean).join(" ");
return user.username ? `${name} (@${user.username})` : name;
};
const withLabel = (label: string, body: string | undefined): string => (body ? `${label} ${body}` : label);
/** Текстовое представление сообщения для истории; undefined — если сохранять нечего. */
export const messageText = (msg: Message): string | undefined => {
const body = msg.text ?? msg.caption;
if (msg.photo) return withLabel("[фото]", body);
if (msg.video) return withLabel("[видео]", body);
if (msg.voice) return "[голосовое сообщение]";
if (msg.video_note) return "[видеосообщение]";
if (msg.sticker) return withLabel("[стикер]", msg.sticker.emoji);
if (msg.document) return withLabel(`[файл ${msg.document.file_name ?? "без имени"}]`, body);
if (msg.poll) return withLabel("[опрос]", msg.poll.question);
return body;
};
/** В темах форума каждое сообщение формально отвечает на первое сообщение темы — это не настоящий ответ. */
const replyToMessageId = (msg: Message): number | null => {
const original = msg.reply_to_message;
if (!original || original.forum_topic_created) return null;
return original.message_id;
};
export const toChatMessage = (msg: Message): ChatMessage | undefined => {
const text = messageText(msg);
if (!text) return undefined;
return {
chatId: msg.chat.id,
messageId: msg.message_id,
userId: msg.from?.id ?? null,
date: msg.date,
author: msg.from ? displayName(msg.from) : (msg.sender_chat?.title ?? "Аноним"),
text,
replyToMessageId: replyToMessageId(msg),
};
};
const messageEntities = (msg: Message): { text: string; entities: MessageEntity[] } => ({
text: msg.text ?? msg.caption ?? "",
entities: msg.entities ?? msg.caption_entities ?? [],
});
/** Ники из упоминаний `@username` (без собаки), без повторов. */
export const mentionedUsernames = (msg: Message): string[] => {
const { text, entities } = messageEntities(msg);
const usernames = entities
.filter((entity) => entity.type === "mention")
.map((entity) => text.slice(entity.offset + 1, entity.offset + entity.length));
return [...new Set(usernames)];
};
/** Пользователи, упомянутые по имени-ссылке — так Telegram упоминает людей без ника. */
export const mentionedUsers = (msg: Message): User[] =>
messageEntities(msg).entities.flatMap((entity) => (entity.type === "text_mention" ? [entity.user] : []));
/** Режет длинный ответ на части под лимит Telegram, по возможности по переносам строк. */
export const splitMessage = (text: string): string[] => {
if (text.length <= TELEGRAM_MESSAGE_LIMIT) return text ? [text] : [];
const newline = text.lastIndexOf("\n", TELEGRAM_MESSAGE_LIMIT);
const cut = newline > TELEGRAM_MESSAGE_LIMIT / 2 ? newline : TELEGRAM_MESSAGE_LIMIT;
return [text.slice(0, cut), ...splitMessage(text.slice(cut).trimStart())];
};
const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
/** `@username` целиком: `@boltun_bot2` не считается упоминанием `@boltun_bot`. */
const mentionPattern = (username: string): RegExp => new RegExp(`@${escapeRegExp(username)}(?![a-z0-9_])`, "gi");
export const mentions = (text: string, username: string): boolean => mentionPattern(username).test(text);
export const removeMention = (text: string, username: string): string =>
text
.replace(mentionPattern(username), "")
.replace(/[ \t]{2,}/g, " ")
.replace(/[ \t]+([,.!?])/g, "$1")
.trim();