Repo .git was damaged (refs/ missing, index truncated, object store near-empty). Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak, then full worktree committed as a rollback baseline.
130 lines
4.8 KiB
Python
130 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
gen_changelog.py — 生成 PocketBase 风格的 changelogs JSON(无需 PocketBase 服务)
|
||
|
||
用途:
|
||
你在本地维护一个 Markdown 源文件(按 `## <时间戳>` 分段写 changelog),
|
||
本脚本把它导出成前端 change-logs.html 所 fetch 的 tamabox.changelogs.json。
|
||
输出格式完全兼容 PocketBase list API 的返回结构,但无需部署任何后端服务,
|
||
因此不会在你的 1C1G 服务器上多占哪怕 1 MB 常驻内存。
|
||
|
||
输出位置:static/tamabox.changelogs.json —— 该文件被 static 包以 //go:embed 打入
|
||
二进制,由 flamego.Static 在 /static 路径同源提供,前端直接 fetch 同源路径,
|
||
不再依赖任何外部域名(去掉 api-direct.337845818.best 等外链,降低可用性/安全风险)。
|
||
|
||
源文件格式(dev/changelog-source.md 示例):
|
||
## 2024-03-04 21:45:55
|
||
新增:用户私信功能上线。
|
||
修复:移动端问题列表间距过窄。
|
||
|
||
## 2024-02-29 00:00:55
|
||
优化:首页加载速度提升约 30%。
|
||
|
||
注意:
|
||
- 时间戳支持 `YYYY-MM-DD HH:MM:SS` / `YYYY-MM-DD` / 带毫秒的 `...fffZ` / ISO 格式,
|
||
脚本会统一归一化为 PocketBase 风格的 `YYYY-MM-DD HH:MM:SS.fffZ`(UTC)。
|
||
- 正文里的换行默认会被转成 `<br>`,因为前端用 x-html 渲染;传 --no-br 可关闭。
|
||
- 导出时按时间倒序排列,最新一条排在最前(前端读 items[0] 即取最新)。
|
||
|
||
用法:
|
||
python dev/gen_changelog.py
|
||
python dev/gen_changelog.py 源文件.md 输出.json
|
||
python dev/gen_changelog.py 源文件.md 输出.json --no-br
|
||
"""
|
||
import json
|
||
import re
|
||
import sys
|
||
from datetime import datetime, timezone
|
||
|
||
COLLECTION_ID = "tamabox"
|
||
COLLECTION_NAME = "tamabox-changelogs"
|
||
DEFAULT_SRC = "dev/changelog-source.md"
|
||
DEFAULT_OUT = "static/tamabox.changelogs.json"
|
||
|
||
TS_LINE = re.compile(r"^##\s+(.+?)\s*$")
|
||
FULL_TS = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}Z$")
|
||
|
||
|
||
def normalize_ts(raw: str) -> str:
|
||
"""把用户输入的时间归一化为 PocketBase 风格时间戳:YYYY-MM-DD HH:MM:SS.fffZ(UTC)。"""
|
||
raw = raw.strip()
|
||
if FULL_TS.match(raw):
|
||
return raw
|
||
for fmt in ("%Y-%m-%d %H:%M:%S.%fZ", "%Y-%m-%d %H:%M:%S",
|
||
"%Y-%m-%d %H:%M", "%Y-%m-%d"):
|
||
try:
|
||
dt = datetime.strptime(raw, fmt)
|
||
return dt.strftime("%Y-%m-%d %H:%M:%S") + ".000Z"
|
||
except ValueError:
|
||
continue
|
||
try:
|
||
dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
||
return dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + ".000Z"
|
||
except ValueError:
|
||
sys.exit(f"[错误] 无法解析时间戳: {raw!r}")
|
||
|
||
|
||
def parse_source(text: str):
|
||
"""按 `## <时间戳>` 分段,返回 [(ts, content), ...],顺序与源文件一致。"""
|
||
entries, cur_ts, buf = [], None, []
|
||
for line in text.splitlines():
|
||
m = TS_LINE.match(line)
|
||
if m:
|
||
if cur_ts is not None:
|
||
entries.append((cur_ts, "\n".join(buf).strip()))
|
||
cur_ts, buf = m.group(1).strip(), []
|
||
elif cur_ts is not None:
|
||
buf.append(line)
|
||
if cur_ts is not None:
|
||
entries.append((cur_ts, "\n".join(buf).strip()))
|
||
if not entries:
|
||
sys.exit("[错误] 源文件未找到任何 '## <时间戳>' 分段")
|
||
return entries
|
||
|
||
|
||
def build(entries, br=True):
|
||
"""把条目导出为 PocketBase list API 风格 JSON(最新在前)。"""
|
||
parsed = [(normalize_ts(ts), c) for ts, c in entries]
|
||
parsed.sort(key=lambda x: x[0], reverse=True) # 倒序:最新 published_at 在前
|
||
items = []
|
||
for ts, content in parsed:
|
||
items.append({
|
||
"collectionId": COLLECTION_ID,
|
||
"collectionName": COLLECTION_NAME,
|
||
"content": content.replace("\n", "<br>\n") if br else content,
|
||
"created": ts,
|
||
"id": "none",
|
||
"published_at": ts,
|
||
"updated": ts,
|
||
})
|
||
return {
|
||
"page": 1,
|
||
"perPage": len(items) or 1,
|
||
"totalItems": len(items),
|
||
"totalPages": 1,
|
||
"items": items,
|
||
}
|
||
|
||
|
||
def main():
|
||
args = sys.argv[1:]
|
||
br = "--no-br" not in args
|
||
paths = [a for a in args if not a.startswith("--")]
|
||
src = paths[0] if len(paths) > 0 else DEFAULT_SRC
|
||
out = paths[1] if len(paths) > 1 else DEFAULT_OUT
|
||
try:
|
||
with open(src, encoding="utf-8") as f:
|
||
text = f.read()
|
||
except FileNotFoundError:
|
||
sys.exit(f"[错误] 找不到源文件: {src}")
|
||
data = build(parse_source(text), br=br)
|
||
with open(out, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
latest = data["items"][0]["published_at"] if data["items"] else "N/A"
|
||
print(f"[完成] 已生成 {out}({data['totalItems']} 条记录,最新发布于 {latest})")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|