sim_sys_server.py 7.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. import html, re, zipfile
  2. from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  3. from urllib.parse import unquote, urlparse
  4. import pathlib
  5. ARCHIVE = str(pathlib.Path(__file__).resolve().parent / '如东SWT40_控制律仿真台_20260906.zip')
  6. def readable(name):
  7. try:
  8. return name.encode('cp437').decode('utf-8')
  9. except UnicodeError:
  10. return name
  11. with zipfile.ZipFile(ARCHIVE) as archive:
  12. PAGES = {
  13. readable(name.rsplit('/', 1)[-1]): archive.read(name).decode('utf-8')
  14. for name in archive.namelist()
  15. if name.endswith('.html') and readable(name.rsplit('/', 1)[-1])[:1] in {'0', '1', '2', '3', '4'}
  16. }
  17. THEME = '''
  18. <style id="guanlan-subsystem-theme">
  19. :root{--bg:#edf7f9;--panel:#ffffff;--ink:#133047;--ink2:#28536b;--ink3:#61788b;--line:#cbe2eb;--line2:#e1eef3;--spec:#0e8898;--specf:#dff4f4;--meas:#2077a8;--measf:#e5f2fa;--ok:#23875b;--warn:#d48818;--bad:#d94b43}
  20. html,body{background:var(--bg)!important;color:var(--ink)!important}.wrap{max-width:1420px!important;padding:20px 28px 44px!important}.guanlan-top{display:flex;align-items:center;justify-content:space-between;gap:18px;margin:0 0 18px;padding:16px 20px;border-radius:18px;background:linear-gradient(110deg,#10384d,#0e6470);color:#fff;box-shadow:0 12px 30px #0c50661c}.guanlan-top b{font-size:15px;letter-spacing:.04em}.guanlan-top a{color:#e8fbff;text-decoration:none;border:1px solid #a7dce3;border-radius:999px;padding:8px 13px;font-size:13px}.hd{border-radius:20px!important;overflow:hidden}.panel,.ctl,.note,.box{border-color:var(--line)!important;box-shadow:0 8px 22px #1450660b!important}.ctl{background:#f8fcfd!important}.ctl label{color:var(--ink2)!important}.ctl input,.ctl select{accent-color:#0e8898!important}.card,.panel{background:#fff!important}.btn,.primary{background:#0e8898!important;color:white!important;border-color:#0e8898!important}.back{margin:0!important}#th{display:none!important}
  21. .thermal-band{grid-column:1/-1;display:grid;grid-template-columns:1.2fr 2.8fr;gap:16px;align-items:center;background:linear-gradient(100deg,#f7fbfc,#e9f8f8);border:1px solid #bcdfe5;border-radius:16px;padding:14px 16px;margin:3px 0 10px}.thermal-band h2{font-size:16px;margin:0 0 4px;color:#133047}.thermal-band p{margin:0;color:#61788b;font-size:13px}.thermal-meter{position:relative;height:24px;border-radius:999px;background:linear-gradient(90deg,#2a9d68 0 50%,#e2aa2a 50% 84%,#dc5148 84%);box-shadow:inset 0 0 0 1px #9fc7d2}.thermal-meter::before{content:'';position:absolute;top:-4px;bottom:-4px;left:calc(var(--temp-pct,22)*1%);width:4px;border-radius:3px;background:#133047;box-shadow:0 0 0 3px #fff}.thermal-meter::after{content:attr(data-temp);position:absolute;top:-30px;left:calc(var(--temp-pct,22)*1%);transform:translateX(-50%);white-space:nowrap;font:700 13px/1.1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#133047}.thermal-scale{display:flex;justify-content:space-between;margin-top:7px;color:#61788b;font:11px ui-monospace,SFMono-Regular,Menlo,monospace}.thermal-meta{display:flex;justify-content:flex-end;gap:10px;margin-top:9px;color:#45677a;font-size:12px}.thermal-meta strong{color:#0e6e7a}
  22. @media(max-width:640px){.wrap{padding:12px 12px 30px!important}.guanlan-top{padding:12px 14px;border-radius:14px}.guanlan-top b{font-size:13px}.guanlan-top a{font-size:12px;padding:7px 10px}.thermal-band{grid-template-columns:1fr;padding:13px}.thermal-meter{margin-top:14px}.thermal-meta{justify-content:flex-start;flex-wrap:wrap}.ctl{gap:10px!important}.ctl label{min-width:100%!important}}
  23. </style>'''
  24. THERMAL = '''
  25. <script id="guanlan-thermal-strip">
  26. (()=>{const mount=()=>{const controls=document.querySelector('.ctl');if(!controls||document.querySelector('.thermal-band'))return;const band=document.createElement('section');band.className='thermal-band';band.innerHTML='<div><h2>定子温度与保护裕度</h2><p>温度条按 0–155 °C 标尺显示;深色标记为当前定子真值。</p></div><div><div class="thermal-meter" id="thermalMeter" data-temp="-- °C"></div><div class="thermal-scale"><span>0 °C</span><span>80 °C 风机 1 级</span><span>100 °C 风机 2 级</span><span>130.2 °C 告警</span><span>155 °C 保护</span></div><div class="thermal-meta"><span id="thermalRead">定子真值:--</span><span id="thermalMargin">保护余量:--</span></div></div>';controls.insertAdjacentElement('afterend',band);const sync=()=>{const found=document.body.innerText.match(/★定子真值\\s+([0-9.]+)\\s*°C/);const temp=found?Number(found[1]):null;const meter=document.getElementById('thermalMeter'),read=document.getElementById('thermalRead'),margin=document.getElementById('thermalMargin');if(!Number.isFinite(temp)){meter.dataset.temp='计算中';read.textContent='定子真值:计算中';margin.textContent='保护余量:计算中'}else{const pct=Math.max(1,Math.min(99,temp/155*100));meter.style.setProperty('--temp-pct',pct.toFixed(1));meter.dataset.temp=temp.toFixed(1)+' °C';read.textContent='定子真值:'+temp.toFixed(1)+' °C';margin.textContent='保护余量:'+(155-temp).toFixed(1)+' °C'}};setInterval(sync,120);sync()};if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',mount);else mount()})();
  27. </script>'''
  28. # 对外脱敏 —— 规则表唯一正本在 release/scrub_rules.py, 禁止在此另抄一份
  29. from scrub_rules import scrub, residual
  30. def page_for(name):
  31. raw = PAGES[name]
  32. # Current delivered generator page references two removed top-level fields. Map them
  33. # to their structured source fields before its own simulation loop starts.
  34. if name.startswith('3_'):
  35. raw = raw.replace('TH.A_noload', 'TH.stator_model.A').replace('TH.B_copper', 'TH.stator_model.B')
  36. if name.startswith('0_'):
  37. raw = raw.replace('如东 SWT-4.0-130 控制律仪表台', '如东 SWT-4.0-130 · 四系统联调')
  38. raw = raw.replace('<html lang="zh-CN">', '<html lang="zh-CN" data-theme="light">', 1)
  39. raw = raw.replace('</head>', THEME + '</head>', 1)
  40. header = '<div class="guanlan-top"><b>DASHENG · 分系统仿真</b><a href="http://127.0.0.1:18084/大生_样板_门户_单文件.html#sim">← 返回仿真中心</a></div>'
  41. raw = raw.replace('<div class="wrap">', '<div class="wrap">' + header, 1)
  42. raw = raw.replace('$("#th").onclick=()=>{', '$("#th").style.display="none"; $("#th").onclick=()=>{', 1)
  43. if name.startswith('3_'):
  44. raw = raw.replace('</body>', THERMAL + '</body>', 1)
  45. raw = scrub(raw) # 最后一道: 连注入的 header/THEME 一起洗
  46. return raw
  47. class Handler(BaseHTTPRequestHandler):
  48. def do_GET(self):
  49. name = unquote(urlparse(self.path).path).lstrip('/') or '0_四系统合页.html'
  50. if name not in PAGES:
  51. self.send_error(404, 'simulation page not found')
  52. return
  53. body = page_for(name).encode('utf-8')
  54. self.send_response(200)
  55. self.send_header('Content-Type', 'text/html; charset=utf-8')
  56. self.send_header('Content-Length', str(len(body)))
  57. self.end_headers()
  58. self.wfile.write(body)
  59. def log_message(self, fmt, *args):
  60. print('[18792]', fmt % args, flush=True)
  61. import pathlib as _pl, sys as _sys0
  62. _sys0.path.insert(0, str(_pl.Path(__file__).resolve().parents[1]))
  63. from src import logfile as _lf; _lf.prefix_stdout('sim_sys') # 统一日志格式 (用户令 2)
  64. ThreadingHTTPServer(('127.0.0.1', 18792), Handler).serve_forever()