chore: baseline snapshot before moderation-strictness feature

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.
This commit is contained in:
tamakyi
2026-09-10 10:56:27 +08:00
commit e3e59e7c69
296 changed files with 67458 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
#!/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()