This commit is contained in:
2026-07-18 10:18:55 +08:00 Unverified
parent a2fc19edd3
commit 3867373850
69 changed files with 394 additions and 214 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

+64
View File
@@ -0,0 +1,64 @@
import hashlib
import json
from pathlib import Path
from PIL import Image, ImageOps
ROOT = Path(__file__).resolve().parents[1]
CACHE = ROOT / ".tmp" / "gzhh-images"
RAW = CACHE / "raw"
MANIFEST = CACHE / "manifest.json"
ARTICLES = ROOT / "src" / "data" / "articles.generated.json"
OUTPUT = ROOT / "public" / "assets" / "article-covers"
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
by_url = {
item["url"]: item
for item in manifest
if item.get("status") == "ok" and item.get("file")
}
articles = json.loads(ARTICLES.read_text(encoding="utf-8"))
OUTPUT.mkdir(parents=True, exist_ok=True)
exported = 0
missing = []
used_files = set()
for article in articles:
source_url = article.get("coverSourceUrl") or article.get("cover", "")
if not source_url.startswith("http"):
continue
cache_entry = by_url.get(source_url)
if not cache_entry:
missing.append({"title": article.get("title", ""), "url": source_url})
continue
key = hashlib.sha256(source_url.encode("utf-8")).hexdigest()[:12]
output_name = f"cover-{key}.webp"
output_path = OUTPUT / output_name
with Image.open(RAW / cache_entry["file"]) as image:
frame = ImageOps.exif_transpose(image).convert("RGB")
frame.thumbnail((960, 960), Image.Resampling.LANCZOS)
frame.save(output_path, "WEBP", quality=82, method=6)
article["coverSourceUrl"] = source_url
article["cover"] = f"/assets/article-covers/{output_name}"
used_files.add(output_name)
exported += 1
ARTICLES.write_text(
json.dumps(articles, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
summary = {
"articles": len(articles),
"exported": exported,
"uniqueFiles": len(used_files),
"missing": len(missing),
"bytes": sum((OUTPUT / name).stat().st_size for name in used_files),
}
print(json.dumps(summary, ensure_ascii=False))
if missing:
raise RuntimeError(f"Missing {len(missing)} cached cover images")
+11 -2
View File
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { readdir, readFile, writeFile } from "node:fs/promises"; import { readdir, readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import path from "node:path"; import path from "node:path";
@@ -14,6 +15,12 @@ const cleanMarkdown = (value) =>
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim(); .trim();
const getLocalCoverPath = (url) => {
if (!url) return "";
const key = createHash("sha256").update(url).digest("hex").slice(0, 12);
return `/assets/article-covers/cover-${key}.webp`;
};
const getExcerpt = (body, title) => { const getExcerpt = (body, title) => {
const ignored = /(?:data:image|%3Csvg|transform=|阅读|点赞|分享|推荐|留言|2024届612|font-family|__bottom-bar__|sns_opr_btn|picture_content|page_content)/i; const ignored = /(?:data:image|%3Csvg|transform=|阅读|点赞|分享|推荐|留言|2024届612|font-family|__bottom-bar__|sns_opr_btn|picture_content|page_content)/i;
const lines = body const lines = body
@@ -60,16 +67,18 @@ const parseArticle = (fileName, raw) => {
const contentStart = raw.search(/>\s*原文地址:/); const contentStart = raw.search(/>\s*原文地址:/);
const body = contentStart >= 0 ? raw.slice(contentStart).split(/\r?\n/).slice(2).join("\n") : raw; const body = contentStart >= 0 ? raw.slice(contentStart).split(/\r?\n/).slice(2).join("\n") : raw;
const images = [...body.matchAll(/!\[[^\]]*\]\((https?:\/\/[^\s\)]+)\)/g)] const images = [...body.matchAll(/!\[[^\]]*\]\((https?:\/\/[^\s\)]+)\)/g)]
.map((match) => match[1]) .map((match) => match[1].replaceAll("&", "&"))
.filter((url) => /mmbiz\.qpic\.cn/.test(url)); .filter((url) => /mmbiz\.qpic\.cn/.test(url));
const excerptText = getExcerpt(body, title); const excerptText = getExcerpt(body, title);
const coverSourceUrl = images[0] ?? "";
return { return {
title, title,
date, date,
displayDate: date ? date.replaceAll("-", ".") : "日期待补", displayDate: date ? date.replaceAll("-", ".") : "日期待补",
sourceUrl, sourceUrl,
cover: images[0] ?? "", cover: getLocalCoverPath(coverSourceUrl),
coverSourceUrl,
excerpt: excerptText.length > 88 ? `${excerptText.slice(0, 88)}` : excerptText || "一页从公众号找回的班级记录。", excerpt: excerptText.length > 88 ? `${excerptText.slice(0, 88)}` : excerptText || "一页从公众号找回的班级记录。",
category: getCategory(title), category: getCategory(title),
fileName fileName
File diff suppressed because it is too large Load Diff
+1
View File
@@ -6,6 +6,7 @@ export interface WechatArticle {
displayDate: string; displayDate: string;
sourceUrl: string; sourceUrl: string;
cover: string; cover: string;
coverSourceUrl?: string;
excerpt: string; excerpt: string;
category: ArticleCategory; category: ArticleCategory;
fileName: string; fileName: string;