diff --git a/write-server/Dockerfile b/write-server/Dockerfile index f59f1db8..2083f908 100644 --- a/write-server/Dockerfile +++ b/write-server/Dockerfile @@ -36,7 +36,7 @@ FROM node:20-alpine AS runner WORKDIR /app # Install system dependencies (gcompat for glibc Hugo binary) -RUN apk add --no-cache git openssh-client su-exec gcompat +RUN apk add --no-cache git openssh-client su-exec gcompat ffmpeg # Install Hugo 0.128.2 extended ARG HUGO_VERSION=0.128.2 diff --git a/write-server/src/lib/bot/poll.ts b/write-server/src/lib/bot/poll.ts index 540c979c..69cb85b4 100644 --- a/write-server/src/lib/bot/poll.ts +++ b/write-server/src/lib/bot/poll.ts @@ -233,6 +233,7 @@ async function handleDocument(chatId: number, fileId: string, fileName?: string, try { const fs = await import("fs"); const path = await import("path"); + const { execSync } = await import("child_process"); const { getSession } = await import("./sessions"); const s = getSession(chatId); @@ -254,32 +255,47 @@ async function handleDocument(chatId: number, fileId: string, fileName?: string, const articleDir = s.dirPath; if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true }); - const ext = fileName?.split(".").pop() || filePath.split(".").pop() || "gif"; - const filename = Date.now() + "-" + fileId.slice(0, 8) + "." + ext; - fs.writeFileSync(path.join(articleDir, filename), Buffer.from(fileBuffer)); + const ext = fileName?.split(".").pop() || filePath.split(".").pop() || "bin"; + const baseFilename = Date.now() + "-" + fileId.slice(0, 8); + const filename = baseFilename + "." + ext; + const fullPath = path.join(articleDir, filename); + fs.writeFileSync(fullPath, Buffer.from(fileBuffer)); - // 根据 Telegram 消息类型判断,而不是猜文件名 - const extLower = ext.toLowerCase(); - const isImage = ["gif", "webp", "png", "jpg", "jpeg"].includes(extLower); - const isVideo = ["mp4", "webm", "mov", "avi", "mkv", "flv"].includes(extLower); const fileCaption = caption || fileName || "file"; - if (isImage) { - appendContent(chatId, `\n![${fileCaption}](${filename})\n`); - } else if (fileType === "animation") { - // Telegram GIF 动画 → 自动播放、循环、静音 - appendContent(chatId, `\n{{< video src="${filename}" loop="true" muted="true" >}}\n`); - await sendMessage(chatId, `🎞️ GIF 已保存:${filename}`); + if (fileType === "animation") { + // Telegram GIF → ffmpeg 转成真正的 GIF + const gifFilename = baseFilename + ".gif"; + const gifPath = path.join(articleDir, gifFilename); + try { + await sendMessage(chatId, "🎞️ 正在转换 GIF..."); + execSync(`ffmpeg -y -i "${fullPath}" -vf "fps=15,scale=480:-1:flags=lanczos" -loop 0 "${gifPath}"`, { timeout: 60000 }); + fs.unlinkSync(fullPath); + appendContent(chatId, `\n![${fileCaption}](${gifFilename})\n`); + await sendMessage(chatId, `🎞️ GIF 已转换:${gifFilename}`); + } catch (e) { + console.error("[bot:gif-convert]", e instanceof Error ? e.message : String(e)); + appendContent(chatId, `\n{{< video src="${filename}" loop="true" muted="true" >}}\n`); + await sendMessage(chatId, `🎞️ GIF 转换失败,已保存为视频:${filename}`); + } return; - } else if (fileType === "video" || isVideo) { - // 普通视频 → 带控制栏 + } else if (fileType === "video") { appendContent(chatId, `\n{{< video src="${filename}" caption="${fileCaption}" >}}\n`); await sendMessage(chatId, `🎬 视频已保存:${filename}`); return; } else { - appendContent(chatId, `\n[${fileCaption}](${filename})\n`); + const extLower = ext.toLowerCase(); + const isImage = ["gif", "webp", "png", "jpg", "jpeg"].includes(extLower); + const isVideo = ["mp4", "webm", "mov", "avi", "mkv", "flv"].includes(extLower); + if (isImage) { + appendContent(chatId, `\n![${fileCaption}](${filename})\n`); + } else if (isVideo) { + appendContent(chatId, `\n{{< video src="${filename}" caption="${fileCaption}" >}}\n`); + } else { + appendContent(chatId, `\n[${fileCaption}](${filename})\n`); + } + await sendMessage(chatId, `📎 文件已保存:${filename}`); } - await sendMessage(chatId, `📎 文件已保存:${filename}`); } catch (e) { console.error("[bot:document]", e instanceof Error ? e.message : String(e)); await sendMessage(chatId, "❌ 文件保存失败");