Участники чата, промпты в файлах, личности только из кода
- Бот запоминает участников (имя, фамилия, ник, активность) и 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:
87
src/services/chat-member-service.ts
Normal file
87
src/services/chat-member-service.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user