Skip to content

Commit ea9d80e

Browse files
committed
fix: recognize images in attached files
1 parent a9b185a commit ea9d80e

8 files changed

Lines changed: 122 additions & 12 deletions

File tree

src/bot/handlers/document.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,42 @@ export async function handleDocumentMessage(
7676
return;
7777
}
7878

79+
if (mimeType.startsWith("image/")) {
80+
const storedModel = getStored();
81+
const capabilities = await getCapabilities(storedModel.providerID, storedModel.modelID);
82+
83+
if (!supportsInput(capabilities, "image")) {
84+
logger.warn(
85+
`[Document] Model ${storedModel.providerID}/${storedModel.modelID} doesn't support image input`,
86+
);
87+
await ctx.reply(t("bot.photo_model_no_image"));
88+
89+
if (caption.trim().length > 0) {
90+
await processPrompt(ctx, caption, deps);
91+
}
92+
return;
93+
}
94+
95+
await ctx.reply(t("bot.file_downloading"));
96+
const downloadedFile = await downloadFile(ctx.api, doc.file_id);
97+
98+
const dataUri = toDataUri(downloadedFile.buffer, mimeType);
99+
100+
const filePart: FilePartInput = {
101+
type: "file",
102+
mime: mimeType,
103+
filename: filename,
104+
url: dataUri,
105+
};
106+
107+
logger.info(
108+
`[Document] Sending image (${downloadedFile.buffer.length} bytes, ${filename}, ${mimeType}) with prompt`,
109+
);
110+
111+
await processPrompt(ctx, caption, deps, [filePart]);
112+
return;
113+
}
114+
79115
if (mimeType === "application/pdf") {
80116
const storedModel = getStored();
81117
const capabilities = await getCapabilities(storedModel.providerID, storedModel.modelID);
@@ -112,7 +148,8 @@ export async function handleDocumentMessage(
112148
return;
113149
}
114150

115-
logger.debug(`[Document] Unsupported document MIME type: ${mimeType}, ignoring`);
151+
logger.warn(`[Document] Unsupported document MIME type: ${mimeType}, filename=${filename}`);
152+
await ctx.reply(t("bot.file_type_unsupported"));
116153
} catch (err) {
117154
logger.error("[Document] Error handling document message:", err);
118155
await ctx.reply(t("bot.file_download_error"));

src/i18n/de.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ export const de: I18nDictionary = {
9696
"bot.file_downloading": "⏳ Lade Datei herunter...",
9797
"bot.file_too_large": "⚠️ Datei ist zu groß (max. {maxSizeMb}MB)",
9898
"bot.file_download_error": "🔴 Datei konnte nicht heruntergeladen werden",
99+
"bot.file_type_unsupported":
100+
"⚠️ Dieser Dateityp wird nicht unterstützt. Sende ein Bild, PDF oder eine Text-/Code-Datei.",
99101
"bot.model_no_pdf": "⚠️ Das aktuelle Modell unterstützt keine PDF-Eingabe. Sende nur Text.",
100102
"bot.text_file_too_large": "⚠️ Textdatei ist zu groß (max. {maxSizeKb}KB)",
101103

src/i18n/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export const en = {
8989
"bot.file_downloading": "⏳ Downloading file...",
9090
"bot.file_too_large": "⚠️ File is too large (max {maxSizeMb}MB)",
9191
"bot.file_download_error": "🔴 Failed to download file",
92+
"bot.file_type_unsupported":
93+
"⚠️ This file type is not supported. Send an image, PDF, or text/code file.",
9294
"bot.model_no_pdf": "⚠️ Current model doesn't support PDF input. Sending text only.",
9395
"bot.text_file_too_large": "⚠️ Text file is too large (max {maxSizeKb}KB)",
9496

src/i18n/es.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ export const es: I18nDictionary = {
9696
"bot.file_downloading": "⏳ Descargando archivo...",
9797
"bot.file_too_large": "⚠️ El archivo es demasiado grande (max {maxSizeMb}MB)",
9898
"bot.file_download_error": "🔴 No se pudo descargar el archivo",
99+
"bot.file_type_unsupported":
100+
"⚠️ Este tipo de archivo no es compatible. Envía una imagen, PDF o archivo de texto/código.",
99101
"bot.model_no_pdf": "⚠️ El modelo actual no admite entrada PDF. Enviaré solo texto.",
100102
"bot.text_file_too_large": "⚠️ El archivo de texto es demasiado grande (max {maxSizeKb}KB)",
101103

src/i18n/fr.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export const fr: I18nDictionary = {
9797
"bot.file_downloading": "⏳ Téléchargement du fichier...",
9898
"bot.file_too_large": "⚠️ Le fichier est trop volumineux (max {maxSizeMb}MB)",
9999
"bot.file_download_error": "🔴 Impossible de télécharger le fichier",
100+
"bot.file_type_unsupported":
101+
"⚠️ Ce type de fichier n'est pas pris en charge. Envoyez une image, un PDF ou un fichier texte/code.",
100102
"bot.model_no_pdf":
101103
"⚠️ Le modèle actuel ne prend pas en charge les PDF. Envoi du texte uniquement.",
102104
"bot.text_file_too_large": "⚠️ Le fichier texte est trop volumineux (max {maxSizeKb}KB)",

src/i18n/ru.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export const ru: I18nDictionary = {
8989
"bot.file_downloading": "⏳ Скачиваю файл...",
9090
"bot.file_too_large": "⚠️ Файл слишком большой (макс. {maxSizeMb}МБ)",
9191
"bot.file_download_error": "🔴 Не удалось скачать файл",
92+
"bot.file_type_unsupported":
93+
"⚠️ Этот тип файла не поддерживается. Отправьте изображение, PDF или текстовый/кодовый файл.",
9294
"bot.model_no_pdf": "⚠️ Текущая модель не поддерживает PDF. Отправляю только текст.",
9395
"bot.text_file_too_large": "⚠️ Текстовый файл слишком большой (макс. {maxSizeKb}КБ)",
9496

src/i18n/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const zh: I18nDictionary = {
8080
"bot.file_downloading": "⏳ 正在下载文件...",
8181
"bot.file_too_large": "⚠️ 文件过大(最大 {maxSizeMb}MB)",
8282
"bot.file_download_error": "🔴 下载文件失败",
83+
"bot.file_type_unsupported": "⚠️ 不支持此文件类型。请发送图片、PDF 或文本/代码文件。",
8384
"bot.model_no_pdf": "⚠️ 当前模型不支持PDF输入。将仅发送文本。",
8485
"bot.text_file_too_large": "⚠️ 文本文件过大(最大 {maxSizeKb}KB)",
8586

tests/bot/handlers/document.test.ts

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -250,41 +250,103 @@ describe("bot/handlers/document", () => {
250250
});
251251
});
252252

253-
describe("unsupported file types", () => {
254-
it("ignores unsupported MIME types silently", async () => {
253+
describe("image files", () => {
254+
it("downloads and sends image documents when model supports images", async () => {
255255
const { ctx, replyMock } = createDocumentContext({
256256
document: {
257-
file_id: "zip-file-id",
258-
file_unique_id: "zip-unique-id",
259-
file_name: "archive.zip",
260-
mime_type: "application/zip",
257+
file_id: "image-file-id",
258+
file_unique_id: "image-unique-id",
259+
file_name: "photo.png",
260+
mime_type: "image/png",
261261
file_size: 5000,
262262
},
263+
caption: "Describe this image",
263264
});
264265
const { deps, processPromptMock, downloadMock } = createDocumentDeps();
265266

266267
await handleDocumentMessage(ctx, deps);
267268

268-
expect(replyMock).not.toHaveBeenCalled();
269+
expect(replyMock).toHaveBeenCalledWith(t("bot.file_downloading"));
270+
expect(downloadMock).toHaveBeenCalled();
271+
expect(processPromptMock).toHaveBeenCalledWith(
272+
ctx,
273+
"Describe this image",
274+
deps,
275+
expect.arrayContaining([
276+
expect.objectContaining({
277+
type: "file",
278+
mime: "image/png",
279+
filename: "photo.png",
280+
url: expect.stringMatching(/^data:image\/png;base64,/),
281+
}),
282+
]),
283+
);
284+
});
285+
286+
it("shows error when model does not support images", async () => {
287+
const { ctx, replyMock } = createDocumentContext({
288+
document: {
289+
file_id: "image-file-id",
290+
file_unique_id: "image-unique-id",
291+
file_name: "photo.png",
292+
mime_type: "image/png",
293+
file_size: 5000,
294+
},
295+
});
296+
const { deps, processPromptMock, downloadMock } = createDocumentDeps({
297+
getModelCapabilities: vi.fn().mockResolvedValue({
298+
input: { image: false },
299+
}),
300+
});
301+
302+
await handleDocumentMessage(ctx, deps);
303+
304+
expect(replyMock).toHaveBeenCalledWith(t("bot.photo_model_no_image"));
269305
expect(downloadMock).not.toHaveBeenCalled();
270306
expect(processPromptMock).not.toHaveBeenCalled();
271307
});
272308

273-
it("ignores image files", async () => {
274-
const { ctx, replyMock } = createDocumentContext({
309+
it("sends caption-only when model does not support images but caption exists", async () => {
310+
const { ctx } = createDocumentContext({
275311
document: {
276312
file_id: "image-file-id",
277313
file_unique_id: "image-unique-id",
278314
file_name: "photo.png",
279315
mime_type: "image/png",
280316
file_size: 5000,
281317
},
318+
caption: "Describe this image",
319+
});
320+
const { deps, processPromptMock, downloadMock } = createDocumentDeps({
321+
getModelCapabilities: vi.fn().mockResolvedValue({
322+
input: { image: false },
323+
}),
282324
});
283-
const { deps, processPromptMock } = createDocumentDeps();
284325

285326
await handleDocumentMessage(ctx, deps);
286327

287-
expect(replyMock).not.toHaveBeenCalled();
328+
expect(downloadMock).not.toHaveBeenCalled();
329+
expect(processPromptMock).toHaveBeenCalledWith(ctx, "Describe this image", deps);
330+
});
331+
});
332+
333+
describe("unsupported file types", () => {
334+
it("shows error for unsupported MIME types", async () => {
335+
const { ctx, replyMock } = createDocumentContext({
336+
document: {
337+
file_id: "zip-file-id",
338+
file_unique_id: "zip-unique-id",
339+
file_name: "archive.zip",
340+
mime_type: "application/zip",
341+
file_size: 5000,
342+
},
343+
});
344+
const { deps, processPromptMock, downloadMock } = createDocumentDeps();
345+
346+
await handleDocumentMessage(ctx, deps);
347+
348+
expect(replyMock).toHaveBeenCalledWith(t("bot.file_type_unsupported"));
349+
expect(downloadMock).not.toHaveBeenCalled();
288350
expect(processPromptMock).not.toHaveBeenCalled();
289351
});
290352
});

0 commit comments

Comments
 (0)