增加图片下载本地逻辑(暂未正式启用

This commit is contained in:
2026-09-07 13:34:40 +08:00
parent 909c666646
commit 8efd64bda4
5 changed files with 126 additions and 2 deletions
+1
View File
@@ -5,6 +5,7 @@
"packageManager": "pnpm@11.22.0", "packageManager": "pnpm@11.22.0",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"cache-images": "node scripts/cache-images.mjs",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint"
+84
View File
@@ -0,0 +1,84 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const contentRoot = path.join(root, "src", "data");
const outputRoot = path.join(root, "public", "media", "news");
const manifestPath = path.join(root, "src", "lib", "image-map.generated.ts");
const remoteUrlPattern = /https?:\/\/[^\s)>'"]+/g;
const imageExtensionPattern = /\.(?:avif|gif|jpe?g|png|svg|webp)(?:[?#].*)?$/i;
async function collectMarkdownFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) files.push(...await collectMarkdownFiles(entryPath));
else if (entry.isFile() && entry.name.endsWith(".md")) files.push(entryPath);
}
return files;
}
function cleanUrl(value) {
return value.replace(/[.,;:!?]+$/, "");
}
function getExtension(url, contentType) {
const pathname = new URL(url).pathname;
const extension = path.extname(pathname).toLowerCase();
if (extension && imageExtensionPattern.test(pathname)) return extension;
const typeExtension = contentType.split("/")[1]?.split(";")[0]?.toLowerCase();
return typeExtension === "jpeg" ? ".jpg" : `.${typeExtension || "bin"}`;
}
async function downloadImage(url) {
const response = await fetch(url, {
redirect: "follow",
signal: AbortSignal.timeout(15000),
});
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
const contentType = response.headers.get("content-type") || "application/octet-stream";
if (!contentType.startsWith("image/")) throw new Error(`unexpected content type ${contentType}`);
const bytes = Buffer.from(await response.arrayBuffer());
const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 16);
const fileName = `${hash}${getExtension(url, contentType)}`;
await writeFile(path.join(outputRoot, fileName), bytes);
return `/media/news/${fileName}`;
}
const markdownFiles = await collectMarkdownFiles(contentRoot);
const remoteUrls = new Set();
for (const filePath of markdownFiles) {
const content = await readFile(filePath, "utf8");
for (const match of content.matchAll(remoteUrlPattern)) {
const url = cleanUrl(match[0]);
if (imageExtensionPattern.test(url)) remoteUrls.add(url);
}
}
await mkdir(outputRoot, { recursive: true });
const imageMap = {};
let failed = 0;
const urls = [...remoteUrls];
const concurrency = 8;
for (let index = 0; index < urls.length; index += concurrency) {
const batch = urls.slice(index, index + concurrency);
await Promise.all(batch.map(async (url) => {
try {
imageMap[url] = await downloadImage(url);
} catch (error) {
failed += 1;
console.warn(`[cache-images] skipped ${url}: ${error.message}`);
}
}));
}
const generated = `// Generated by scripts/cache-images.mjs. Do not edit.\nexport const imageMap: Record<string, string> = ${JSON.stringify(imageMap, null, 2)};\n`;
await writeFile(manifestPath, generated, "utf8");
console.log(`[cache-images] cached ${Object.keys(imageMap).length}/${remoteUrls.size} images${failed ? `; ${failed} failed` : ""}`);
+3 -1
View File
@@ -2,6 +2,7 @@ import fs from "fs/promises";
import path from "path"; import path from "path";
import type { Locale } from "@/lib/i18n"; import type { Locale } from "@/lib/i18n";
import { markdownToHtml } from "@/lib/markdown"; import { markdownToHtml } from "@/lib/markdown";
import { imageMap } from "@/lib/image-map.generated";
export type NewsItem = { export type NewsItem = {
id: string; id: string;
@@ -71,7 +72,8 @@ function normalizeNewsItem(fileName: string, raw: NewsFrontmatter, body: string)
const date = raw.date?.trim() || "未注明日期"; const date = raw.date?.trim() || "未注明日期";
const category = (Array.isArray(raw.category) ? raw.category[0] : raw.category)?.trim() || "未分类"; const category = (Array.isArray(raw.category) ? raw.category[0] : raw.category)?.trim() || "未分类";
const tags = raw.tags ?? []; const tags = raw.tags ?? [];
const cover = raw.cover?.trim() || DEFAULT_NEWS_COVER; const coverSource = raw.cover?.trim() || DEFAULT_NEWS_COVER;
const cover = imageMap[coverSource] ?? coverSource;
const slug = raw.slug?.trim() || fileName.replace(/\.md$/i, ""); const slug = raw.slug?.trim() || fileName.replace(/\.md$/i, "");
if (!title) { if (!title) {
+35
View File
@@ -0,0 +1,35 @@
// Generated by scripts/cache-images.mjs. Do not edit.
export const imageMap: Record<string, string> = {
"https://r2-image.kivotosrailway.com/2025/05/01/68137fa6a70de.jpg": "/media/news/897d764a65897170.jpg",
"https://r2-image.kivotosrailway.com/2025/05/01/68137fb192614.jpg": "/media/news/852dd5b369bf377d.jpg",
"https://r2-image.kivotosrailway.com/2025/05/01/68137fffd1a19.png": "/media/news/7ae2b8fba574195b.png",
"https://r2-image.kivotosrailway.com/2025/05/01/68137ff194540.png": "/media/news/7f7c61ff4b22140d.png",
"https://r2-image.kivotosrailway.com/2025/05/01/68137ffd40a9c.png": "/media/news/faa9264b9cbf052b.png",
"https://r2-image.kivotosrailway.com/2025/05/01/68137f9cf1f48.png": "/media/news/8ecb7a6b90f318e0.png",
"https://r2-image.kivotosrailway.com/2025/05/01/68137ffa67186.png": "/media/news/acd56aafe8b41f05.png",
"https://s21.ax1x.com/2024/09/20/pAMSLRJ.png": "/media/news/02882b262ac53ec2.png",
"https://r2-image.kivotosrailway.com/2025/05/01/6813812298936.png": "/media/news/d699056d1f6db7a2.png",
"https://r2-image.kivotosrailway.com/2025/05/01/681381518e5b7.png": "/media/news/e1ff2fb0e0402a83.png",
"https://r2-image.kivotosrailway.com/2025/04/02/67ece50d760f8.png": "/media/news/625c4eff0e6c3206.png",
"https://r2-image.kivotosrailway.com/2025/04/02/67ece40c0bc76.png": "/media/news/8419859923ddbe8f.png",
"https://r2-image.kivotosrailway.com/2025/04/02/67ecdfbf7e858.png": "/media/news/26f4a1313bbcf030.png",
"https://r2-image.kivotosrailway.com/2025/04/02/67ecf1b42ef19.png": "/media/news/f51e4ded0e8d6b85.png",
"https://r2-image.kivotosrailway.com/2025/01/07/677d16a21da4c.png": "/media/news/9e3cba51761fc108.png",
"https://r2-image.kivotosrailway.com/2025/01/14/6785481e73832.png": "/media/news/694e30e5ce74b863.png",
"https://r2-image.kivotosrailway.com/2025/02/17/67b20e14d2e42.png": "/media/news/c3ab207db385ddd7.png",
"https://image.kivotosrailway.com/uploads/696b7416b1764.jpg": "/media/news/f1e7be44f3b76abc.jpg",
"https://r2-image.kivotosrailway.com/2025/05/14/682374369d005.png": "/media/news/d67395b9a4d20687.png",
"https://r2-image.kivotosrailway.com/2025/05/13/68236919b3ebe.png": "/media/news/0dce62106c0c48a5.png",
"https://r2-image.kivotosrailway.com/2025/05/14/6823735cea4de.png": "/media/news/06b8cffc367a6f28.png",
"https://s21.ax1x.com/2024/09/20/pAMS0KI.png": "/media/news/c6e4e344e54d4976.png",
"https://s21.ax1x.com/2024/09/20/pAMSWxs.png": "/media/news/5149b301e12e850b.png",
"https://r2-image.kivotosrailway.com/2025/12/14/693ec8c79b4b8.jpg": "/media/news/3a2bc283672922bc.jpg",
"https://r2-image.kivotosrailway.com/2025/12/14/693ec8d313ba2.jpg": "/media/news/77bd04054f6a7224.jpg",
"https://r2-image.kivotosrailway.com/2025/12/14/693e5bbe608e5.jpg": "/media/news/99e9760bf7b5f31a.jpg",
"https://r2-image.kivotosrailway.com/2025/05/01/681396eb28788.jpg": "/media/news/17c8d9be981d42ea.jpg",
"https://r2-image.kivotosrailway.com/2025/05/01/681396fd3a792.jpg": "/media/news/cd0c5383ca66c6b2.jpg",
"https://r2-image.kivotosrailway.com/2025/05/01/681396f661df5.jpg": "/media/news/d245b5b6ea4f98d5.jpg",
"https://s21.ax1x.com/2024/12/19/pAO8xoT.png": "/media/news/09f416760a398ccd.png",
"https://r2-image.kivotosrailway.com/2025/05/01/681398635f9b2.png": "/media/news/378bea798cf7c855.png",
"https://r2-image.kivotosrailway.com/2025/05/01/681398634fbc4.png": "/media/news/378bea798cf7c855.png"
};
+3 -1
View File
@@ -1,5 +1,7 @@
import { marked } from "marked"; import { marked } from "marked";
import { imageMap } from "@/lib/image-map.generated";
export function markdownToHtml(markdown: string): string { export function markdownToHtml(markdown: string): string {
return marked.parse(markdown, { gfm: true, breaks: false }) as string; const localizedMarkdown = markdown.replace(/https?:\/\/[^\s)>'"]+/g, (url) => imageMap[url.replace(/[.,;:!?]+$/, "")] ?? url);
return marked.parse(localizedMarkdown, { gfm: true, breaks: false }) as string;
} }