原文里 docs/screenshots/ 放的是 6 张同尺寸 SVG 占位图,只为让 README
在仓库刚建好时能完整渲染。现在换成 78 张真实运行截图(1272x900,
取自一台 2 核 4G 的 Debian 13 虚拟机)。
改动:
- 新增 docs/screenshots/ 78 张 jpg,按 9 个功能分组编号命名
(01-login / 02-overview… / 09-settings-4),覆盖全部 43 个功能页
- 删除 6 张 SVG 占位图与 devtools/make-placeholder-shots.py
- README「截图」章节重写:首屏放 8 张精选大图,其余 70 张按功能分
组放进 9 个 <details> 折叠块,每组内用两列表格 + 图注
- README 项目结构补充 docs/screenshots 与 devtools/shots-map.py
- dist/RELEASE_NOTES.md 同步「截图已全部为真图」
为什么文件名必须 ASCII:
GitHub Releases 资产名已被实测证明会静默剥离非 ASCII 字符
(DOCKER-使用说明.md → DOCKER-.md);且原始文件名含 &(如
AC&AP管理中心.jpg、UPnp&NAT-PMP.jpg)在 shell / CI 里都要额外转义。
devtools/shots-map.py 负责「中文名 → ASCII 名」的映射与复制,
并做完整性校验(漏拷/多余/重名都会中止)。
新增两项预检,挂进 devtools/sync.sh:
- devtools/check-md-links.py:所有 md 的本地引用逐条查存在性
- 截图清单双向校验:README 引用的 78 张与目录里的 78 个文件必须一一对应
验证:check-md-links 扫 10 个 md / 98 个本地引用,0 死链;
78 张图与源目录 md5 集合完全一致;
t-packaging / t-audit / t-deps / t-cachebust 全部通过。
81 lines
2.5 KiB
Python
81 lines
2.5 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
校验 README.md(及其它 md)中引用的本地文件是否真实存在。
|
|
|
|
只在「文档里写了相对路径 → 仓库里必须有这个文件」这个语义上做检查,
|
|
不联网、不检查外链(外链交给 CI 或人工)。
|
|
|
|
三种引用形式都要覆盖:
|
|
1. Markdown 图片/链接  [text](path)
|
|
2. HTML img src <img src="path">
|
|
3. 目录树里的裸路径 (不查,块里的东西本来就是示意)
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
MD_RE = re.compile(r"!?\[[^\]]*\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
|
IMG_RE = re.compile(r"<img[^>]+src=[\"']([^\"']+)[\"']", re.I)
|
|
|
|
|
|
def check(md_path):
|
|
with open(md_path, encoding="utf-8") as f:
|
|
text = f.read()
|
|
base = os.path.dirname(md_path)
|
|
bad = []
|
|
seen = set()
|
|
for m in list(MD_RE.finditer(text)) + list(IMG_RE.finditer(text)):
|
|
target = m.group(1).strip()
|
|
if target in seen:
|
|
continue
|
|
seen.add(target)
|
|
# 跳过外部链接、锚点、mailto 等
|
|
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*:", target) or target.startswith("#"):
|
|
continue
|
|
# 去掉锚点与查询串
|
|
p = target.split("#", 1)[0].split("?", 1)[0]
|
|
if not p:
|
|
continue
|
|
full = os.path.normpath(os.path.join(base, p))
|
|
if not os.path.exists(full):
|
|
bad.append((target, m.start()))
|
|
return bad, len(seen)
|
|
|
|
|
|
def main():
|
|
total_refs = 0
|
|
all_bad = []
|
|
mds = []
|
|
for dirpath, dirnames, filenames in os.walk(ROOT):
|
|
dirnames[:] = [d for d in dirnames if d not in (".git", "node_modules", "__pycache__")]
|
|
for fn in filenames:
|
|
if fn.lower().endswith(".md"):
|
|
mds.append(os.path.join(dirpath, fn))
|
|
|
|
for md in sorted(mds):
|
|
bad, n = check(md)
|
|
total_refs += n
|
|
rel = os.path.relpath(md, ROOT)
|
|
if bad:
|
|
print("!! %s" % rel)
|
|
for t, _ in bad:
|
|
print(" 死链 -> %s" % t)
|
|
all_bad.extend((rel, t) for t, _ in bad)
|
|
else:
|
|
print("ok %-34s %d 个本地引用全部命中" % (rel, n))
|
|
|
|
print("-" * 60)
|
|
print("扫描 %d 个 md,共 %d 个本地引用" % (len(mds), total_refs))
|
|
if all_bad:
|
|
print("!! 发现 %d 条死链" % len(all_bad))
|
|
return 1
|
|
print("没有死链 ✓")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|