Files
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

262 lines
11 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.
# -*- coding: utf-8 -*-
"""钉住「后端消息英文对照表」这条机制(MSG_EN / _MSG_RX / _msg_lookup / ok / fail)。
三段:
A. 表本身正确(占位符、英文无汉字、码值透传)
B. **查表真的生效** —— 抽出真函数 exec,不在源码里 grep 字面量
C. 反向验证:逐个注入、逐个确认变红、确认还原
为什么 B 段非要 exec 真代码:这一轮第一版就栽在这 ——
判据写成「源码里有没有 `return _msg_lookup(cn) or cn`」,全绿;
而产品里 msg_en 其实一直是 None(只有 msg 走了查表)。
静态断言全绿、真功能是坏的,只有真跑才抓得到。
⚠️ 反向验证会**改写 backend/drouter-helper.py**,因此:
- 子进程跑本文件时必须带 _TMSEN_CHILD=1,否则无限递归(实测直接超时)
- 判据开头先查「源码无注入残留」—— 被 SIGTERM 打断时 finally 不执行,
注入会留在文件里,后面的红全都不好解释(2026-10-05 真踩过)
- 备份放 _dev 内:Windows Python 读不到 Git Bash 的 /tmp
"""
import ast
import io
import os
import re
import shutil
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
HELPER = os.path.join(ROOT, 'backend', 'drouter-helper.py')
BAK = os.path.join(HERE, '_tmsgen.bak')
FAILS = []
N = 0
def chk(name, cond, detail=''):
global N
N += 1
if not cond:
FAILS.append('%s %s' % (name, detail))
print(' FAIL %s %s' % (name, detail))
else:
print(' ok %s' % name)
src = io.open(HELPER, encoding='utf-8').read()
# ---------------------------------------------------------------- A. 表结构
print('[A] 对照表结构')
# 只列**注入脚本真的会写进去**的标记。不要把自己在判据里写的探针也算进去 ——
# 否则探针会匹配到判据源码自己,恒红。
chk('源码无反向验证注入残留',
'INJECT-BROKEN-LOOKUP' not in src and 'INJECT-BAD-LOOKUP' not in src)
tree = ast.parse(src)
msg_en_node = msg_rx_node = None
for n in tree.body:
if isinstance(n, ast.Assign):
tid = getattr(n.targets[0], 'id', None)
if tid == 'MSG_EN':
msg_en_node = n
elif tid == '_MSG_RX':
msg_rx_node = n
chk('MSG_EN 是模块级常量', msg_en_node is not None)
chk('_MSG_RX 是模块级常量', msg_rx_node is not None)
chk('MSG_EN 是 dict 字面量',
msg_en_node is not None and isinstance(msg_en_node.value, ast.Dict))
tbl = {}
if msg_en_node is not None:
for k, v in zip(msg_en_node.value.keys, msg_en_node.value.values):
tbl[k.value] = v.value
chk('MSG_EN 非空', len(tbl) > 400, '实际 %d 条' % len(tbl))
bad_ph = []
for cn, en in tbl.items():
a = len(re.findall(r'%[-+ #0-9.]*[sdf]', cn))
b = len(re.findall(r'%[-+ #0-9.]*[sdf]', en))
if a != b:
bad_ph.append((cn, a, en, b))
chk('占位符个数一一对应', not bad_ph, str(bad_ph[:3]))
han = [(cn, en) for cn, en in tbl.items() if re.search(r'[\u4e00-\u9fff]', en)]
chk('英文值不含汉字', not han, str(han[:3]))
for code in ('NO_DOMAIN', 'NO_PUBLIC_IP', 'BAD_OP'):
chk('码值透传 %s' % code, tbl.get(code) == code, '实际 %r' % tbl.get(code))
# _MSG_RX 里必须是**已编译正则对象**,不是字符串 ——
# _msg_lookup 写的是 `_rx.match(cn)`,存字符串会 AttributeError。
rx_bad = []
if msg_rx_node is not None and isinstance(msg_rx_node.value, ast.List):
for el in msg_rx_node.value.elts:
if not (isinstance(el, ast.Tuple) and len(el.elts) == 3):
rx_bad.append('不是三元组')
continue
# re.compile 的 func 是 **Attribute**(value=Name('re'), attr='compile'),
# 不是 Name —— 2026-10-05 踩过:用 getattr(func,'id','') 判,恒为 '',
# 判据对真实的 re.compile() 也报红。两种形态都放行。
f = el.elts[1].func if isinstance(el.elts[1], ast.Call) else None
is_re = (isinstance(f, ast.Attribute) and f.attr == 'compile'
and isinstance(f.value, ast.Name) and f.value.id == 're')
is_re = is_re or (isinstance(f, ast.Name) and f.id == 're_compile')
if not is_re:
rx_bad.append('第 2 项不是 re.compile(): %s' % ast.dump(el.elts[1])[:60])
chk('_MSG_RX 每项都是 (tpl, re 对象, en)', not rx_bad, str(rx_bad[:2]))
# ---------------------------------------------------------------- B. 真跑
# 从真源码 ast 抽符号再 exec —— 测的是真代码。
print('\n[B] 真跑 _pick / _resolve / ok / fail')
ns = {'re': re}
FUNCS = ('_msg_lookup', '_pick', '_resolve', 'ok', 'fail')
CONSTS = ('MSG_EN', '_MSG_RX', '_cur_lang', '_DEFAULT_LANG')
picked = []
for n in tree.body:
if isinstance(n, ast.Assign) and getattr(n.targets[0], 'id', None) in CONSTS:
picked.append(n)
elif isinstance(n, ast.FunctionDef) and n.name in FUNCS:
picked.append(n)
chk('抽到 %d 个函数 + %d 个常量' % (len(FUNCS), len(CONSTS)),
len(picked) == len(FUNCS) + len(CONSTS), '实际 %d 个' % len(picked))
exec(compile(ast.Module(body=picked, type_ignores=[]), HELPER, 'exec'), ns)
pick, ok, fail, lookup = ns['_pick'], ns['ok'], ns['fail'], ns['_msg_lookup']
cur = ns['_cur_lang']
def set_lang(v):
cur[0] = v
set_lang('en-US')
chk('英文界面:字面量文案走精确键', pick('设置已保存', None) == 'Settings saved',
'实际 %r' % pick('设置已保存', None))
set_lang('zh-CN')
chk('中文界面:显示中文原文', pick('设置已保存', None) == '设置已保存')
chk('中文界面:忽略 en 参数', pick('设置已保存', 'Override') == '设置已保存')
set_lang('en-US')
chk('英文界面:显式 en 参数优先', pick('设置已保存', 'Override') == 'Override')
# 模板正则:必须**比对完整英文**,不能只比前缀。
# 只比前缀时,「捕获组没回填」会红成「查不到」,红的原因会误导排查方向。
for cn_tpl, args, expect in [
('未知操作:%s', ('badop',), 'Unknown action: badop'),
('读取失败:%s', ('/etc/x',), 'Failed to read: /etc/x'),
('不支持的打印服务操作:%s', ('xyz',), 'Unsupported print service action: xyz'),
('受保护的系统账号,禁止删除:%s(UID %d)', ('root', 0),
'Protected system account, deletion is forbidden: root (UID 0)'),
('未知操作:%s', ('another-op',), 'Unknown action: another-op'),
]:
rendered = cn_tpl % args
got = pick(rendered, None)
chk('模板正则兜底+回填: %s' % cn_tpl[:18], got == expect,
'渲染=%r 期望=%r 实得=%r' % (rendered, expect, got))
chk(' └ 英文里不该残留占位符: %s' % cn_tpl[:14], '%s' not in (got or ''),
'实得=%r' % got)
set_lang('en-US')
chk('查不到时回落中文(不空白)',
pick('这是一条完全不在表里的文案', None) == '这是一条完全不在表里的文案')
chk('空串输入不炸', pick('', None) == '')
o = ok({'x': 1}, '设置已保存')
chk('ok(): msg_cn 保留中文原文', o['msg_cn'] == '设置已保存', repr(o))
chk('ok(): msg_en 为英文(前端按语言自选时读它)',
o['msg_en'] == 'Settings saved', repr(o))
chk('ok(): msg 按语言选英文', o['msg'] == 'Settings saved', repr(o))
chk('ok(): data 透传', o['data'] == {'x': 1}, repr(o))
f = fail('快照不存在:%s' % 'snap1', 'NOT_FOUND')
chk('fail(): msg_cn 保留原文', f['msg_cn'] == '快照不存在:snap1', repr(f))
chk('fail(): msg_en 走模板正则并回填',
f['msg_en'] == 'Snapshot does not exist: snap1', repr(f))
chk('fail(): msg 走模板正则', f['msg'] == 'Snapshot does not exist: snap1', repr(f))
set_lang('zh-CN')
chk('fail(): 中文界面 msg 仍是中文',
fail('快照不存在:%s' % 'snap1', 'X')['msg'] == '快照不存在:snap1')
# 码值透传:英文界面下也照原样(这些是机器可读码,翻成句子反而破坏前端判断)
set_lang('en-US')
chk('码值 msg 透传', fail('NO_DOMAIN', 'NO_DOMAIN')['msg'] == 'NO_DOMAIN')
# 短模板不做正则兜底
mis = []
for probe in ('会话已保存', '服务已启动', '已处理 3 项', '计划 5 个'):
if pick(probe, None) != probe:
mis.append((probe, pick(probe, None)))
chk('短模板不做正则兜底(不误匹配)', not mis, str(mis))
# ---------------------------------------------------------------- 早退
IN_CHILD = os.environ.get('_TMSEN_CHILD') == '1'
if IN_CHILD:
print('\n(子进程模式:只跑 A/B 段,供父进程的反向验证读取)')
print('==== t-msg-en(child): %d 条判据, %d 失败 ====' % (N, len(FAILS)))
sys.exit(1 if FAILS else 0)
# ---------------------------------------------------------------- C. 反向验证
print('\n[C] 反向验证(逐个注入 → 必须变红 → 还原)')
CHILD_ENV = dict(os.environ, _TMSEN_CHILD='1')
def run_child():
r = subprocess.run([sys.executable, __file__], capture_output=True,
text=True, env=CHILD_ENV)
return r.stdout + r.stderr
shutil.copyfile(HELPER, BAK)
orig = io.open(HELPER, encoding='utf-8').read()
try:
# C1: 摘掉查表 —— 英文界面整体回落中文
a1 = " en = en or _msg_lookup(msg)"
chk('C1 锚点在当前源码里存在', orig.count(a1) == 1,
'锚点出现 %d 次' % orig.count(a1))
inj = orig.replace(a1, " en = en # INJECT-BROKEN-LOOKUP", 1)
chk('C1 注入点唯一', inj != orig and inj.count('INJECT-BROKEN-LOOKUP') == 1)
io.open(HELPER, 'w', encoding='utf-8', newline='').write(inj)
c = run_child()
red = [l for l in c.split('\n') if 'FAIL' in l]
chk('C1 摘掉查表 -> 判据变红', bool(red), '重跑后没有任何 FAIL 行')
chk('C1 红在「msg_en 变 None」那条上(而非红在别处)',
any('msg_en' in l for l in red),
'期望红在 ok(): msg_en 为英文,实际红在:\n ' + '\n '.join(red)[:400])
# C2: 破坏捕获组回填(第一版就是这么写的:直接 return _en)
# 这条比 C1 隐蔽:C1 破坏后英文界面全变中文,一眼就看出来;
# C2 破坏后英文界面**大部分正常**,只有带占位符的那几条多出 %s。
shutil.copyfile(BAK, HELPER)
orig2 = io.open(HELPER, encoding='utf-8').read()
a2 = " return _en % m.groups()"
chk('C2 锚点在当前源码里存在', orig2.count(a2) == 1,
'锚点出现 %d 次' % orig2.count(a2))
inj2 = orig2.replace(a2, " return _en # INJECT-BAD-LOOKUP", 1)
chk('C2 注入点唯一', inj2 != orig2 and inj2.count('INJECT-BAD-LOOKUP') == 1)
io.open(HELPER, 'w', encoding='utf-8', newline='').write(inj2)
c2 = run_child()
red2 = [l for l in c2.split('\n') if 'FAIL' in l]
chk('C2 破坏回填 -> 判据变红', bool(red2), '重跑后没有任何 FAIL 行')
chk('C2 红在「不该残留占位符」那条上',
any('不该残留' in l for l in red2), '\n '.join(red2)[:500])
finally:
shutil.copyfile(BAK, HELPER)
if os.path.exists(BAK):
os.remove(BAK)
restored = io.open(HELPER, encoding='utf-8').read()
chk('注入还原后源码与原始一致', restored == orig)
c3 = run_child()
head = c3.split('[C]')[0]
chk('还原后 A/B 段全绿', 'FAIL' not in head,
'\n'.join(l for l in head.split('\n') if 'FAIL' in l)[:600])
print('\n==== t-msg-en: %d 条判据, %d 失败 ====' % (N, len(FAILS)))
if FAILS:
for x in FAILS:
print(' - ' + x)
sys.exit(1)
print('MSG_EN_OK')