2026-09-04 14:43:22 +08:00
|
|
|
|
# -*- 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 这类)不在此列,
|
|
|
|
|
|
# 那些本来就该原样显示。
|
2026-09-04 15:17:06 +08:00
|
|
|
|
# 字段名到翻译组的对应。**同名不等于同组**:全局参数的 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",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 14:43:22 +08:00
|
|
|
|
ENUM_FIELDS = [
|
|
|
|
|
|
"frozen_reason", # 冻结来路:COMMAND_HALT / BRAKE / MANUAL / NONE
|
|
|
|
|
|
"cushion_state", # 安全垫:SOLID / THIN / NONE
|
|
|
|
|
|
"lot_kind", # 批次:BASE / FILL / ADD / DCA / T0 / RECON
|
2026-09-04 15:17:06 +08:00
|
|
|
|
"arbiter", # 谁裁的:rule / judge / user / system
|
2026-09-04 14:43:22 +08:00
|
|
|
|
"stype", # 交易方案类型:T0 / GRID / TRAIL
|
2026-09-04 15:17:06 +08:00
|
|
|
|
"autonomy", # 全局自主档:full / propose_only / off
|
2026-09-04 14:43:22 +08:00
|
|
|
|
"ref_source", # 参考位来源:bionic / self_calc / user / none
|
2026-09-04 15:17:06 +08:00
|
|
|
|
"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
|
2026-09-04 14:43:22 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-09-04 15:17:06 +08:00
|
|
|
|
# 这三个字段的取值是**开放集**,不许声称全集,也不许在兜底里吞掉原值:
|
|
|
|
|
|
# reject_code 由交易对端下发,协议文档明写可以补充
|
|
|
|
|
|
# level 上游信号原样透传,本仓库没有产出方
|
|
|
|
|
|
# accum_state 决策系统给的中文原文,本来就不需要翻译
|
|
|
|
|
|
# 它们不进上面的名单,但在这里登记一笔,免得以后有人当成漏网又去补翻译。
|
|
|
|
|
|
OPEN_SET_FIELDS = ["reject_code", "level", "accum_state", "msg_type"]
|
|
|
|
|
|
|
2026-09-04 14:43:22 +08:00
|
|
|
|
# 这些字段带 `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:
|
2026-09-04 15:17:06 +08:00
|
|
|
|
group = FIELD_GROUP.get(f)
|
2026-09-04 14:43:22 +08:00
|
|
|
|
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()
|