|
@@ -1,233 +0,0 @@
|
|
|
-# -*- coding: utf-8 -*-
|
|
|
|
|
-"""新前端单文件快照 —— 把页面会取的接口响应烤进 HTML, 静态站 (云上 nginx) 双击即开。
|
|
|
|
|
-
|
|
|
|
|
-与经典 /api/snapshot 的分工: 经典页烤 2874 个 key (它的页面会取很多带参接口); 新版取数面小得多,
|
|
|
|
|
-只烤下面这几类。拦截器策略与经典一致 —— **实时优先、快照兜底**:
|
|
|
|
|
- · ask / ask_status / ask_models / rpt_compose / rpt_export → 永远走网络 (云上 nginx /api/ 后面有真服务)
|
|
|
|
|
- · ont_chain / ont_obj / ont_query → 先网络, 取不到再回落快照 (离线可用)
|
|
|
|
|
- · 其余 (fleet / curves / maint_survey / vibcms) → 只走快照; 未烤的窗回落到默认窗
|
|
|
|
|
-
|
|
|
|
|
-英文版: 与在线 API 同一套投影 (_redact → _proj)。公网默认 slim: 不携带 _original 原文副本, 只烤默认窗;
|
|
|
|
|
-需要内部追溯时可用 slim=0 生成全量包。上云前仍过 scripts/windscada_cloud_pack.py 的脱敏闸。
|
|
|
|
|
-"""
|
|
|
|
|
-from __future__ import annotations
|
|
|
|
|
-
|
|
|
|
|
-import json
|
|
|
|
|
-import re
|
|
|
|
|
-import time
|
|
|
|
|
-from typing import Optional
|
|
|
|
|
-
|
|
|
|
|
-from . import build
|
|
|
|
|
-
|
|
|
|
|
-WINS = ['2026年', '2026H1', '2025H2', '全程', '近30日', '近90日']
|
|
|
|
|
-SYS9 = ['叶片与叶根', '变桨', '偏航', '齿轮箱', '发电机', '主轴承', '变流器', '塔架与基础', '主控与传感网']
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-_PUBLIC_LITERAL_CJK = re.compile(r'[\u3400-\u9fff\u3000-\u303f\uff00-\uffef]')
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _strip_public_comments(page: str) -> str:
|
|
|
|
|
- """Remove build-time comments from the public English artifact.
|
|
|
|
|
-
|
|
|
|
|
- The public snapshot is a product demo, so design notes, user-decision notes,
|
|
|
|
|
- and internal audit language should not ship inside CSS/JS comments.
|
|
|
|
|
- """
|
|
|
|
|
- page = re.sub(r'/\*.*?\*/', '', page, flags=re.S)
|
|
|
|
|
- page = re.sub(r'(?m)^[ \t]*//.*(?:\n|$)', '', page)
|
|
|
|
|
- return page
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _escape_public_cjk_source(page: str) -> str:
|
|
|
|
|
- """Unicode-escape CJK and full-width characters in public English source.
|
|
|
|
|
-
|
|
|
|
|
- Some Chinese values are still legitimate machine keys in the browser logic
|
|
|
|
|
- and snapshot lookup tables. Escaping keeps JavaScript semantics intact while
|
|
|
|
|
- preventing literal Chinese from appearing in the public HTML source.
|
|
|
|
|
- Runtime display is still audited separately.
|
|
|
|
|
- """
|
|
|
|
|
- return _PUBLIC_LITERAL_CJK.sub(lambda m: '\\u%04x' % ord(m.group(0)), page)
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _scrub_public_ascii_source(page: str) -> str:
|
|
|
|
|
- """Remove public-facing ASCII placeholders and internal handoff labels."""
|
|
|
|
|
- replacements = [
|
|
|
|
|
- (r'\[translation pending\]', 'Review required'),
|
|
|
|
|
- (r'Vibration-line handoff', 'Vibration evidence package'),
|
|
|
|
|
- (r'python(?:3)?\s+-c\s+"[^"]+"', 'managed knowledge-graph refresh'),
|
|
|
|
|
- (r'python(?:3)?\s+-c\s+\\"[^"]+\\"', 'managed knowledge-graph refresh'),
|
|
|
|
|
- (r'handoff[_ -]?vibration[_ -]?v2\.json', 'Vibration evidence package'),
|
|
|
|
|
- (r'handoff[_ -]?vibration[_ -]?v2', 'vibration evidence package'),
|
|
|
|
|
- (r'handoff\\u65e5\\u671f', 'evidence_package_date'),
|
|
|
|
|
- (r'handoff日期', 'evidence_package_date'),
|
|
|
|
|
- (r'only_handoff', 'only_vibration_package'),
|
|
|
|
|
- (r'transcribed from the vibration line', 'reported by the vibration evidence line'),
|
|
|
|
|
- (r'verbatim transcription', 'source-grounded evidence'),
|
|
|
|
|
- (r'fusion does not assign grades', 'fusion preserves the source rating'),
|
|
|
|
|
- (r'hardest evidence', 'strongest evidence'),
|
|
|
|
|
- (r'currently rising', 'active trend'),
|
|
|
|
|
- (r'outputs/rudong', 'outputs/<site>'),
|
|
|
|
|
- (r'Rudong', 'offshore site'),
|
|
|
|
|
- (r'Huanneng', 'site operator'),
|
|
|
|
|
- (r'BPFI|BPFO|FTF|BSF', 'bearing marker'),
|
|
|
|
|
- (r'characteristic frequenc(?:y|ies)', 'bearing signature marker'),
|
|
|
|
|
- (r'spectra', 'condition traces'),
|
|
|
|
|
- (r'spectrum', 'condition trace'),
|
|
|
|
|
- (r'waveforms?', 'condition traces'),
|
|
|
|
|
- (r'\bAddress\b', 'Resolve'),
|
|
|
|
|
- (r'\baddress\b', 'resolve'),
|
|
|
|
|
- (r'addressing', 'resolving'),
|
|
|
|
|
- (r'Addressing', 'Resolving'),
|
|
|
|
|
- (r'coordinate(?:d)?', 'aligned'),
|
|
|
|
|
- (r'\bOwner\b', 'Client'),
|
|
|
|
|
- (r'\bowner\b', 'client'),
|
|
|
|
|
- (r'\bAddress\b', 'Resolve'),
|
|
|
|
|
- (r'\baddress\b', 'resolve'),
|
|
|
|
|
- ]
|
|
|
|
|
- for pat, repl in replacements:
|
|
|
|
|
- page = re.sub(pat, repl, page, flags=re.I)
|
|
|
|
|
- return page
|
|
|
|
|
-
|
|
|
|
|
-def public_literal_cjk_report(page: str) -> list[str]:
|
|
|
|
|
- """Return short literal-CJK snippets for the public source gate."""
|
|
|
|
|
- hits = []
|
|
|
|
|
- for m in _PUBLIC_LITERAL_CJK.finditer(page):
|
|
|
|
|
- a = max(0, m.start() - 24)
|
|
|
|
|
- b = min(len(page), m.end() + 24)
|
|
|
|
|
- hits.append(page[a:b].replace('\n', '\\n'))
|
|
|
|
|
- if len(hits) >= 20:
|
|
|
|
|
- break
|
|
|
|
|
- return hits
|
|
|
|
|
-def _nk(path: str, **kw) -> str:
|
|
|
|
|
- """与页内 __NK 同语义: 参数解码后按字母排序, 空值丢弃。"""
|
|
|
|
|
- a = sorted('%s=%s' % (k, v) for k, v in kw.items() if v not in (None, ''))
|
|
|
|
|
- return path + ('?' + '&'.join(a) if a else '')
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _strip_original(o):
|
|
|
|
|
- """Drop internal source-text mirrors from public English snapshots."""
|
|
|
|
|
- if isinstance(o, dict):
|
|
|
|
|
- return {k: _strip_original(v) for k, v in o.items() if k != '_original'}
|
|
|
|
|
- if isinstance(o, list):
|
|
|
|
|
- return [_strip_original(v) for v in o]
|
|
|
|
|
- return o
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def bake(lang: str, ctx: dict, peer: str = '', noindex: bool = False, slim: Optional[bool] = None) -> tuple[str, dict]:
|
|
|
|
|
- """返回 (html, report)。ctx 由服务端注入 (fleet_view/curve_view/…/_redact/_jclean/_proj/_REQ/ont)。
|
|
|
|
|
-
|
|
|
|
|
- peer 对方语言快照的相对 URL。**静态文件下 `?lang=` 不起作用** —— 点了只会重载同一个文件,
|
|
|
|
|
- 所以双语互切必须指向对方的真实文件。空 = 该版不提供切换 (公开英文版不链内部中文版)。
|
|
|
|
|
- noindex 写入 robots meta。站点已有 robots.txt Disallow, 这是第二道 (含真名的中文内部件用)。
|
|
|
|
|
- slim 英文公网瘦身模式: 只烤默认窗, 且删除 _original 原文副本; None 表示英文默认开启。
|
|
|
|
|
- """
|
|
|
|
|
- en = lang == 'en'
|
|
|
|
|
- if slim is None:
|
|
|
|
|
- slim = en
|
|
|
|
|
- t0 = time.time()
|
|
|
|
|
- snap, skip = {}, []
|
|
|
|
|
- R = ctx['_REQ']
|
|
|
|
|
- R.lang = 'en' if en else ''
|
|
|
|
|
- R.keep_orig = bool(en and not slim) # full 英文包才留原文; slim 未译出则显式 pending
|
|
|
|
|
-
|
|
|
|
|
- def lp(**kw): # 英文取数带 lang/keep (与 app.js 的 URL 一致), 中文不带
|
|
|
|
|
- if en:
|
|
|
|
|
- kw.update(lang='en', keep='1')
|
|
|
|
|
- return kw
|
|
|
|
|
-
|
|
|
|
|
- def _bake(key, fn):
|
|
|
|
|
- try:
|
|
|
|
|
- v = ctx['_redact'](ctx['_jclean'](fn()))
|
|
|
|
|
- if en:
|
|
|
|
|
- v = ctx['_proj'](v, 'en')
|
|
|
|
|
- snap[key] = _strip_original(v) if (en and slim) else v
|
|
|
|
|
- return True
|
|
|
|
|
- except Exception as e: # 失败记名不静默 —— 守护步骤失败必须响亮
|
|
|
|
|
- skip.append('%s: %s' % (key, str(e)[:80]))
|
|
|
|
|
- return False
|
|
|
|
|
-
|
|
|
|
|
- active_wins = WINS[:1] if (en and slim) else WINS
|
|
|
|
|
- for w in active_wins:
|
|
|
|
|
- _bake(_nk('/api/fleet', **lp(win=w)), lambda w=w: ctx['fleet_view'](w))
|
|
|
|
|
- # 曲线按窗重算(2026-09-21 用户令「发电性能随时间窗变化」):离线快照只烤**首个窗**
|
|
|
|
|
- # (每窗 10~45 s,全烤 6 窗会让打包多等几分钟;页面选其它窗时按"未烤 ⇒ 走实时接口"回落)。
|
|
|
|
|
- _bake(_nk('/api/curves', **lp(win=WINS[0])), lambda: ctx['curve_view'](WINS[0]))
|
|
|
|
|
- _bake(_nk('/api/maint_survey', **lp()), lambda: dict(rows=ctx['survey']()))
|
|
|
|
|
- _bake(_nk('/api/vibcms', **lp()), ctx['vibcms_results'])
|
|
|
|
|
- om = ctx['ont']
|
|
|
|
|
- for s in SYS9:
|
|
|
|
|
- _bake(_nk('/api/ont_chain', **lp(kind='prev', system=s)), lambda s=s: om.ont_chain_preventive(system=s))
|
|
|
|
|
- _bake(_nk('/api/ont_chain', **lp(kind='tree', system=s)), lambda s=s: om.ont_fault_tree(system=s))
|
|
|
|
|
- codes = ctx['alarm_codes']()
|
|
|
|
|
- for c in codes:
|
|
|
|
|
- _bake(_nk('/api/ont_chain', **lp(kind='fault', code=c)), lambda c=c: om.ont_chain_fault(code=c))
|
|
|
|
|
-
|
|
|
|
|
- # ── 逐台 / 单问题 (浮层用) ────────────────────────────────────────
|
|
|
|
|
- # /turbine/<t> 与 /problem/<t>/<sys> 原是**服务端独立页**, 静态站上一律 404 (2026-09-03 实逮:
|
|
|
|
|
- # 部件问题页点机组号或系统名 → 404)。前端已改为页内浮层, 这里把它要的数据烤进来。
|
|
|
|
|
- # **只烤默认窗**: 每台每系统再乘 6 个窗会让快照翻几倍, 而拦截器对未烤的窗本就会回落默认窗。
|
|
|
|
|
- w0 = WINS[0]
|
|
|
|
|
- fl0 = snap.get(_nk('/api/fleet', **lp(win=w0))) or {}
|
|
|
|
|
- pairs = set()
|
|
|
|
|
- for sysn, rows in (fl0.get('systems') or {}).items():
|
|
|
|
|
- base = sysn.split('·')[0] # 分组键可能带等级后缀 (塔架与基础·良好)
|
|
|
|
|
- for r in (rows or []):
|
|
|
|
|
- if isinstance(r, dict) and r.get('t'):
|
|
|
|
|
- pairs.add((r['t'], base))
|
|
|
|
|
- turbs = sorted({t for t, _ in pairs})
|
|
|
|
|
- for tb in turbs:
|
|
|
|
|
- _bake(_nk('/api/turbine', **lp(t=tb, win=w0)), lambda tb=tb: dict(problems=ctx['turbine_problems'](tb, w0)))
|
|
|
|
|
- for tb, sy in sorted(pairs):
|
|
|
|
|
- _bake(_nk('/api/problem', **lp(t=tb, sys=sy, win=w0)),
|
|
|
|
|
- lambda tb=tb, sy=sy: dict(problems=ctx['single_problem'](tb, sy, w0)))
|
|
|
|
|
-
|
|
|
|
|
- page = build.render(lang, include_records=('terms' if (en and slim) else True))
|
|
|
|
|
- if en and slim:
|
|
|
|
|
- page = _strip_public_comments(page)
|
|
|
|
|
- wins_baked = [w for w in active_wins if _nk('/api/fleet', **lp(win=w)) in snap]
|
|
|
|
|
- inject = ('<script>window.__SNAP__=' + json.dumps(snap, ensure_ascii=False, allow_nan=False).replace('<', '\\u003c') + ';\n'
|
|
|
|
|
- 'window.__WINS=' + json.dumps(wins_baked, ensure_ascii=False) + ';window.__DEFWIN=window.__WINS[0]||"";\n'
|
|
|
|
|
- # key 归一化: 参数解码后排序 —— 编码差异与参数顺序都会让原样 URL 比对静默落空
|
|
|
|
|
- 'window.__NK=function(u){var s=String(u),i=s.indexOf("?");if(i<0)return s;'
|
|
|
|
|
- 'var p=s.slice(0,i),a=s.slice(i+1).split("&").filter(Boolean).map(function(kv){'
|
|
|
|
|
- 'var j=kv.indexOf("=");if(j<0)return kv;var k=kv.slice(0,j),v=kv.slice(j+1);'
|
|
|
|
|
- 'try{v=decodeURIComponent(v.replace(/\\+/g," "))}catch(e){}'
|
|
|
|
|
- 'return v===""?null:k+"="+v}).filter(function(x){return x!==null}).sort();'
|
|
|
|
|
- 'return p+(a.length?"?"+a.join("&"):"")};\n'
|
|
|
|
|
- 'window.__PEER=' + json.dumps(peer) + ';\n'
|
|
|
|
|
- 'window.__HIT={ok:0,miss:[]};window.__RF=window.fetch;\n'
|
|
|
|
|
- # ★win 候选键不能 encodeURIComponent —— __NK 已把 k 里所有值 decode 回原文
|
|
|
|
|
- # (含 win=近30日 这种中文), __SNAP__ 的键也是 _nk() 直接拼的原文, 两边都不编码;
|
|
|
|
|
- # 这里若再 encodeURIComponent 就会拼出 win=2026%E5%B9%B4 这种谁都不匹配的键,
|
|
|
|
|
- # 导致非默认窗下的兜底**从未生效** (2026-09-03 实逮: 换窗点浮层报"本窗无记录",
|
|
|
|
|
- # 而对应默认窗的快照其实有数据 —— 兜底静默失效, 不是真没数据)。
|
|
|
|
|
- 'function __fromSnap(k){var c=[k,k.replace(/win=[^&]*/,"win="+(window.__DEFWIN||""))];'
|
|
|
|
|
- 'for(var i=0;i<c.length;i++){var d=window.__SNAP__[c[i]];if(d!==undefined){window.__HIT.ok++;'
|
|
|
|
|
- 'return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(d)}})}}'
|
|
|
|
|
- 'if(window.__HIT.miss.indexOf(k)<0)window.__HIT.miss.push(k);'
|
|
|
|
|
- 'return Promise.resolve({ok:false,status:404,json:function(){return Promise.resolve({err:"snapshot miss: "+k})}})}\n'
|
|
|
|
|
- 'window.fetch=function(u,o){var s=String(u),k=window.__NK(s);'
|
|
|
|
|
- 'if(/^\\/api\\/(ask|ask_status|ask_models|rpt_compose|rpt_export)\\b/.test(s))return window.__RF(u,o);'
|
|
|
|
|
- 'return __fromSnap(k)};\n'
|
|
|
|
|
- '</script>\n')
|
|
|
|
|
- # 注入在语言包之后、图元之前 (拦截器必须先于 app.js 装上)
|
|
|
|
|
- marker = '<script>window.__L='
|
|
|
|
|
- assert marker in page, '骨架缺语言包注入点'
|
|
|
|
|
- i = page.index(marker)
|
|
|
|
|
- j = page.index('</script>', i) + len('</script>')
|
|
|
|
|
- page = page[:j] + '\n' + inject + page[j:]
|
|
|
|
|
- if noindex:
|
|
|
|
|
- page = page.replace('<meta charset="utf-8">',
|
|
|
|
|
- '<meta charset="utf-8">\n<meta name="robots" content="noindex,nofollow,noarchive">', 1)
|
|
|
|
|
- if en and slim:
|
|
|
|
|
- page = _scrub_public_ascii_source(page)
|
|
|
|
|
- page = _escape_public_cjk_source(page)
|
|
|
|
|
- leaks = public_literal_cjk_report(page)
|
|
|
|
|
- if leaks:
|
|
|
|
|
- raise RuntimeError('public English snapshot still contains literal CJK: ' + repr(leaks[:5]))
|
|
|
|
|
- R.lang = ''
|
|
|
|
|
- R.keep_orig = False
|
|
|
|
|
- rep = dict(lang=lang, peer=peer, noindex=noindex, slim=bool(en and slim), keys=len(snap), turbines=len(turbs), pairs=len(pairs), wins=wins_baked, codes=len(codes), skipped=skip,
|
|
|
|
|
- bytes=len(page.encode('utf-8')), secs=round(time.time() - t0, 1))
|
|
|
|
|
- return page, rep
|
|
|