Files
liuzhuohua 3f5f31701e feat(devtools): 新增截图覆盖度检查,README 标明待补页面
核对用户打包发来的 78 张截图,与仓库已入库的 78 张 md5 集合完全一致
(互相零差异),确认上一轮提交已是完整状态,本次不需改动任何图片。

本次补充的是「防漂移」能力:

1. 新增 devtools/check-shots.py —— 功能页 ↔ 截图 ↔ README 引用 三方对账
   - 43 个功能页逐个报告有无截图(当前 40 有 / 3 缺)
   - 映射表登记的截图 vs docs/screenshots/ 实际文件,双向查差异
   - README 引用的 vs 实际存在的,双向查差异
   为什么不用中文页名做子串匹配:「防火墙 IPv4」会命中「防火墙 IPv6」的
   规则说明,误判是静默的假绿。宁可手工维护一张映射表。

2. sync.sh 里原来那段内联的截图清单校验,替换为调用 check-shots.py
   (功能被完整覆盖,且现在多了功能页覆盖度这一维)。

3. sync.sh 的 CRLF 检查改为只扫 git 已追踪的文件。
   原来用 glob('**/*.html') 会扫到未追踪的本地临时预览页
   (_preview-shots.html),它既不会进仓库也不会被 deploy.sh 上传,
   为它报错是纯噪音。

4. README 截图章节新增「还没截图的 3 个页面」折叠块:
   新手向导 / 主题之家 / 升级与保护,附补图流程。
   项目结构章节补充两个新工具。

验证:check-md-links 98 个引用 0 死链;check-shots 三方一致;
      t-audit / t-packaging 全部通过。
2026-09-30 23:05:51 +08:00

148 lines
7.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
截图覆盖度检查:README 的「功能」章节列了 N 个功能页,docs/screenshots/ 里有 M 张图。
这两个数字天然会漂移 —— 加了新功能页忘了截图、补了截图忘了更新 README、
重命名时漏了一张。人工数容易错,所以钉成脚本。
三件事:
1. 逐个功能页报告「有没有截图」,缺的单独列出来
2. 报告哪些截图没被 README 引用(可能是多余的,或忘了写进图集)
3. 报告哪些截图引用了但文件不存在
设计取舍:
功能页名 → 截图文件名 的对应关系靠人工维护在 GROUPS 里,不做模糊匹配。
中文页名做子串匹配看着聪明,实际会误判(「防火墙 IPv4」会同时命中
「防火墙 IPv6」的规则说明),而且一旦误判就是静默的假绿。
宁可手工写死一张表 —— 反正每次加页面本来就要改 README。
"""
import glob
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SHOTS_DIR = os.path.join(ROOT, "docs", "screenshots")
README = os.path.join(ROOT, "README.md")
# 功能页名 → [截图文件名](不含 .jpg)
# 顺序与 README 的「功能」章节一致。空列表 = 该页暂无截图。
PAGES = [
# ── 登入(不在 43 个功能页里,但截图集里有)────────────
("[登入页]", ["01-login"]),
("新手向导", []),
("系统概览", ["02-overview", "02-overview-2", "02-overview-3"]),
("网络状态 / 加速", ["02-netstat", "02-netstat-2", "02-netstat-3"]),
("网卡与桥接", ["03-iface"]),
("WAN 口", ["03-wan", "03-wan-2", "03-wan-3"]),
("PPPoE 多拨", ["03-pppoe", "03-pppoe-2"]),
("LAN 口", ["03-lan"]),
("VLAN / IPTV", ["03-vlan"]),
("网络唤醒 (WOL)", ["03-wol"]),
("DHCP 服务", ["04-dhcp", "04-dhcp-2"]),
("DNS 服务", ["04-dns"]),
("IPv6 / RA", ["04-ipv6ra", "04-ipv6ra-2", "04-ipv6ra-3"]),
("DHCPv6 / 前缀委派", ["04-dhcpv6"]),
("动态域名 DDNS", ["04-ddns", "04-ddns-2", "04-ddns-3"]),
("真·公网 IP 判定", ["04-publicip", "04-publicip-2"]),
("防火墙 IPv4", ["05-fw4", "05-fw4-2"]),
("防火墙 IPv6", ["05-fw6", "05-fw6-2"]),
("端口转发 / DMZ", ["05-dnat"]),
("UPnP / NAT-PMP", ["05-upnp"]),
("访问控制 / 时间组", ["05-acl"]),
("智能限速 QoS", ["06-qos", "06-qos-2"]),
("应用识别 DPI", ["06-dpi", "06-dpi-2"]),
("NTP 时间同步", ["06-ntp"]),
("文件共享 SMB/NFS", ["06-smb", "06-smb-2"]),
("打印服务", ["06-cups", "06-cups-2", "06-cups-3", "06-cups-4"]),
("AC/AP 管理中心", ["06-acap", "06-acap-2"]),
("Web 终端 / 文件", ["07-webshell"]),
("通用 API 接口", ["07-api", "07-api-2"]),
("网络诊断工具", ["07-diag"]),
("IPv6 连通性测试", ["07-ipv6test"]),
("Docker / Compose", ["07-docker", "07-docker-2", "07-docker-3"]),
("Docker 引擎配置", ["07-dockerconf", "07-dockerconf-2", "07-dockerconf-3"]),
("系统日志", ["08-syslog"]),
("连接与流日志", ["08-flowlog", "08-flowlog-2"]),
("依赖自检与安装", ["09-deps", "09-deps-2"]),
("磁盘与日志清理", ["09-cleanup", "09-cleanup-2"]),
("内核转发与加速", ["09-kernel", "09-kernel-2"]),
("证书 / SSL", ["09-tls", "09-tls-2", "09-tls-3"]),
("电源控制", ["09-power"]),
("用户与密钥", ["09-user", "09-user-2"]),
("系统设置", ["09-settings", "09-settings-2", "09-settings-3", "09-settings-4"]),
("主题之家", []),
("升级与保护", []),
]
def main():
on_disk = {os.path.basename(p)[:-4] for p in glob.glob(os.path.join(SHOTS_DIR, "*.jpg"))}
readme = open(README, encoding="utf-8").read()
referenced = set(re.findall(r"docs/screenshots/([^)\s\"]+)\.jpg", readme))
mapped = [k for _, keys in PAGES for k in keys]
# ── 1. 功能页覆盖度 ──────────────────────────────────────
# [xxx] 形式的条目是「不在 43 个功能页里的附加截图」,不计入覆盖度分母
real_pages = [(p, k) for p, k in PAGES if not p.startswith("[")]
extra_shots = [(p, k) for p, k in PAGES if p.startswith("[")]
missing_pages = [p for p, keys in real_pages if not keys]
print("一、功能页覆盖度")
print(" 功能页 %d 个,有截图 %d 个,缺 %d 个"
% (len(real_pages), len(real_pages) - len(missing_pages), len(missing_pages)))
if missing_pages:
for p in missing_pages:
print(" 缺 → %s" % p)
else:
print(" 全部功能页都有截图 ✓")
if extra_shots:
n = sum(len(k) for _, k in extra_shots)
print(" 另有附加截图 %d 张(%s)" % (n, "、".join(p.strip("[]") for p, _ in extra_shots)))
print()
# ── 2. 映射表 vs 磁盘 ───────────────────────────────────
print("二、映射表 vs docs/screenshots/")
ghost = sorted(set(mapped) - on_disk) # 表里有、磁盘没有(漏拷/写错名)
orphan = sorted(on_disk - set(mapped)) # 磁盘有、表里没有(漏登记)
if ghost:
print(" 表里登记了但文件不存在(%d 个):" % len(ghost))
for g in ghost:
print(" %s.jpg" % g)
if orphan:
print(" 文件存在但没登记进映射表(%d 个):" % len(orphan))
for o in orphan:
print(" %s.jpg" % o)
if not ghost and not orphan:
print(" 一一对应 ✓(%d 张)" % len(mapped))
print()
# ── 3. README 引用 ──────────────────────────────────────
print("三、README 引用 vs 实际文件")
dead = sorted(referenced - on_disk) # README 引用了但文件不存在
never = sorted(on_disk - referenced) # 文件存在但 README 没展示
if dead:
print(" README 引用了但文件不存在(%d 个):" % len(dead))
for d in dead:
print(" %s.jpg" % d)
if never:
print(" 文件存在但 README 没引用(%d 个):" % len(never))
for n in never:
print(" %s.jpg" % n)
if not dead and not never:
print(" 完全对应 ✓(各 %d 张)" % len(referenced))
print()
bad = bool(ghost or orphan or dead)
print("=" * 60)
if bad:
print("发现问题(见上),请修正")
return 1
print("结构一致 ✓ 缺图功能页 %d 个(不影响一致性,纯提示)" % len(missing_pages))
return 0
if __name__ == "__main__":
sys.exit(main())