refactor(bot): centralize runtime state; support albums + safer vision handling

- make MessageStore.put() return StoredMessage and allow collectReplyChainText() to work with StoredMessage
- move muted users + answers loading into Environment (add Answers model + GEMINI_IMAGE_MODEL)
- extract message handling into processNewMessage() and add media-group (album) caching/downloading by unique_file_id
- for Ollama: check model capabilities before sending images; use replyToMessage helper consistently
- add /geminiGenImage command stub for Gemini image generation
This commit is contained in:
2026-01-29 20:01:30 +03:00
parent 5f4405c9ee
commit b74e0a0f3e
16 changed files with 312 additions and 148 deletions
+7 -2
View File
@@ -34,7 +34,8 @@ export class GeminiChat extends ChatCommand {
const chatId = msg.chat.id;
const messageParts = await collectReplyChainText(msg);
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map(part => {
@@ -64,7 +65,7 @@ export class GeminiChat extends ChatCommand {
if (messageParts[0].images?.length) {
const images = messageParts[0].images;
images.forEach(image=>{
images.forEach(image => {
const base64Image = Buffer.from(fs.readFileSync(image)).toString("base64");
input.push({
type: "image",
@@ -144,6 +145,10 @@ export class GeminiChat extends ChatCommand {
}
break;
}
case "image": {
const image = event.delta.data;
console.log("image", image);
}
}
}
}
+60
View File
@@ -0,0 +1,60 @@
import {ChatCommand} from "../base/chat-command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {googleAi} from "../index";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class GeminiGenerateImage extends ChatCommand {
command = "geminiGenImage";
argsMode = "required" as const;
title = "/geminiGenImage";
description = "Generate image with Gemini";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
const prompt = match?.[3];
return this.executeGenImage(msg, prompt);
}
async executeGenImage(msg: Message, text: string): Promise<void> {
if (!text || text.trim().length === 0) return;
let waitMessage: Message;
try {
waitMessage = await replyToMessage({
message: msg,
text: Environment.genImageText,
});
const interaction = await googleAi.interactions.create({
model: Environment.GEMINI_IMAGE_MODEL,
response_modalities: ["image"],
input: text,
});
interaction.outputs?.forEach((output, index) => {
if (output.type === "image") {
// const image = output.data;
console.log(`Image output ${index + 1}:`, output);
} else {
console.log(`Output ${index + 1}: ${output}`);
}
});
} catch (e) {
logError(e);
await replyToMessage({
message: waitMessage,
text: `Произошла ошибка!\n${e.toString()}`,
disableLinkPreview: true
}).catch(logError);
}
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
import {addMute} from "../db/database";
import {ChatCommand} from "../base/chat-command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
@@ -35,7 +34,7 @@ export class Ignore extends ChatCommand {
return;
}
if (await addMute(id)) {
if (await Environment.addMute(id)) {
await oldSendMessage(msg, text + " в муте! 🔇").catch(logError);
} else {
await oldSendMessage(msg, text + " уже в муте 🤔").catch(logError);
+5 -3
View File
@@ -7,6 +7,7 @@ import {
escapeMarkdownV2Text,
logError,
oldReplyToMessage,
photoPathByUniqueId,
startIntervalEditor
} from "../util/utils";
import {Environment} from "../common/environment";
@@ -33,7 +34,8 @@ export class MistralChat extends ChatCommand {
const chatId = msg.chat.id;
const messageParts = await collectReplyChainText(msg);
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map(part => {
@@ -43,8 +45,8 @@ export class MistralChat extends ChatCommand {
text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + part.content,
});
if (part.images && part.images.length > 0) {
const base64Image = Buffer.from(fs.readFileSync(part.images[0])).toString("base64");
for (const image of part.images) {
const base64Image = Buffer.from(fs.readFileSync(photoPathByUniqueId(image))).toString("base64");
content.push({
type: "image_url",
imageUrl: "data:image/jpeg;base64," + base64Image
+26 -10
View File
@@ -1,17 +1,19 @@
import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {abortOllamaRequest, bot, getOllamaRequest, ollama, ollamaRequests} from "../index";
import {abortOllamaRequest, bot, chatCommands, getOllamaRequest, ollama, ollamaRequests} from "../index";
import {
collectReplyChainText,
escapeMarkdownV2Text,
logError,
oldReplyToMessage,
replyToMessage,
startIntervalEditor
} from "../util/utils";
import {Environment} from "../common/environment";
import {MessageStore} from "../common/message-store";
import {Cancel} from "../callback_commands/cancel";
import {OllamaCancel} from "../callback_commands/ollama-cancel";
import {OllamaGetModel} from "./ollama-get-model";
export class OllamaChat extends ChatCommand {
command = "ollama";
@@ -30,7 +32,8 @@ export class OllamaChat extends ChatCommand {
const chatId = msg.chat.id;
const messageParts = await collectReplyChainText(msg);
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map(part => {
@@ -52,19 +55,32 @@ export class OllamaChat extends ChatCommand {
return total + (curr.images?.length ?? 0);
}, 0);
if (imagesCount) {
try {
const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadModelInfo();
if (modelInfo) {
const caps = modelInfo.capabilities || [];
if (!caps.includes("vision")) {
await replyToMessage({
message: msg,
text: "Моя текущая модель не умеет анализировать изображения 🥹"
});
return;
}
}
} catch (e) {
logError(e);
}
}
const uuid = crypto.randomUUID();
const cancelMarkup = {inline_keyboard: [[Cancel.withData(new OllamaCancel().data + " " + uuid).asButton()]]};
waitMessage = await bot.sendMessage({
chat_id: chatId,
waitMessage = await replyToMessage({
message: msg,
text: imagesCount ?
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
: Environment.waitText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
: Environment.waitText
});
const stream = await ollama.chat({
+6 -1
View File
@@ -3,6 +3,7 @@ import {Message} from "typescript-telegram-bot-api";
import {boolToEmoji, logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {ollama} from "../index";
import {ShowResponse} from "ollama";
export class OllamaGetModel extends ChatCommand {
title = "/ollamaGetModel";
@@ -10,7 +11,7 @@ export class OllamaGetModel extends ChatCommand {
async execute(msg: Message): Promise<void> {
try {
const showResponse = await ollama.show({model: Environment.OLLAMA_MODEL});
const showResponse = await this.loadModelInfo();
const caps = showResponse.capabilities;
@@ -27,4 +28,8 @@ export class OllamaGetModel extends ChatCommand {
await replyToMessage({message: msg, text: e.toString()}).catch(logError);
}
}
async loadModelInfo(): Promise<ShowResponse | null> {
return ollama.show({model: Environment.OLLAMA_MODEL});
}
}
+3 -3
View File
@@ -1,10 +1,10 @@
import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {logError, randomValue, oldReplyToMessage} from "../util/utils";
import {prefixAnswers} from "../db/database";
import {logError, randomValue, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class PrefixResponse extends ChatCommand {
async execute(msg: Message): Promise<void> {
await oldReplyToMessage(msg, randomValue(prefixAnswers)).catch(logError);
await replyToMessage({message: msg, text: randomValue(Environment.ANSWERS.prefix)}).catch(logError);
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {logError, randomValue, oldReplyToMessage} from "../util/utils";
import {testAnswers} from "../db/database";
import {logError, oldReplyToMessage, randomValue} from "../util/utils";
import {Environment} from "../common/environment";
export class Test extends ChatCommand {
regexp = /^(test|тест|еуые|ntcn|инноке(нтий|ш|нтич))/i;
@@ -9,6 +9,6 @@ export class Test extends ChatCommand {
description = "System functionality check";
async execute(msg: Message) {
await oldReplyToMessage(msg, randomValue(testAnswers) || "а").catch(logError);
await oldReplyToMessage(msg, randomValue(Environment.ANSWERS.test) || "а").catch(logError);
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
import {removeMute} from "../db/database";
import {ChatCommand} from "../base/chat-command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
@@ -34,7 +33,7 @@ export class Unignore extends ChatCommand {
return;
}
if (await removeMute(id)) {
if (await Environment.removeMute(id)) {
await oldSendMessage(msg, text + " больше не в муте! 🔈").catch(logError);
} else {
await oldSendMessage(msg, text + " не был в муте 🤔").catch(logError);
+2 -2
View File
@@ -1,7 +1,7 @@
import {ChatCommand} from "../base/chat-command";
import {logError, oldSendMessage, randomValue} from "../util/utils";
import {betterAnswers} from "../db/database";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
export class WhatBetter extends ChatCommand {
command = ["what", "что"];
@@ -19,7 +19,7 @@ export class WhatBetter extends ChatCommand {
const a = m[2].trim();
const b = m[4].trim();
const text = `${randomValue(betterAnswers)} ${randomValue([a, b])}`;
const text = `${randomValue(Environment.ANSWERS.better)} ${randomValue([a, b])}`;
await oldSendMessage(msg, text).catch(logError);
}