| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- # -*- 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
|