diff --git a/write-server/next-env.d.ts b/write-server/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/write-server/next-env.d.ts +++ b/write-server/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/write-server/src/lib/bot/handlers.ts b/write-server/src/lib/bot/handlers.ts index efd1d555..c81cd949 100644 --- a/write-server/src/lib/bot/handlers.ts +++ b/write-server/src/lib/bot/handlers.ts @@ -201,9 +201,7 @@ handlers.help = async (chatId) => { `├─ 🎙️ 语音消息:自动转文字 + 生成语音播放器`, `├─ 🎵 MP3 文件:交互式问答生成音乐卡片`, `├─ 📷 普通图片:直接插入文章`, - `├─ 📷 HEIC 图片(iOS 实况照片):`, - `│ └─ 动态效果无法自动提取`, - `│ └─ 需先转为 GIF/视频再发送`, + `├─ 📷 Live Photo(iOS HEIC):自动转换为视频 + 生成 livephoto 播放器`, `├─ 🎬 视频/GIF:直接插入文章`, `└─ 📄 文档:保存到文章目录`, ].join("\n")); diff --git a/write-server/src/lib/bot/poll.ts b/write-server/src/lib/bot/poll.ts index e5e421b0..f19063bf 100644 --- a/write-server/src/lib/bot/poll.ts +++ b/write-server/src/lib/bot/poll.ts @@ -502,13 +502,14 @@ async function handleAudioEnhanced(chatId: number, fileId: string, fileName?: st } // ============================================================ -// 新增:处理图片(增强版:支持封面图上传 + 实况照片引导) +// 新增:处理图片(增强版:支持封面图上传 + Live Photo 自动转换) // ============================================================ async function handlePhotoEnhanced(chatId: number, photos: { file_id: string }[], caption?: 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); @@ -536,27 +537,64 @@ async function handlePhotoEnhanced(chatId: number, photos: { file_id: string }[] if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true }); const ext = filePath.split(".").pop() || "jpg"; - const filename = Date.now() + "-" + largestPhoto.file_id.slice(0, 8) + "." + ext; - fs.writeFileSync(path.join(articleDir, filename), Buffer.from(imgBuffer)); + const baseFilename = Date.now() + "-" + largestPhoto.file_id.slice(0, 8); + const filename = baseFilename + "." + ext; + const fullPath = path.join(articleDir, filename); + fs.writeFileSync(fullPath, Buffer.from(imgBuffer)); - const imgCaption = caption || "image"; - appendContent(chatId, `\n![${imgCaption}](${filename})\n`); - - // 只对 HEIC 文件(iOS 实况照片)显示提示 + // HEIC 文件(iOS Live Photo)自动转换 const isHEIC = ext.toLowerCase() === "heic" || ext.toLowerCase() === "heif"; if (isHEIC) { - await sendMessage(chatId, `📷 图片已保存:${filename} + await sendMessage(chatId, "🎞️ 检测到 Live Photo,正在转换为视频..."); + + try { + // 转换 HEIC 为 MP4 视频 + const videoFilename = baseFilename + ".mp4"; + const videoPath = path.join(articleDir, videoFilename); + + // 使用 ffmpeg 转换 HEIC 为 MP4(保持原有帧率和质量) + execSync(`ffmpeg -y -i "${fullPath}" -c:v libx264 -crf 23 -preset medium -pix_fmt yuv420p "${videoPath}"`, { timeout: 60000 }); + + // 提取第一帧作为封面图 + const coverFilename = baseFilename + "-cover.jpg"; + const coverPath = path.join(articleDir, coverFilename); + execSync(`ffmpeg -y -i "${videoPath}" -vframes 1 -q:v 2 "${coverPath}"`, { timeout: 30000 }); + + // 删除原始 HEIC 文件 + fs.unlinkSync(fullPath); + + // 生成 livephoto 短代码 + const livePhotoShortcode = `\n\n{{< livephoto src="${videoFilename}" cover="${coverFilename}" >}}\n\n`; + appendContent(chatId, livePhotoShortcode); + + await sendMessage(chatId, `✅ Live Photo 转换完成! -💡 检测到 iOS 实况照片(HEIC 格式) +📹 视频:${videoFilename} +🖼️ 封面:${coverFilename} -动态效果无法自动提取。如需保留动态效果,请: +已插入 livephoto 短代码,支持点击播放动态效果。`, { parse_mode: "HTML" }); + + return; + } catch (convertError) { + console.error("[bot:heic-convert]", convertError instanceof Error ? convertError.message : String(convertError)); + // 转换失败,回退到普通图片处理 + appendContent(chatId, `\n![Live Photo](${filename})\n`); + await sendMessage(chatId, `📷 图片已保存:${filename} + +⚠️ Live Photo 转换失败 + +动态效果无法提取,已作为普通图片插入。如需动态效果,请: 1. 在 iPhone 上使用「快捷指令」将实况照片转为 GIF -2. 或使用第三方 App 导出为视频后重新发送 - -继续发送其他内容...`, { parse_mode: "HTML" }); - } else { - await sendMessage(chatId, `📷 图片已保存:${filename}`); +2. 或使用第三方 App 导出为视频后重新发送`, { parse_mode: "HTML" }); + return; + } } + + // 普通图片处理 + const imgCaption = caption || "image"; + appendContent(chatId, `\n![${imgCaption}](${filename})\n`); + await sendMessage(chatId, `📷 图片已保存:${filename}`); + } catch (e) { console.error("[bot:photo]", e instanceof Error ? e.message : String(e)); await sendMessage(chatId, "❌ 图片保存失败");