Участники чата, промпты в файлах, личности только из кода

- Бот запоминает участников (имя, фамилия, ник, активность) и 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>
This commit is contained in:
yunogasai
2026-09-24 18:42:23 +02:00
parent 99c4660da9
commit 26da4bb5ee
35 changed files with 567 additions and 241 deletions

View File

@@ -0,0 +1,87 @@
import type { ChatMemberRepository } from "../repositories/chat-member-repository.js";
import type { ChatMember, MemberProfile } from "../types.js";
interface ChatMemberServiceDeps {
chatMemberRepository: ChatMemberRepository;
}
export interface MentionQuery {
/** Ники из `@username` без собаки. */
usernames: string[];
/** Пользователи из упоминаний по имени-ссылке (у них может не быть ника). */
profiles: MemberProfile[];
}
export interface ResolvedMentions {
members: ChatMember[];
/** Ники, которых бот в этом чате не встречал. */
unknownUsernames: string[];
}
/** Регистр и «ё» не важны: «Алёна» найдётся по «алена». */
const normalize = (value: string): string => value.toLocaleLowerCase("ru").replaceAll("ё", "е").trim();
const words = (member: ChatMember): string[] =>
[member.firstName, member.lastName ?? "", member.username ?? ""].flatMap((part) => normalize(part).split(/\s+/));
const usernameIs = (username: string) => (member: ChatMember) =>
member.username !== null && normalize(member.username) === normalize(username);
/** Каждое слово запроса — начало одного из слов имени или ника: «петр» найдёт «Петров». */
const nameMatches = (query: string) => (member: ChatMember) => {
const memberWords = words(member);
return normalize(query)
.split(/\s+/)
.filter(Boolean)
.every((token) => memberWords.some((word) => word.startsWith(token)));
};
const uniqueByUser = (members: ChatMember[]): ChatMember[] =>
members.filter((member, index) => members.findIndex((m) => m.userId === member.userId) === index);
export const createChatMemberService = ({ chatMemberRepository }: ChatMemberServiceDeps) => {
/** Участник написал сообщение: обновляем имя, ник, счётчик и время активности. */
const recordActivity = (chatId: number, profile: MemberProfile, date: number): void =>
chatMemberRepository.upsert(chatId, profile, { messages: 1, seenAt: date });
/** Запоминаем имя и ник без учёта активности (правка сообщения, упоминание по ссылке). */
const remember = (chatId: number, profile: MemberProfile): void =>
chatMemberRepository.upsert(chatId, profile, { messages: 0, seenAt: 0 });
const list = (chatId: number): ChatMember[] => chatMemberRepository.listByChat(chatId);
/** Поиск по нику (с @ или без) или по имени/фамилии; сначала — недавно активные. */
const search = (chatId: number, query: string): ChatMember[] => {
const cleaned = query.trim().replace(/^@/, "");
if (!cleaned) return [];
const members = list(chatId);
const byUsername = members.filter(usernameIs(cleaned));
return byUsername.length > 0 ? byUsername : members.filter(nameMatches(cleaned));
};
/** Превращает упоминания из вопроса в известных участников чата. */
const resolveMentions = (chatId: number, { usernames, profiles }: MentionQuery): ResolvedMentions => {
profiles.forEach((profile) => remember(chatId, profile));
const members = list(chatId);
const byUsername = usernames.map((username) => ({ username, member: members.find(usernameIs(username)) }));
const byProfile = profiles
.map((profile) => chatMemberRepository.findById(chatId, profile.userId))
.filter((member) => member !== undefined);
return {
members: uniqueByUser([...byUsername.flatMap(({ member }) => (member ? [member] : [])), ...byProfile]),
unknownUsernames: byUsername.filter(({ member }) => !member).map(({ username }) => username),
};
};
return { recordActivity, remember, list, search, resolveMentions };
};
export type ChatMemberService = ReturnType<typeof createChatMemberService>;