Files
drouter/devtools/check-md-links.py
liuzhuohua a8659dfb98 docs(readme): 用 78 张真实界面截图替换 SVG 占位图
原文里 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 全部通过。
2026-09-30 22:46:43 +08:00

81 lines
2.5 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
校验 README.md(及其它 md)中引用的本地文件是否真实存在。
只在「文档里写了相对路径 → 仓库里必须有这个文件」这个语义上做检查,
不联网、不检查外链(外链交给 CI 或人工)。
三种引用形式都要覆盖:
1. Markdown 图片/链接 ![alt](path) [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())