Упрощение: общие хелперы, команды и упоминания по entities, фото только для моделей со зрением

- Одна formatUtc и один displayName(profile) вместо копий в разных слоях
- commandOf/mentionsUser/textWithoutMention работают по entities для текста и подписей;
  убраны askInCaption, regex-поиск упоминаний и отдельный composer.command("ask")
- Фото скачиваются только если модель их видит; пустой вопрос с картинкой формулирует промпт
- Поддержка фото задаётся явно для моделей по умолчанию, для своей AI_MODEL — через AI_IMAGE_INPUT
- Активность участников считается по сохранённой истории, а не отдельными счётчиками;
  один хендлер для message и edited_message, упоминания не пишутся дважды
- SQLite: synchronous = NORMAL для WAL
- Кнопки не-админов отклоняются через bot.drop(isAdmin)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yunogasai
2026-09-24 20:10:56 +02:00
parent f8273e8a23
commit e812f1b1e2
21 changed files with 269 additions and 266 deletions

View File

@@ -1,4 +1,4 @@
import type { ChatMember, ChatMessage, ImageAttachment, Persona } from "../../types.js";
import type { ChatMember, ChatMessage, ImageAttachment, ImageSource, Persona } from "../../types.js";
import { formatChatMembers } from "./format-chat-members.js";
import { formatChatMessages } from "./format-chat-messages.js";
@@ -7,6 +7,7 @@ export interface Question {
chatTitle: string;
/** Как зовут спрашивающего: имя, фамилия и ник. */
askerName: string;
/** Может быть пустым, если прислали только картинку. */
text: string;
/** Сообщение, на которое ответил спрашивающий, — сразу передаём его как контекст. */
repliedTo?: ChatMessage;
@@ -14,20 +15,22 @@ export interface Question {
mentionedMembers: ChatMember[];
/** Упомянутые ники, которых бот в чате не встречал. */
unknownUsernames: string[];
/** Фото из вопроса и из сообщения, на которое ответили. */
/** Где в вопросе есть картинки — даже если модели их не отправляем. */
imageSources: ImageSource[];
/** Скачанные картинки для модели; пусто, если модель их не видит или скачать не вышло. */
images: ImageAttachment[];
}
const IMAGE_SOURCES: Record<ImageAttachment["source"], string> = {
const IMAGE_SOURCES: Record<ImageSource, string> = {
question: "из вопроса",
replied: "из сообщения, на которое ответили",
};
/** Подсказка модели, откуда картинки — или что посмотреть их она не может. */
const imagesNote = (images: ImageAttachment[], canSeeImages: boolean): string | undefined => {
if (images.length === 0) return undefined;
const imagesNote = ({ imageSources, images }: Question): string | undefined => {
if (imageSources.length === 0) return undefined;
if (!canSeeImages) return "К сообщению приложено изображение, но ты не можешь его просмотреть — честно скажи об этом.";
if (images.length === 0) return "К сообщению приложено изображение, но ты не можешь его просмотреть — честно скажи об этом.";
return `Приложены изображения (по порядку): ${images.map((image) => IMAGE_SOURCES[image.source]).join(", ")}.`;
};
@@ -54,12 +57,12 @@ export const buildInstructions = (systemRules: string, question: Question, perso
.filter(Boolean)
.join("\n\n");
export const buildPrompt = (question: Question, canSeeImages: boolean): string => {
export const buildPrompt = (question: Question): string => {
const replied = question.repliedTo
? `Вопрос задан в ответ на сообщение:\n${formatChatMessages([question.repliedTo])}`
: undefined;
const text = replied ? `Вопрос: ${question.text}` : question.text;
const asked = question.text || "(текста нет — прислали только изображение, опиши и прокомментируй его)";
return [replied, imagesNote(question.images, canSeeImages), text].filter(Boolean).join("\n\n");
return [replied, imagesNote(question), replied ? `Вопрос: ${asked}` : asked].filter(Boolean).join("\n\n");
};