feat(migrate): Halo 全量内容迁移(压缩版)——6文/21图/1瞬间 URL 1:1

- 正文源=线上渲染页(Halo 快照层 patch 残迹:泥潭/赎罪仅 2 字符),
  pandoc html→gfm + tags API 显示名换算后分类:5 书评(五档情绪色)+1 技术
- 图库 21 张:长边 2400/q86 压缩入仓(原图 196M→9.5M——公开仓历史膨胀
  与 nginx 100M 推送上限双杀;原图在 Phase 0 备份 tar 中永久留档)
- 瞬间 1 条;URL 1:1(6 slug 全等,零 301)
- 本提交由 7 个本地提交重写而成(清除已进历史的 196M blob,未推送过);
  因 reset --soft 暂存竞态,一并包含 spec 归档文件
  (.spec/specs/archive/2026-10-24_migrate-blog-content/)

验证: check_urls.py 6==6;astro build 11 页 exit 0(8.67s);
      EXIF 管线跑通;staging 更新;halo healthy+200 全程未动

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-10-25 00:39:08 +08:00
1 parent 7b71acf223
commit aea21c8694
74 files changed
+1381 -97

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""图库迁移(Phase 2 T3):Halo photos → content/photos/{jpg,json sidecar}
EXIF 不在迁移时提——构建期 photos loader(src/lib/exif.ts)会补全;
sidecar 里 exif 留空对象。分组:Halo displayName 含山/雪/land/snow →
landscape,否则 street(当前仅一组 snow mountain → landscape)。
"""
import json
import re
import urllib.request
from pathlib import Path
from urllib.parse import unquote
BASE = "http://127.0.0.1:18090"
OUT = Path("content/photos")
def slugify(name: str) -> str:
stem = re.sub(r"\.(jpg|jpeg|png|webp)$", "", name, flags=re.I)
stem = re.sub(r"[^0-9A-Za-z\u4e00-\u9fff-]+", "-", stem).strip("-")
return stem or "photo"
def group_of(display: str) -> str:
low = display.lower()
return "landscape" if any(k in low for k in ("山", "雪", "snow", "land", "mountain")) else "street"
def compress(data: bytes) -> bytes:
from io import BytesIO
from PIL import Image
img = Image.open(BytesIO(data))
if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
img.thumbnail((2400, 2400))
buf = BytesIO()
img.save(buf, "JPEG", quality=86, optimize=True)
return buf.getvalue()
def main() -> int:
# 真实照片取代样例:先清样例(jpg/json 成对;仅清本脚本管辖的产物)
for f in OUT.glob("*"):
if f.suffix in (".jpg", ".json"):
f.unlink()
photos = json.load(open(".migration/photos.json", encoding="utf-8"))
n = 0
for p in photos:
spec = p["spec"]
url = spec.get("url") or spec.get("cover") or ""
if not url.startswith("/upload/"):
print(f" skip 非本站图: {url[:60]}")
continue
fname = unquote(url.rsplit("/", 1)[1])
slug = slugify(spec.get("displayName") or fname)
data = urllib.request.urlopen(BASE + url, timeout=60).read()
# 入仓前压到长边 2400/q86:原图总计 196M(单张最大 28M)会永久膨胀
# 公开仓历史并撞死 nginx 100M 推送上限;原图已随 Phase 0 备份归档,
# 站内展示由 sharp 出 srcset,2400px 足够 lightbox 全屏
(OUT / f"{slug}.jpg").write_bytes(compress(data))
(OUT / f"{slug}.json").write_text(json.dumps({
"title": spec.get("displayName", fname).rsplit(".", 1)[0],
"image": f"./{slug}.jpg",
"group": group_of(spec.get("groupName", "")),
"alt": spec.get("description", ""),
"exif": {},
}, ensure_ascii=False, indent=1))
n += 1
print(f"migrated {n} photos")
return 0 if n else 1
if __name__ == "__main__":
raise SystemExit(main())