feat: separate Ollama text/image/think models + add YouTube downloader

Support OLLAMA_IMAGE_MODEL and new OLLAMA_THINK_MODEL (both default to OLLAMA_MODEL)

Add /ollamathink command; validate thinking capability; disable image-analysis flow in think mode

Make /ollama-get-model show Text/Image/Think blocks only when models differ

Add /ytdl (/youtube) command + auto-detect YouTube URLs in messages

Cache downloaded videos to data/video and schedule daily cleanup

Move photo storage from data/temp to data/photo; improve env boolean parsing via ifTrue

Update deps (youtubei.js, puppeteer*) and TS config (allowJs, allowSyntheticDefaultImports)
This commit is contained in:
2026-01-31 19:36:11 +03:00
parent 23052fae0f
commit 810151263d
14 changed files with 1921 additions and 66 deletions
+1
View File
@@ -81,6 +81,7 @@ export class MistralChat extends ChatCommand {
const stream = await mistralAi.chat.stream({
model: Environment.MISTRAL_MODEL,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
messages: chatMessages as any
});
+26 -7
View File
@@ -16,7 +16,7 @@ import {OllamaCancel} from "../callback_commands/ollama-cancel";
import {OllamaGetModel} from "./ollama-get-model";
export class OllamaChat extends ChatCommand {
command = "ollama";
command = ["ollama", "ollamathink"];
argsMode = "required" as const;
title = "/ollama";
@@ -24,10 +24,10 @@ export class OllamaChat extends ChatCommand {
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
console.log("match", match);
return this.executeOllama(msg, match?.[3]);
return this.executeOllama(msg, match?.[3], match?.[1]?.toLowerCase()?.startsWith("ollamathink"));
}
async executeOllama(msg: Message, text: string): Promise<void> {
async executeOllama(msg: Message, text: string, think: boolean = false): Promise<void> {
if (!text || text.trim().length === 0) return;
const chatId = msg.chat.id;
@@ -55,7 +55,7 @@ export class OllamaChat extends ChatCommand {
return total + (curr.images?.length ?? 0);
}, 0);
if (imagesCount) {
if (!think && imagesCount) {
try {
const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadImageModelInfo();
if (modelInfo) {
@@ -73,20 +73,38 @@ export class OllamaChat extends ChatCommand {
}
}
if (think) {
try {
const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadThinkModelInfo();
if (modelInfo) {
const caps = modelInfo.capabilities || [];
if (!caps.includes("thinking")) {
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 replyToMessage({
message: msg,
text: imagesCount ?
text: (!think && imagesCount) ?
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
: Environment.waitText
});
const stream = await ollama.chat({
model: imagesCount ? Environment.OLLAMA_IMAGE_MODEL : Environment.OLLAMA_MODEL,
model: think ? Environment.OLLAMA_THINK_MODEL : imagesCount ? Environment.OLLAMA_IMAGE_MODEL : Environment.OLLAMA_MODEL,
stream: true,
think: false,
think: think,
messages: chatMessages,
});
@@ -153,6 +171,7 @@ export class OllamaChat extends ChatCommand {
message_id: waitMessage.message_id,
text: "🤔 Размышляю...",
parse_mode: "Markdown",
reply_markup: cancelMarkup
}).catch(logError);
}
+43 -5
View File
@@ -11,14 +11,48 @@ export class OllamaGetModel extends ChatCommand {
async execute(msg: Message): Promise<void> {
try {
let modelInfo = await this.loadModelInfo();
const modelText = "```Text\n" + this.getModelText(Environment.OLLAMA_MODEL, modelInfo) + "```";
modelInfo = await this.loadImageModelInfo();
const imageModelText = "```Image\n" + this.getModelText(Environment.OLLAMA_IMAGE_MODEL, modelInfo) + "```";
const model = Environment.OLLAMA_MODEL;
const imageModel = Environment.OLLAMA_IMAGE_MODEL;
const thinkModel = Environment.OLLAMA_THINK_MODEL;
const promises: (Promise<ShowResponse | null> | null)[] = [this.loadModelInfo()];
if (imageModel && imageModel !== model) {
promises.push(this.loadImageModelInfo());
} else {
promises.push(null);
}
if (thinkModel && thinkModel !== model) {
promises.push(this.loadThinkModelInfo());
} else {
promises.push(null);
}
const infos = await Promise.all(promises);
let modelInfo = infos[0];
const modelText = "```Text\n" + this.getModelText(model, modelInfo) + "```";
modelInfo = infos[1];
const imageModelText = modelInfo ?
"```Image\n" + this.getModelText(imageModel, modelInfo) + "```" : null;
modelInfo = infos[2];
const thinkModelText = modelInfo ?
"```Think\n" + this.getModelText(thinkModel, modelInfo) + "```" : null;
const modelInfos = [modelText];
if (imageModelText) {
modelInfos.push(imageModelText);
}
if (thinkModelText) {
modelInfos.push(thinkModelText);
}
await replyToMessage({
message: msg,
text: modelText + "\n\n" + imageModelText,
text: modelInfos.join("\n\n"),
parse_mode: "Markdown"
}).catch(logError);
@@ -44,4 +78,8 @@ export class OllamaGetModel extends ChatCommand {
async loadImageModelInfo(): Promise<ShowResponse | null> {
return ollama.show({model: Environment.OLLAMA_IMAGE_MODEL});
}
async loadThinkModelInfo(): Promise<ShowResponse | null> {
return ollama.show({model: Environment.OLLAMA_THINK_MODEL});
}
}
+54
View File
@@ -0,0 +1,54 @@
import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {bot} from "../index";
import {downloadVideoFromYouTube} from "../util/ytdl";
export class YouTubeDownload extends ChatCommand {
command = ["ytdl", "youtube"];
argsMode = "required" as const;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const url = match?.[3];
return this.downloadYouTubeVideo(msg, url);
}
async downloadYouTubeVideo(msg: Message, url: string): Promise<void> {
let waitMessage: Message | null = null;
try {
waitMessage = await replyToMessage({message: msg, text: "⏳ Секунду..."});
const {time, exists, buffer} = await downloadVideoFromYouTube(url);
if (buffer) {
const start = Date.now();
waitMessage = await bot.editMessageMedia({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
media: {
type: "video",
media: buffer
}
}) as Message;
const diff = Date.now() - start;
waitMessage = await bot.editMessageCaption({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
caption: `✅ [Видео](${url})` + (exists ? " загружено из кэша" : " успешно скачано") + " за " + (time + diff) + "мс",
parse_mode: "MarkdownV2"
}) as Message;
}
} catch (e) {
logError(e);
if (waitMessage && "text" in waitMessage) {
await bot.editMessageText({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
text: `⚠️ Произошла ошибка.\n${e}`,
});
}
}
}
}