# -*- coding: utf-8 -*- """页面文案守卫:枚举字段不许直接印到页面上(静态扫描,不连库、不起浏览器)。 ## 为什么要有它 app/web/static/index.html 是三千行的单文件页面,**没有任何单测**。改错了不会有 东西报警。而它上面最容易出的错,是把后端送来的枚举值直接塞进模板—— 页面上就出现 COMMAND_HALT、REJECT、issue_command:EXIT_STOCK 这种东西。 页面里其实早就有翻译层(T 表加 tx 函数),问题是它被系统性地绕开了: 2026-09-04 实测,同一份文件里 :899 走了 tx('frozen', …)、:1533 没走,印的是 同一个字段。这不是没想到,是执行漏网。所以需要一道机器检查。 ## 它检查什么 一、**枚举字段直出**:模板里形如 {{ x.frozen_reason }} 的插值,字段名落在 ENUM_FIELDS 里,而外面没有包 tx( 或别的翻译函数,就报。 二、**判据码直出**:failed_checks、reason 这类会带 `CODE: ` 前缀的字段, 必须过 plainCheck 才能显示。 三、**整个对象打给人看**:JSON.stringify 出现在给交易员看的区域就报。 运维面板那几处是有意的,登记在 JSON_OK 里放行。 四、**翻译表兜底不许裸奔**:tx 找不到码时必须把码退到括号里, 不能让一个英文词单独立在页面上当句子。 ## 怎么维护 加了新的枚举字段就往 ENUM_FIELDS 里加一行。这份名单是这道检查的全部依据, 不加就等于不检查——所以它和 T 表要一起改。 跑法:python3 scripts/test_page_enum_guard.py """ import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) PAGE = os.path.join(os.path.dirname(HERE), "app", "web", "static", "index.html") # 后端会送到页面的枚举字段。取值全集在 T 表里,两者要一起改。 # 只列**枚举**字段:后端已经写成中文句子的自由文本(basis、why、note 这类)不在此列, # 那些本来就该原样显示。 # 字段名到翻译组的对应。**同名不等于同组**:全局参数的 autonomy 是 # full / propose_only / off(param_store.py:142),而个股交易方案的 autonomy 是 # auto / confirm(strategy_service.py:23),两套取值完全不同。所以这份对应表里 # 按"字段在哪个对象上"区分,不能只按字段名。 FIELD_GROUP = { "frozen_reason": "frozen", "cushion_state": "cushion", "lot_kind": "lot", "arbiter": "arbiter", "stype": "stype", "ref_source": "refSource", "autonomy": "autonomy", # 全局参数那一套 "strategy_autonomy": "stratAutonomy", # 个股交易方案那一套,取值不同 "judge_verdict": "judgeVerdict", "origin_type": "originType", "cancel_state": "cancelState", "conn_state": "connState", "proc_state": "inboxState", } ENUM_FIELDS = [ "frozen_reason", # 冻结来路:COMMAND_HALT / BRAKE / MANUAL / NONE "cushion_state", # 安全垫:SOLID / THIN / NONE "lot_kind", # 批次:BASE / FILL / ADD / DCA / T0 / RECON "arbiter", # 谁裁的:rule / judge / user / system "stype", # 交易方案类型:T0 / GRID / TRAIL "autonomy", # 全局自主档:full / propose_only / off "ref_source", # 参考位来源:bionic / self_calc / user / none "judge_verdict", # 研判闸结论:PASS / REJECT / UNAVAILABLE / STRATEGY / STRATEGY_RISK "origin_type", # 提议来路:plan / proposal / system / strategy "cancel_state", # 撤单进度:NONE / REQUESTED / SENT "conn_state", # 通道状态:INIT / CONNECTING / ONLINE / OFFLINE / STOPPED ] # 这三个字段的取值是**开放集**,不许声称全集,也不许在兜底里吞掉原值: # reject_code 由交易对端下发,协议文档明写可以补充 # level 上游信号原样透传,本仓库没有产出方 # accum_state 决策系统给的中文原文,本来就不需要翻译 # 它们不进上面的名单,但在这里登记一笔,免得以后有人当成漏网又去补翻译。 OPEN_SET_FIELDS = ["reject_code", "level", "accum_state", "msg_type"] # 这些字段带 `CODE: ` 前缀,显示前必须剥掉。 CODED_FIELDS = ["failed_checks"] # 把整个对象打给人看,只有落在**交易员会看的区域**才算问题。两类不算: # 一是存本地状态(sessionStorage / localStorage),那不是给人看的; # 二是运维面板里那几个"原样看接口返回"的框,读者是运维不是交易员, # 它们本来就该显示原始数据。 # 判定靠变量名:写进 opsResult / planResult 这类运维面板的框,或者进浏览器存储, # 都放行。别的地方出现 JSON.stringify 就报。 JSON_SKIP_MARKS = ( "sessionStorage", "localStorage", # 存本地状态,不是给人看的 "opsResult", "planResult", # 运维面板:有意显示接口原文 "console.", # 控制台 # 下面几处都在运维面板里,读者是运维不是交易员,本来就该看原始数据。 # 每加一条都要写清它在哪个面板、为什么放行。 "planStatus.theme_sync", # 上游计划诊断行:主题映射,排查用 "planStatus.query", # 上游计划诊断行:本次请求参数,排查用 "chgLog", # 运维面板「快照台账」:整行原文 "report.report", # 运维面板「快照台账」:日报原文 "wsc.queue", # 运维面板「ws 通道」:出口队列计数 "s.row.payload", # 运维面板「ws 通道」:报文原文 "s.row.hard_numbers", # 只在悬浮提示里作原文备查,正文已是中文 "s.row)", # 运维面板「快照台账」:整行原文 ) # 允许包住枚举字段的翻译函数。 WRAPPERS = ("tx(", "opCn(", "plainCheck(", "propVerdictLine(") def fail(msgs): for m in msgs: print(" FAIL", m) def main(): src = open(PAGE, encoding="utf-8").read() lines = src.split("\n") bad = [] # 一、枚举字段直出 for i, line in enumerate(lines, 1): for f in ENUM_FIELDS: # 只看插值与属性绑定里出现的字段引用 for m in re.finditer(r"[\w.\[\]]*\.%s\b" % re.escape(f), line): seg = line[max(0, m.start() - 60):m.end() + 20] if any(w in seg for w in WRAPPERS): continue # v-if 这类判断用途不算显示 if re.search(r'v-(if|else-if|show)="[^"]*\.%s\b' % re.escape(f), line): continue # JS 里做比较也不算显示 if re.search(r"\.%s\s*(===|!==|==|!=|&&|\|\||\?)" % re.escape(f), seg): continue if "{{" not in line and ":title=" not in line and ":content=" not in line: continue bad.append(f"第 {i} 行:枚举字段 {f} 直接印到页面上,没走翻译。\n" f" {line.strip()[:120]}") # 二、判据码直出 for i, line in enumerate(lines, 1): for f in CODED_FIELDS: if f"{f}" in line and "{{" in line and "plainCheck" not in line: bad.append(f"第 {i} 行:{f} 带大写判据码前缀,显示前必须过 plainCheck。\n" f" {line.strip()[:120]}") # 三、整个对象打给人看 for i, line in enumerate(lines, 1): if "JSON.stringify" in line: if not any(k in line for k in JSON_SKIP_MARKS): bad.append(f"第 {i} 行:把整个对象打给交易员看。要么改成中文说明," f"要么说明它落在运维面板并登记进 JSON_SKIP_MARKS。\n" f" {line.strip()[:120]}") # 四、兜底不许裸奔 m = re.search(r"const tx = \(g, c\) => \{(.*?)\n \};", src, re.S) if not m: bad.append("找不到 tx 函数,这道检查的前提没了") elif "return String(c);" in m.group(1): bad.append("tx 的兜底是原样返回,会让一个英文词单独立在页面上当句子。" "改成把码退到括号里。") # 五、翻译表与本文件的字段名单要对得上 for f in ENUM_FIELDS: group = FIELD_GROUP.get(f) if group and f" {group}: {{" not in src and f" {group}:{{" not in src: bad.append(f"字段 {f} 在名单里,但 T 表里没有对应的 {group} 组") if bad: print(f"页面文案守卫:发现 {len(bad)} 处") fail(bad) sys.exit(1) print(f"ALL OK — 页面文案守卫:{len(ENUM_FIELDS)} 个枚举字段、" f"{len(CODED_FIELDS)} 个带判据码的字段、整对象直出、翻译兜底 全部通过") if __name__ == "__main__": main()