Files
drouter/_dev/t-fingerprint.py
liuzhuohua b7ca9c89e8 feat(i18n/layout): 全站中英双语 + 四轮排版修复收官 + 修 boot_id 重启检测
v1.0.10 两件大事:

1. 界面中英双语(英文界面无中文残留)
   - 新增 web/i18n.js(约 4200 条词条),零依赖零构建
   - 后端下发中文数据走 bt4() 查表翻译;独立模块(upstream/netdetail/
     realtime/update)纳入翻译并注册切语言重绘钩子
   - 后端「状态/聚合」接口只回稳定枚举+原始数值,不下发中文文案
   - 顺带修一个真实 XSS:顶层 const 不挂 window,独立脚本 esc 静默退化成零转义

2. 修掉「文字竖着排/错位」(多分辨率反复暴露)
   - 单元格 min-content 只有 1 字宽 → word-break:keep-all + 表头 nowrap
   - 依赖卡片正文被标签/按钮挤成 27px → .dep-body{flex:1 1 200px} + flex-wrap
   - 裸 flex:1(下限为0)给正文 → 一律改 flex:1 1 <px>
   - .dep-item 横向容器直接塞块级子项 → 退回块级
   - 关键陷阱:auto-fill 布局屏越宽列数越多单卡越窄,窄屏反而测不出来,
     真机审计推进到「逐视图点按钮 + 1920/2560 宽视口 + 中英双语」
   - 守卫 t-layout.py 扩到 35 条,12 处反向验证全红

3. 修真缺陷:_link_since() 重启检测被误改成 if False:(注释还写着「已修」),
   uptime 归零后相减得负数 → 恢复比对 boot_id

附带:新增 25 个检查器(t-*.py)、排版审计工具链(shot-*/ocr-*/live-*)、
版本号兜底值同步到 1.0.10、.gitignore 收纳 i18n/排版排查的一次性脚本。
2026-10-10 04:50:32 +08:00

118 lines
3.8 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 -*-
"""验证:新增的前端文件都会被自动指纹化(不依赖真机)。
⚠️ 为什么要专门验:1.0.10 新增了 5 个 web/*.js,如果它们没被
注入 `?v=<mtime>`,就会走 `no-cache` 分支 —— 浏览器每次都重拉,
倒也不影响正确性;但**如果将来有人给它们加上长缓存**
(`public, max-age=31536000, immutable`)而 URL 不带指纹,
用户改了语言/更新了版本后会**永远看不到新内容**。
所以现在就把「指纹化覆盖到新文件」钉住。
做法:抠出 _ASSET_RE,直接喂真实的 <script> 片段看有没有被替换。
"""
import io
import os
import re
import sys
os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SRC = io.open('backend/drouter-web.py', encoding='utf-8').read()
fails = []
n = 0
def ck(desc, cond, extra=''):
global n
n += 1
print(('ok ' if cond else 'FAIL ') + desc + (' ' + str(extra) if extra else ''))
if not cond:
fails.append(desc)
# ---- 抠出 _ASSET_RE 的正则 ----
m = re.search(r'_ASSET_RE = re\.compile\(\s*\n?(.*?)\)\n', SRC, re.S)
if not m:
print('✘ 找不到 _ASSET_RE')
sys.exit(1)
pat_src = m.group(1).strip()
try:
_r = eval(pat_src, {'__builtins__': {}}, {})
# 抠出来的可能是裸 r'...',也可能是 re.compile(r'...')。
# eval 一层拿到的是**字符串**(re.compile 对象不会被 eval 出),
# 所以要自己包一次 —— 我第一版忘了包,拿到字符串就用 .sub() 直接崩。
import re as _re
ASSET_RE = _r if hasattr(_r, 'sub') else _re.compile(_r)
except Exception as e:
print('✘ 正则无法编译:%s' % e)
print(' 内容:%s' % pat_src)
sys.exit(1)
print('正则:%s' % pat_src)
print()
# ---- 真机 index.html 里的实际脚本标签 ----
idx = io.open('web/index.html', encoding='utf-8').read()
scripts = re.findall(r'<script src="([^"]+)"', idx)
ck('index.html 里有脚本', len(scripts) > 0, len(scripts))
cache = {}
def ver(rel):
if rel not in cache:
f = os.path.join('web', rel.lstrip('/'))
try:
cache[rel] = int(os.path.getmtime(f))
except OSError:
cache[rel] = 0
return cache[rel]
def sub(mm):
pre, url, post = mm.group(1), mm.group(2), mm.group(3)
if '?' in url:
return mm.group(0)
return '%s%s?v=%d%s' % (pre, url, ver(url), post)
print('--- 逐个验证是否被指纹化 ---')
NEW_FILES = ['/i18n.js', '/update.js', '/upstream.js',
'/netdetail.js', '/realtime.js', '/app.js']
for s in scripts:
if not s.endswith('.js'):
continue
tag = '<script src="%s"></script>' % s
out = ASSET_RE.sub(sub, tag)
fingered = '?v=' in out
base = s.rsplit('.', 1)[-1]
label = '新文件 %s' % s if s in NEW_FILES else '原有 %s' % s
ck('%s 被指纹化' % label, fingered, out)
# ---- 反向:已带 ? 的不重复加 ----
tag = '<script src="/app.js?v=123"></script>'
out = ASSET_RE.sub(sub, tag)
ck('已带 ?v= 的不会被重复注入', out.count('?v=') == 1, out)
# ---- 边界:非 .js/.css 资源不处理 ----
tag = '<link href="/logo.svg">'
out = ASSET_RE.sub(sub, tag)
ck('.svg 不被处理(正则只匹配 js/css)', out == tag, out)
# ---- 关键:新文件必须真的存在于磁盘(否则 ver() 返回 0,注入 ?v=0 仍是长缓存 → 危险)----
print()
print('--- 新文件必须真实存在(否则 ?v=0 配长缓存 = 永不更新)---')
for f in NEW_FILES:
p = os.path.join('web', f.lstrip('/'))
ck('%s 存在' % f, os.path.isfile(p))
mtime = ver(f)
ck('%s 的 mtime 有效(非 0)' % f, mtime > 0, mtime)
print()
print('=' * 56)
print('指纹化检查 %d 条,失败 %d 条' % (n, len(fails)))
for f in fails:
print(' FAIL: %s' % f)
sys.exit(1 if fails else 0)