feat: M3-M7 全模块落地 + 集成接线

- M3 认证/权限/审计/限流 + 全部 REST 接口
- M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制)
- M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配)
- M6 WebSocket 实时同步中心
- M7 xlsx 导出 + 赛事包 zip + 备份
- 规则包 YAML 导入导出(差异预览)
- 字体 vendor 子集化(4 套 x2 字重)
- 集成:realtime/export/rules 接入 main+httpapi;backup 子命令
- 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
This commit is contained in:
AskaEth committed 2026-09-09 19:02:24 +08:00
1 parent d0e3b93e76
commit bc8b1ee6c0
145 files changed
+25159 -321

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# 字体清单(subset-fonts.sh 与 gen-manifest.py 的唯一数据源)
#
# 字段(| 分隔):
# id | 显示名 | font-family | 分类 | 授权 | 来源说明 | Regular 源文件 | Bold 源文件
#
# 源文件相对 $FONT_SRC(默认 ~/Fonts)。
# 新增字体:在下面加一行,然后重跑
# PYTHON=<带 fonttools+brotli 的 python> vendor/fonts/tools/subset-fonts.sh
noto-sans|Noto Sans SC|Noto Sans SC|sans-serif|OFL-1.1|Google Noto Sans SC (noto-sans/sc)|noto-sans/sc/NotoSansSC-Regular.otf|noto-sans/sc/NotoSansSC-Bold.otf
noto-serif|Noto Serif SC|Noto Serif SC|serif|OFL-1.1|Google Noto Serif SC (noto-serif/sc/SubsetOTF/SC)|noto-serif/sc/SubsetOTF/SC/NotoSerifSC-Regular.otf|noto-serif/sc/SubsetOTF/SC/NotoSerifSC-Bold.otf
misans|MiSans|MiSans|sans-serif|MiSans Font License|小米 MiSans (MiSans/ttf)|MiSans/ttf/MiSans-Regular.ttf|MiSans/ttf/MiSans-Bold.ttf
source-han-sans|思源黑体 Source Han Sans CN|Source Han Sans CN|sans-serif|OFL-1.1|Adobe Source Han Sans CN (思源黑体)|思源黑体/SourceHanSansCN-Regular.otf|思源黑体/SourceHanSansCN-Bold.otf
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""生成 vendor/fonts/manifest.json。
数据源:vendor/fonts/tools/fonts.conf(字体元数据)+ vendor/fonts/*.woff2(实际产物)。
每次跑完 subset-fonts.sh 会自动调用;也可单独运行:
python3 vendor/fonts/tools/gen-manifest.py
"""
from __future__ import annotations
import hashlib
import json
import subprocess
import sys
from datetime import date
from pathlib import Path
HERE = Path(__file__).resolve().parent
OUT_DIR = HERE.parent
ROOT = OUT_DIR.parent.parent # 仓库根,用于生成 vendor/fonts/... 形式的相对路径
CONF = HERE / "fonts.conf"
WEIGHTS = (("regular", 400), ("bold", 700))
SUBSET_DESC = "ASCII + GB2312 一级/二级汉字 + 中文标点 + 数字符号"
def charset_count() -> int:
"""复用 subset-fonts.sh 的字符集生成逻辑,保证清单与产物一致。"""
code = (
"chars=set()\n"
"for c in range(0x20,0x7F): chars.add(chr(c))\n"
"for hi in range(0xA1,0xFF):\n"
" for lo in range(0xA1,0xFF):\n"
" try: chars.add(bytes((hi,lo)).decode('gb2312'))\n"
" except UnicodeDecodeError: pass\n"
"chars.update(' ·—…‘’“”※→←↑↓√×÷±≈≠≤≥∞°℃℉¥€£©®™№§¶†‡•‰′″〇')\n"
"chars.update('「」『』【】〔〕〖〗《》〈〉')\n"
"chars.update('0123456789')\n"
"chars.update('!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~')\n"
"print(len(chars))\n"
)
return int(subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True).stdout)
def human(n: int) -> str:
return f"{n / 1024 / 1024:.2f} MB" if n >= 1024 * 1024 else f"{n / 1024:.0f} KB"
def read_conf() -> list[dict[str, str]]:
rows = []
for line in CONF.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
fid, name, family, category, license_, source, reg, bold = (p.strip() for p in line.split("|"))
rows.append(
{
"id": fid,
"displayName": name,
"fontFamily": family,
"category": category,
"license": license_,
"source": source,
"regular": reg,
"bold": bold,
}
)
return rows
def main() -> int:
fonts = []
grand_total = 0
for row in read_conf():
weights = []
font_total = 0
for weight_name, weight_class in WEIGHTS:
path = OUT_DIR / f"{row['id']}-{weight_name}.woff2"
if not path.is_file():
print(f"缺少产物: {path},请先运行 subset-fonts.sh", file=sys.stderr)
return 1
data = path.read_bytes()
size = len(data)
font_total += size
weights.append(
{
"weight": weight_class,
"style": "normal",
"file": path.name,
"path": str(path.relative_to(ROOT)),
"bytes": size,
"size": human(size),
"sha256": hashlib.sha256(data).hexdigest(),
}
)
grand_total += font_total
fonts.append(
{
"id": row["id"],
"displayName": row["displayName"],
"fontFamily": row["fontFamily"],
"cssVar": f"--font-{row['id']}",
"category": row["category"],
"license": row["license"],
"source": row["source"],
"totalBytes": font_total,
"totalSize": human(font_total),
"weights": weights,
}
)
manifest = {
"version": 1,
"generatedAt": date.today().isoformat(),
"generator": "vendor/fonts/tools/subset-fonts.sh",
"subset": {
"charset": SUBSET_DESC,
"charCount": charset_count(),
"format": "woff2",
"layoutFeatures": "kern,liga,clig,calt,ccmp,locl,mark,mkmk",
},
"totalBytes": grand_total,
"totalSize": human(grand_total),
"fonts": fonts,
}
out = OUT_DIR / "manifest.json"
out.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"manifest.json: {len(fonts)} 套字体,{len(fonts) * 2} 个文件,合计 {human(grand_total)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""把子集字体内部的 name 表 / OS2 字重统一成 CSS 期望的规范值。
为什么需要:思源黑体的老版本 OTF 把每个字重写成独立族名
(`Source Han Sans CN Regular` / `Source Han Sans CN Bold`),
MiSans 的 usWeightClass 是 330/630 这类非标准值。
@font-face 的 `font-family` 描述符虽然能屏蔽内部族名,但保持一致更省心,
也避免字体被单独安装/引用时族名对不上。
用法(由 subset-fonts.sh 调用):
python3 vendor/fonts/tools/normalize-names.py
"""
from __future__ import annotations
import sys
from pathlib import Path
from fontTools.ttLib import TTFont
HERE = Path(__file__).resolve().parent
OUT_DIR = HERE.parent
CONF = HERE / "fonts.conf"
WEIGHTS = (("regular", "Regular", 400), ("bold", "Bold", 700))
STALE_NAME_IDS = (1, 2, 3, 4, 6, 16, 17, 21, 22)
PLATFORMS = ((3, 1, 0x409), (1, 0, 0)) # Windows / Mac,英文
def set_name(name, name_id: int, value: str) -> None:
for platform_id, plat_enc_id, lang_id in PLATFORMS:
name.setName(value, name_id, platform_id, plat_enc_id, lang_id)
def normalize(path: Path, family: str, style: str, weight: int) -> None:
font = TTFont(path)
name = font["name"]
for record in list(name.names):
if record.nameID in STALE_NAME_IDS:
name.removeNames(record.nameID, record.platformID, record.platEncID, record.langID)
full_name = family if style == "Regular" else f"{family} {style}"
set_name(name, 1, family) # family
set_name(name, 2, style) # subfamily
set_name(name, 3, f"{full_name}:subset") # unique id
set_name(name, 4, full_name) # full name
set_name(name, 6, full_name.replace(" ", "")) # postscript name
set_name(name, 16, family) # typographic family
set_name(name, 17, style) # typographic subfamily
os2 = font["OS/2"]
os2.usWeightClass = weight
if weight >= 700:
os2.fsSelection = (os2.fsSelection & ~0x40) | 0x20 # BOLD
font["head"].macStyle |= 0x01
else:
os2.fsSelection = (os2.fsSelection & ~0x20) | 0x40 # REGULAR
font["head"].macStyle &= ~0x01
font.flavor = "woff2"
font.save(path)
font.close()
def main() -> int:
for line in CONF.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
fid, _, family = [p.strip() for p in line.split("|")][:3]
for weight_name, style, weight_class in WEIGHTS:
path = OUT_DIR / f"{fid}-{weight_name}.woff2"
if not path.is_file():
print(f"缺少产物: {path}", file=sys.stderr)
return 1
normalize(path, family, style, weight_class)
print(f" {path.name:32s} -> {family} / {style} ({weight_class})")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# 字体子集化脚本 —— 把 ~/Fonts 下的中文字体裁剪成 woff2 并写入 vendor/fonts/
#
# 用途:离线部署,字体必须随仓库分发,禁止 CDN。
# 产物:vendor/fonts/<id>-<weight>.woff2 + vendor/fonts/manifest.json
#
# 依赖:python3 + fonttools + brotli
# pip install fonttools brotli
# # 系统 Python 受 PEP 668 保护时用虚拟环境:
# # python3 -m venv --system-site-packages ~/.tmp/fontenv
# # ~/.tmp/fontenv/bin/pip install brotli
# # PYTHON=~/.tmp/fontenv/bin/python vendor/fonts/tools/subset-fonts.sh
#
# 用法:
# vendor/fonts/tools/subset-fonts.sh
# FONT_SRC=~/Fonts PYTHON=python3 vendor/fonts/tools/subset-fonts.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
FONT_SRC="${FONT_SRC:-$HOME/Fonts}"
PYTHON="${PYTHON:-python3}"
FONTS_CONF="$SCRIPT_DIR/fonts.conf"
# 子集字符集:ASCII + GB2312 一级/二级汉字 + 中文标点 + 数字/全角符号
CHARSET="$("$PYTHON" - <<'PY'
import sys
chars = set()
# 1) ASCII 可打印字符
for c in range(0x20, 0x7F):
chars.add(chr(c))
# 2) GB2312 全部合法双字节码位(一级 3755 + 二级 3008 汉字,及全角标点/希腊/俄文等)
for hi in range(0xA1, 0xFF):
for lo in range(0xA1, 0xFF):
try:
chars.add(bytes((hi, lo)).decode("gb2312"))
except UnicodeDecodeError:
pass
# 3) 常用中文标点 / 数字符号补充
chars.update(
" ·—…‘’“”※→←↑↓√×÷±≈≠≤≥∞°℃℉¥€£©®™№§¶†‡•‰′″〇"
"「」『』【】〔〕〖〗《》〈〉"
"0123456789"
"!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"
)
sys.stdout.write("".join(sorted(chars)))
PY
)"
CHAR_COUNT="$(printf '%s' "$CHARSET" | "$PYTHON" -c 'import sys; print(len(sys.stdin.read()))')"
# 子集化参数:
# --flavor=woff2 输出 woff2(需 brotli)
# --layout-features 保留字距/连字等,丢弃 CJK 竖排等无用特性以压体积
# --name-IDs='*' 保留完整 name 表(含本地化族名)
# --recommended-glyphs 保留 .notdef/.null 等推荐字形
# --drop-tables+=DSIG 丢弃无用的数字签名表
SUBSET_OPTS=(
--flavor=woff2
--layout-features='kern,liga,clig,calt,ccmp,locl,mark,mkmk'
--name-IDs='*'
--name-languages='*'
--recommended-glyphs
--drop-tables+=DSIG
)
mkdir -p "$OUT_DIR"
echo "charset: ${CHAR_COUNT} 个字符(ASCII + GB2312 一级/二级 + 中文标点 + 数字符号)"
while IFS='|' read -r id name family category license source_note reg_src bold_src; do
case "$id" in ''|'#'*) continue ;; esac
for pair in "regular:400:$reg_src" "bold:700:$bold_src"; do
IFS=':' read -r weight_name weight_class src_rel <<<"$pair"
src="$FONT_SRC/$src_rel"
out="$OUT_DIR/$id-$weight_name.woff2"
[ -f "$src" ] || { echo "缺少源字体: $src" >&2; exit 1; }
printf ' %-26s %s -> %s\n' "$id-$weight_name" "$(basename "$src")" "$(basename "$out")"
"$PYTHON" -m fontTools.subset "$src" "${SUBSET_OPTS[@]}" \
--text="$CHARSET" --output-file="$out"
done
done < "$FONTS_CONF"
"$PYTHON" "$SCRIPT_DIR/normalize-names.py"
"$PYTHON" "$SCRIPT_DIR/gen-manifest.py"
echo "完成。产物目录:$OUT_DIR"
ls -la "$OUT_DIR"/*.woff2