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

122 lines
5.0 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 -*-
"""钉住「所有真跑 app.js 的检查器都必须有 t() 桩」。
为什么需要这条(2026-10-05 的实际经过):
补完 i18n 后,app.js **顶层**就有 t('…') 调用了 —— 左侧导航的
`g: t('概览')` 之类是模块加载时求值的常量。于是每个把 app.js 灌进
vm / Function 求值的检查器都必须提供 t() 桩,否则在**第一行**就
`ReferenceError: t is not defined`:
check-render → theme/design 抛 t is not a function(真机部署被拦)
t-theme-parity → TM_VARS 求值 ReferenceError(回归被拦)
t-webterm → g: t('概览') ReferenceError(部署被拦)
三次都是**逐个被拦**,所以反过来加判据:新增检查器时若要跑 app.js,
忘了桩会立刻红,而不是等部署失败才发现。
判据:
① 列出所有**真跑** app.js 的文件(vm.runInContext / new Function(app.js))
② 每个都必须含 t 桩(`t:` / `const t =` / `.t =` 之一)
③ 列出全部 app.js 顶层 t() 调用数(应 > 0,作为「桩确实必要」的证据)
"""
import io
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
FAILS = []
N = 0
def chk(name, cond, detail=''):
global N
N += 1
if cond:
print(' ok ' + name)
else:
FAILS.append('%s %s' % (name, detail))
print(' FAIL %s %s' % (name, detail))
# ---- ① 找出**真执行** app.js 的文件 ----
# ⚠️ 判据要分清「**执行**」与「**解析**」——
# 2026-10-05 第一版把两者混在一起,误报了两个:
# check-jsstrip.js 只 new Function(src) 让 V8 **解析**(不执行)
# → 语法检查,不需要 t 桩
# bench-appjs.js 只 readFileSync 测体积 → 完全不执行
# 真执行的标志是 runInContext / vm.runInThisContext,
# 或者 new Function(...) 之后**真的调用**了它。
RUNNERS = []
for d in (HERE, os.path.join(ROOT, 'devtools')):
if not os.path.isdir(d):
continue
for fn in sorted(os.listdir(d)):
if not fn.endswith('.js'):
continue
p = os.path.join(d, fn)
try:
s = io.open(p, encoding='utf-8').read()
except Exception:
continue
if 'app.js' not in s:
continue
# 真执行:vm 里跑,或 new Function 之后调用了返回值
vm_run = bool(re.search(r'runInContext|runInNewContext|runInThisContext', s))
fn_called = bool(re.search(r'=\s*new Function\([^)]*\)\s*;?', s)) and \
bool(re.search(r'\(\s*\)\s*;|\(\s*\)\s*\n|\(\s*\);', s))
if vm_run or fn_called:
RUNNERS.append((p, s))
print('真跑 app.js 的检查器: %d 个' % len(RUNNERS))
if not RUNNERS:
chk('至少找到一个真跑 app.js 的检查器', False,
'判据本身可能失效(app.js 路径或执行原语的名字变了)')
else:
chk('至少找到一个真跑 app.js 的检查器', True)
# ---- ② 每个都要有 t 桩 ----
STUB = re.compile(
r"""(?:\bt\s*:\s*(?:\(?\s*\w+\s*\)?\s*=>)|"""
r"""\bconst\s+t\s*=|"""
r"""\b(?:sandbox|sb|ctxObj|ctx|context)\.t\s*=|"""
r"""\bt\s*=\s*\(\s*\w+\s*\)\s*=>\s*\w+)""")
for p, s in RUNNERS:
rel = os.path.relpath(p, ROOT).replace('\\', '/')
# t-i18n-run2.js 自己就是 i18n 判据,它 eval 的是 i18n.js 不是 app.js
if rel.endswith('t-i18n-run2.js'):
print(' skip %s(i18n 判据本身,eval 的是 i18n.js)' % rel)
continue
# t-real-cn2.js 只是把 app.js 当**文本**做正则比对(inTransform() 里
# indexOf/slice),并不 vm 执行它 —— 不需要 t 桩。判据按「提到 app.js
# + 用了 runInContext」粗筛,这类纯文本分析脚本会被误判。
if rel.endswith('t-real-cn2.js'):
print(' skip %s(只读 app.js 文本做正则,不执行)' % rel)
continue
# t-webterm.js 走 runInContext 但正文提到 app.js —— 它确实要桩
chk('%s 有 t() 桩' % rel, bool(STUB.search(s)),
'app.js 顶层有 t() 调用,缺桩会在第一行 ReferenceError')
# ---- ③ app.js 顶层 t() 调用数(桩确实必要的证据)----
app = io.open(os.path.join(ROOT, 'web', 'app.js'), encoding='utf-8').read()
# 粗略计数:t(' 出现总数(顶层 + 函数内都算,作为「规模」证据)
n_all = len(re.findall(r"(?<![A-Za-z0-9_$.])[tT]\('", app))
chk('app.js 里有大量 t() 调用(桩是必要的)', n_all > 100,
'只有 %d 处' % n_all)
# NAV 那种顶层常量(g: t('概览'))—— 单独证明「加载即求值」
n_top = 0
for m in re.finditer(r"^(\s*)g:\s*[tT]\(", app, re.M):
n_top += 1
chk('app.js 顶层常量里就有 t()(模块加载即求值)', n_top > 0,
'没找到 g: t(...) 形态 —— 若代码结构变了,②的桩要求可以放宽')
print('\n==== t-t-stub: %d 条判据, %d 失败 ====' % (N, len(FAILS)))
if FAILS:
for f in FAILS:
print(' - ' + f)
sys.exit(1)
print('T_STUB_OK')