فهرست منبع

2.86.0 汇报定制章节正文移植: 原为空实现(factsHtml/blockHtml 返回空串) => 从 v2.9.2 app.js:531-571 原文移植 rptFacts/rptK/rptBlock 为 lib/reportBlocks.ts(工厂注入依赖, CH.x->chart("x",), D.->d., BD=d.fus.链盘); Report.vue 接线 + App 为 report 取 /api/facts; 验证点月报后 575 元素/10 块/4 图/4180 字, ERRS 0

zhouyang.xie 5 روز پیش
والد
کامیت
f79724026a

+ 1 - 1
VERSION

@@ -1 +1 @@
-2.85.2
+2.86.0

+ 4 - 1
app_common/app_common_guanlan/version.py

@@ -24,7 +24,7 @@ import json
 import pathlib
 
 NAME = '观澜·如东样板 v2'
-VERSION = '2.85.2'         # ★ 版本只改这里
+VERSION = '2.86.0'         # ★ 版本只改这里
 EDITION = 'offline-single-package'
 PACKAGE_STEM = 'app_guanlang'           # 交付包文件名前缀(用户令 2026-09-17)
 
@@ -54,6 +54,9 @@ BUMP_RULE = ('改动落在"解决方案/架构/核心功能" → 大 +1(中/
 #   level: major/minor/patch 表示这一版**相对上一版**是哪一级变化;legacy 表示该版用的是
 #   旧编号体系(0.x,未按本规则),仅作历史对账用。
 HISTORY: tuple[dict, ...] = (
+    dict(version='2.86.0', date='2026-10-06', level='minor',
+         title='汇报定制:章节正文渲染器从 v2.9.2 原文移植(此前为空实现)',
+         note='用户反馈:「汇报定制」页面与原页面不一致。**实逮根因**:Vue 版里两块是**空实现** —— `factsHtml = computed(() => "")` 与 `blockHtml = () => ""` ⇒ 选中任何章节,纸面上**什么都不渲染** (而一致性判据只比默认视图「预览为空」⇒ 两边一致 ⇒ 判据 15/15 也没发现)。**修**:把 v2.9.2 的 `rptFacts` / `rptK` / `rptBlock`(app.js 第 531–571 行)**原文移植**为 `src/lib/reportBlocks.ts`(工厂形式接收注入的 t/fmt/esc/dv/displayText/chart/SYS_KEY/sysName/stateChip/unitNo/STEP/winName/语言包/RPT/FACTS),仅做机械适配:`CH.x(` → `chart("x", `、`D.` → `d.`、`BD` 明确为 `d.fus.链盘`(旧页 app.js:541 的原始定义)。同时在 Report.vue 接线 `blockHtml/factsHtml`,并让 App 为 report 页签补取 `/api/facts`(事实卡契约来源)。**过程实逮(自身缺陷,均已修)**:① 抽取旧码时少取一行 ⇒ 括号不闭合(构建报 Unexpected end of file);② 重复导入 winName(Identifier already declared);③ 语言包导出名是 `ZH` 而非 `zh`。**验证(真应用 jsdom · 点「月报」预设)**:`BEFORE_ELS 1` ⇒ `AFTER_ELS 575 · BLOCKS 10 · CHART_HOSTS 4 · TEXT_LEN 4180`,正文首段为「观澜 · 运行分析报告 / 统计期 2026年 | 38 台 / 9 个章节 / 250,744MWh 上网电量 / 理论可发 287,713 / 1,650h 等效满发小时 / 94.8% 时间可用率 / 12.8% 损失率 / 36,968 MWh / 172h 平均停机间隔 …」,`ERRS 0` ✓。'),
     dict(version='2.85.2', date='2026-10-05', level='patch',
          title='修「执行重算」第 ⑦ 步失败:「kb_ingest.py」 把 「_install_root」 误写成 「_p._install_root」',
          note='用户令(选 A):等"数据重算"跑完后复跑判据。**过程中实逮一个后端代码 bug**:整轮 「rebuild」 耗时 2,701 s、21 步里**只有第 ⑦ 步「本体: 码表/手册/文档」失败** ⇒ 「app_ontology/app_ontology_guanlan/ontology/kb_ingest.py:74」 调用了 **「_p._install_root(__file__)」**,而 「_install_root」 是本模块第 20 行 「from app_common.app_common_guanlan.api import install_root as _install_root」 (或第 23 行的兜底定义)导入的**模块级函数**;「_p」 在此处解析成了别的东西(报错:「AttributeError: module \'pathlib\' has no attribute \'_install_root\'」)⇒ 该步失败并**中断整轮重算**(后面步骤依赖它 ⇒ 「/api/fleet」 持续 500 · 页面呈"无数据")。**修**:全仓检索同写法,仅此 1 处 ⇒ 改为 **「_install_root(__file__)」**;并在远端**单独跑该步验证通过**:「python -m src.ontology.kb_ingest」 ⇒ 产出 「{AlarmCode: 555, WorkInstruction: 559, FailureMode: 150, MaintTask: 559, Doc: 303, Component: 9, causedBy: 60, …}」 ✓。随后重新触发整轮 「rebuild」(各步幂等)以补齐后续步骤,待 「/api/fleet」 恢复 200 后再复跑结构/内容两套判据(15/15 目标)。'),

+ 1 - 1
app_frontEnd/web/src/App.vue

@@ -55,7 +55,7 @@ const err = ref('')
 const TAB_APIS: Record<string, string[]> = {
   system: ['/api/maint_survey'],
   decision: ['/api/ask_models'],
-  report: ['/api/ask_models'],
+  report: ['/api/ask_models', '/api/facts'],
   generation: ['/api/curves'],
 }
 const apis = ref<Record<string, any>>({})

+ 71 - 0
app_frontEnd/web/src/lib/reportBlocks.ts

@@ -0,0 +1,71 @@
+/**
+ * ★P99 汇报定制的**章节正文渲染器** —— 从 v2.9.2 `app.js:531-571` 原文移植(`rptFacts`/`rptK`/`rptBlock`)。
+ * 仅机械适配:`CH.x(` → `chart('x', `;`D.` → `d.`;其余(FACTS/RPT/L/SYS_KEY/sysName/stateChip/unitNo/STEP/
+ * winName/displayText/fmt/dv/esc/chart)由调用方注入,保证与旧页逐字同源。
+ */
+import type { chart as ChartFn } from './legacyCharts'
+
+export interface RptEnv {
+  t: (k: string, o?: Record<string, unknown>) => string
+  fmt: (v: unknown, n?: number) => string
+  esc: (v: unknown) => string
+  dv: (v: unknown) => string
+  displayText: (v: unknown) => string
+  chart: typeof ChartFn
+  SYS_KEY: Record<string, string>
+  sysName: (k: string) => string
+  stateChip: (s: unknown) => string
+  unitNo: (t: string) => string
+  STEP: Record<string, string>
+  winName: (w: string) => string
+  L: Record<string, string>
+  RPT: { narr?: { text?: string; ungrounded?: string[] } | null }
+  FACTS: { cards?: Array<Record<string, unknown>>; contract_sha256?: string } | null | false
+}
+
+export function makeReportBlocks(d: any, env: RptEnv): { rptBlock: (k: string, d: any) => string; rptFacts: () => string } {
+  const { t, fmt, esc, dv, displayText, chart, SYS_KEY, sysName, stateChip, unitNo, STEP, winName, L, RPT, FACTS } = env
+  const BD = (d && d.fus && d.fus.链盘) || {}   // 旧页 app.js:541 的 BD = d.fus.链盘
+    function rptFacts() { if (!FACTS || !FACTS.cards) return ''; const cards = FACTS.cards.filter(c => c.verdict === '定论' || c.verdict === '准定论·预警'); const rows = cards.map(c => `<tr><td><code>${esc(c.claim_id)}</code></td><td>${esc(c.verdict)}</td><td>${esc(c.title)}</td><td>${esc((c.systems || []).join(', ') || '—')}</td><td>${esc(c.aggregation || '—')}</td></tr>`).join('');
+      return `<div class="rpt-blk" data-contract="${esc(FACTS.contract_sha256 || '')}"><div class="blk-ti">${t('rpt.facts.title')} <span class="small muted">${t('rpt.facts.sub', { n: cards.length, sha: (FACTS.contract_sha256 || '').slice(0, 16) })}</span></div><div style="overflow:auto"><table class="tbl"><tr><th>claim</th><th>${t('rpt.facts.verdict')}</th><th>${t('rpt.facts.text')}</th><th>${t('rpt.facts.sys')}</th><th>${t('rpt.facts.agg')}</th></tr>${rows}</table></div></div>`; }
+    // 判别值 (API 按 _KEEP_ZH 原样保留中文, 前端拿来做判别) —— 显示时查语言包。
+    // 查不到才回落原文标注: 振动线的技术速记 (「2R倒谱自基线红警」) 不可枚举也不该猜译。
+    const enumWord = (p) => { const k = String(p || ''); for (const pre of ['attr.', 'vp.']) if (L[pre + k] != null) return t(pre + k); return null; };
+    const partName = (p) => SYS_KEY[p] ? sysName(p) : (enumWord(p) ? esc(enumWord(p)) : dv(p));
+    const partPlain = (p) => SYS_KEY[p] ? sysName(p) : (enumWord(p) || displayText(p));
+  
+    function rptK(v, u, lab, note, cls) { return `<div class="kpi ${cls || ''}"><b>${v}<small>${u}</small></b><span>${lab}<br><span style="opacity:.8">${note || ''}</span></span></div>`; }
+    function rptBlock(k, d) {
+      const E = (d.control && d.control.energy) || {}, em = E.monthly || [], R = (d.rel && !d.rel.err) ? d.rel : null, M = (d.m8 && !d.m8.err) ? d.m8.mtbf : null, F = d.faults || {}, FU = d.fus || {}, BD = FU.链盘 || {};
+      const wrap = (ti, b, src) => `<div class="panel mb"><h3>${ti}</h3>${b}${src ? `<p class="small muted" style="margin-top:8px;padding-top:7px;border-top:1px solid var(--line)">${t('rpt.caliber_lbl')}${src}</p>` : ''}</div>`;
+      if (k === 'kpi') { const last = em[em.length - 1] || {}, prev = em[em.length - 2] || {}, ok = (last.cov || 0) >= .5 && (prev.cov || 0) >= .5, dl = (a, b) => b ? ((a - b) / b * 100) : 0, mom = dl(last.act, prev.act);
+        return `<div class="kpis" style="margin-bottom:14px">${rptK(fmt(E.act), 'MWh', t('rpt.kpi.act'), t('rpt.kpi.theo', { v: fmt(E.theo) }) + ' | ' + (ok ? t('rpt.kpi.mom', { v: (mom >= 0 ? '+' : '') + mom.toFixed(0) }) : t('rpt.kpi.mom_na')))}
+          ${rptK(fmt(E.eflh), 'h', t('rpt.kpi.eflh'), t('rpt.kpi.annual', { v: fmt(Math.round((E.eflh || 0) / Math.max(em.filter(x => x.cov >= .5).length, 1) * 12)) }))}
+          ${rptK(fmt(d.control && d.control.avail, 1), '%', t('rpt.kpi.avail'), t('rpt.kpi.avail_n'), (d.control && d.control.avail >= 95) ? 'ok' : 'warn')}
+          ${rptK(fmt(E.loss_pct, 1), '%', t('rpt.kpi.loss'), fmt(E.loss) + ' MWh', E.loss_pct >= 15 ? 'warn' : '')}
+          ${rptK(M ? fmt(M.MTBF_h) : '—', 'h', t('rpt.kpi.mtbo'), t('rpt.kpi.mtbo_n', { n: M ? fmt(M.停机事件数) : '—', h: M ? M.MDT_h : '—' }))}
+          ${rptK((BD.rows || []).length, t('unit.units'), t('rpt.kpi.action'), t('rpt.kpi.action_n', { a: (FU.kpi || {}).定论 || 0, b: (FU.kpi || {}).预警 || 0, c: (FU.kpi || {}).候选观察 || 0 }), (BD.rows || []).length > 20 ? 'warn' : '')}</div>`; }
+      if (k === 'water') return wrap(t('rpt.water.title'), chart('waterfall', E) + `<p class="small muted" style="margin-top:6px">${t('rpt.water.note', { theo: fmt(E.theo), act: fmt(E.act), loss: fmt(E.loss), pct: E.loss_pct })} | ${E.cap_ok ? t('rpt.water.cap_ok', { cap: fmt(E.cap) }) : t('rpt.water.cap_bad', { cap: fmt(E.cap) })}</p>`, t('rpt.water.cal'));
+      if (k === 'emon') return wrap(t('rpt.emon.title'), em.length ? `<div class="tblwrap"><table><tr><th>${t('rpt.emon.m')}</th><th class="r">${t('rpt.emon.act')}</th><th class="r">${t('rpt.emon.loss')}</th><th class="r">${t('rpt.emon.pct')}</th><th class="r">${t('rpt.emon.eflh')}</th><th>${t('rpt.emon.cov')}</th></tr>${em.map(x => `<tr><td>${x.m}</td><td class="r">${fmt(x.act)}</td><td class="r">${fmt(x.loss)}</td><td class="r">${x.loss_pct}%</td><td class="r">${x.eflh}</td><td class="small" style="color:${x.cov < .5 ? 'var(--warning)' : 'var(--muted)'}">${(x.cov * 100).toFixed(0)}%${x.cov < .5 ? t('rpt.emon.partial') : ''}</td></tr>`).join('')}</table></div>` : '—', t('rpt.emon.cal'));
+      if (k === 'blame') return wrap(t('rpt.blame.title'), chart('blameBar', E) + `<p class="small muted" style="margin-top:8px">${t('rpt.blame.note')}<br><b>${t('rpt.blame.note2')}</b></p>`, t('rpt.blame.cal'));
+      if (k === 'ram') return R ? wrap(t('rpt.ram.title'), chart('ramChart', R.表, partPlain) + `<p class="small muted" style="margin-top:6px">${t('rpt.ram.note', { a: R.设备类停机h, b: R.外部类停机h })}</p>`, t('rpt.ram.cal')) : '';
+      if (k === 'parts') return R ? wrap(t('rpt.parts.title'), `<div class="tblwrap"><table><tr><th>${t('rpt.parts.part')}</th><th class="r">${t('rpt.parts.h')}</th><th class="r">${t('rpt.parts.ev')}</th><th class="r">${t('rpt.parts.mtbf')}</th><th class="r">${t('rpt.parts.mdt')}</th><th class="r">${t('rpt.parts.units')}</th></tr>${R.表.slice(0, 9).map(x => `<tr><td class="small">${partName(x.部件)}${x.外部 ? t('rpt.parts.ext') : ''}</td><td class="r">${x.停机时长h}</td><td class="r">${x.事件数}</td><td class="r">${x.MTBF_h || '—'}</td><td class="r">${x.MDT_h}</td><td class="r">${x.影响台数}</td></tr>`).join('')}</table></div>`, t('rpt.parts.cal')) : '';
+      if (k === 'attrib') return R ? wrap(t('rpt.attrib.title'), chart('attribBar', R.表, R.总停机h) + `<p class="small muted" style="margin-top:6px">${t('rpt.attrib.note')}</p>`, '') : '';
+      if (k === 'pareto') return wrap(t('rpt.pareto.title'), chart('paretoChart', F.pareto_n || [], t('unit.n'), ''), t('rpt.pareto.cal'));
+      if (k === 'quad') return wrap(t('rpt.quad.title'), chart('quadChart', F.quad || []) + `<p class="small muted" style="margin-top:6px">${t('rpt.quad.note')}</p>`, t('rpt.quad.cal'));
+      if (k === 'frate') return wrap(t('rpt.frate.title'), chart('rateChart', F.monthly || { months: [], vals: [] }) + `<p class="small muted" style="margin-top:6px">${t('rpt.frate.note')}</p>`, t('rpt.frate.cal'));
+      if (k === 'units') { const rows = (BD.rows || []).slice(0, 14), stuck = Object.entries(BD.stuck || {}).map(([a, b]) => t('rpt.units.stuck', { n: b, s: t('step.' + (STEP[a] || 'evidence')) })).join(' · ');
+        return wrap(t('rpt.units.title'), `<p class="small muted" style="margin-bottom:8px">${t('rpt.units.head', { n: (BD.rows || []).length, tot: (d.kpi || {}).全场 || 38 })}${stuck}</p><div class="tblwrap"><table><tr><th style="width:52px">${t('ui.unit')}</th><th style="width:80px">${t('rpt.units.state')}</th><th>${t('rpt.units.ev')}</th><th style="width:56px">${t('rpt.units.prog')}</th><th style="width:90px">${t('rpt.units.at')}</th></tr>
+          ${rows.map(r => `<tr><td><b>${unitNo(r.t)}</b></td><td>${stateChip(r.ostate)}</td><td class="small muted">${r.part && r.part.indexOf('未收录') < 0 ? `<b style="color:var(--ink)">${dv(r.part)}</b> · ` : ''}${(r.srcs || []).map(dv).join('/') || '—'}${r.rising ? ` · <span style="color:var(--critical)">${t('rpt.units.rising', { r: r.rising.ratio })}</span>` : ''}</td><td class="r small">${r.done}/6</td><td class="small" style="color:var(--warning)">${r.stuck ? t('step.' + (STEP[r.stuck] || 'evidence')) : t('rpt.units.clear')}</td></tr>`).join('')}</table></div>`, t('rpt.units.cal')); }
+      if (k === 'chain') return wrap(t('rpt.chain.title'), `<p class="small muted">${t('rpt.chain.note', { s: Object.entries(BD.stuck || {}).map(([a, b]) => t('rpt.units.stuck', { n: b, s: t('step.' + (STEP[a] || 'evidence')) })).join(', ') })}</p>`, t('rpt.chain.cal', { b: dv(BD.wo_bound || '') }));
+      if (k === 'loop') { const cl = BD.closed || [];
+        return wrap(t('rpt.loop.title'), `<div class="tblwrap"><table><tr><th>${t('ui.unit')}</th><th>${t('rpt.loop.comp')}</th><th>${t('rpt.loop.date')}</th><th class="r">${t('rpt.loop.pre')}</th><th class="r">${t('rpt.loop.post')}</th><th class="r">${t('rpt.loop.ratio')}</th></tr>${cl.map(x => `<tr><td><b>${unitNo(x.turbine)}</b></td><td class="small">${dv(x.component)}</td><td class="small">${esc(x.replace_date)}</td><td class="r">${x.pre}</td><td class="r">${x.post}</td><td class="r" style="color:var(--good)"><b>${x.ratio}×</b></td></tr>`).join('')}</table></div><p class="small muted" style="margin-top:6px">${t('rpt.loop.note')}</p>`, t('rpt.loop.cal')); }
+      if (k === 'plan') return wrap(t('rpt.plan.title'), `<ol style="margin:0 0 0 20px;line-height:1.85"><li>${t('rpt.plan.1', { n: (BD.stuck || {}).机制 || 0 })}</li><li>${t('rpt.plan.2')}</li><li>${t('rpt.plan.3')}</li><li>${t('rpt.plan.4')}</li></ol>`, t('rpt.plan.cal'));
+      if (k === 'narr') { const N = RPT.narr; if (!N || !N.text) return wrap(t('rpt.narr.title'), `<p class="small muted">${t('rpt.narr.empty')}</p>`, '');
+        return wrap(t('rpt.narr.title'), `<p style="line-height:1.9;margin:0">${esc(N.text).split('\n').join('<br>')}</p>${(N.ungrounded || []).length ? `<p class="small" style="color:var(--warning);margin-top:8px">${t('rpt.narr.ungr', { v: N.ungrounded.map(esc).join(' / ') })}</p>` : ''}`, t('rpt.narr.cal', { m: esc(N.model), g: (N.ungrounded || []).length ? t('rpt.narr.g_n', { n: N.ungrounded.length }) : t('rpt.narr.g_ok') })); }
+      if (k === 'caliber') { const part = F.partial || [];
+        return wrap(t('rpt.cal.title'), `<ul style="margin:0 0 0 20px;line-height:1.85"><li><b>${t('rpt.cal.period')}</b> ${winName(d.win)}${part.length ? t('rpt.cal.partial', { m: part.join('/'), c: Math.round((F.cov_min || 0) * 100) }) : ''}</li><li><b>${t('rpt.cal.denom')}</b> ${t('rpt.cal.denom_d')}</li><li><b>${t('rpt.cal.stop')}</b> ${t('rpt.cal.stop_d')}</li><li><b>${t('rpt.cal.wo')}</b> ${t('rpt.cal.wo_d')}</li><li><b>${t('rpt.cal.rating')}</b> ${t('rpt.cal.rating_d')}</li>${RPT.aud === 'up' ? `<li><b>${t('rpt.cal.deid')}</b> ${t('rpt.cal.deid_d')}</li>` : ''}</ul>`, ''); }
+      return '';
+    }
+  return { rptBlock, rptFacts }
+}

+ 14 - 3
app_frontEnd/web/src/views/Report.vue

@@ -10,7 +10,12 @@
 import { computed, onMounted, reactive, ref } from 'vue'
 import { t } from '../i18n/zh'
 import { winName } from '../lib/render'
-import { esc } from '../lib/vib'
+import { dv, esc, displayText, stateChip } from '../lib/vib'
+import { fmt, STEP, unitNo } from '../lib/render'
+import { SYS_KEY, sysName } from '../lib/render2'
+import { chart } from '../lib/legacyCharts'
+import { makeReportBlocks } from '../lib/reportBlocks'
+import { ZH as L } from '../i18n/zh'
 
 const props = defineProps<{ d: any; apis?: Record<string, any> | null }>()
 
@@ -67,10 +72,16 @@ const libraryHtml = computed(() => RPT_GROUPS.map((g) =>
   + `</div>`).join(''))
 
 /** 旧 `rptFacts()`:事实附注(本版先空实现,块正文与附注由下一轮按对拍补齐) */
-const factsHtml = computed(() => '')
+/** ★P99 事实卡(/api/facts)—— 与旧页同一契约来源 */
+const facts = computed<any>(() => (props.apis && props.apis['/api/facts']) || null)
+const _blocks = computed(() => makeReportBlocks(props.d, {
+  t, fmt, esc, dv, displayText, chart, SYS_KEY, sysName, stateChip, unitNo, STEP, winName, L, RPT,
+  FACTS: facts.value,
+}))
+const factsHtml = computed(() => _blocks.value.rptFacts())
 
 /** 旧 `rptBlock(k, d)`:块正文(同上,先空实现) */
-const blockHtml = (_k: string): string => ''
+const blockHtml = (k: string): string => _blocks.value.rptBlock(k, props.d)
 
 /** 与 decision 同法(旧 bindReport):清单到手后 model 取清单第一个键(已有且仍在清单里则保留) */
 const effectiveModel = computed(() => {

+ 43 - 0
app_frontEnd/web/tools/_check_report_blocks.mjs

@@ -0,0 +1,43 @@
+// P99 核验:点"月报"预设后,纸面是否渲染出章节正文(此前为真空实现)
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { dirname, resolve } from 'node:path'
+import { JSDOM, VirtualConsole } from 'jsdom'
+const HERE = dirname(fileURLToPath(import.meta.url))
+const dist = resolve(HERE, '../../../release/web')
+const GW = 'http://106.120.102.238:28084'
+const fleet = readFileSync(resolve(HERE, '_fleet_fixture.json'), 'utf8')
+const facts = '{"cards":[{"c":"WTG01","verdict":"定论","part":"主轴承"}],"contract_sha256":"abc"}'
+const idx = readFileSync(dist + '/index.html', 'utf8')
+const jsFile = (idx.match(/src="([^"]+\.js)"/) || [])[1].replace(/^.*\//, '')
+const cssFile = (idx.match(/href="([^"]+\.css)"/) || [])[1].replace(/^.*\//, '')
+const code = readFileSync(dist + '/assets/' + jsFile, 'utf8')
+const css = readFileSync(dist + '/assets/' + cssFile, 'utf8')
+const errs = []
+const vc = new VirtualConsole()
+vc.on('jsdomError', (e) => errs.push(String(e && e.message).slice(0, 120)))
+vc.on('error', (...a) => errs.push(a.map(String).join(' ').slice(0, 120)))
+const dom = new JSDOM(idx.replace('</head>', '<style>' + css + '</style></head>'),
+  { url: GW + '/detail/v2#tab=report', runScripts: 'outside-only', pretendToBeVisual: true, virtualConsole: vc })
+const w = dom.window
+w.fetch = async (u) => {
+  const s = String(u)
+  const body = /fleet/.test(s) ? fleet : (/facts/.test(s) ? facts : '{}')
+  return { ok: true, status: 200, json: async () => JSON.parse(body), text: async () => body }
+}
+try { w.eval(code) } catch (e) { errs.push('EVAL ' + String(e).slice(0, 120)) }
+for (let i = 0; i < 24; i++) { await new Promise((r) => setTimeout(r, 400)); if (w.document.querySelector('[data-preset="month"]')) break }
+const before = w.document.querySelector('.paper') ? w.document.querySelector('.paper').querySelectorAll('*').length : 0
+const pre = w.document.querySelector('[data-preset="month"]')
+if (pre) pre.dispatchEvent(new w.MouseEvent('click', { bubbles: true }))
+await new Promise((r) => setTimeout(r, 2500))
+const d = w.document
+const paper = d.querySelector('.paper')
+const blocks = paper ? paper.querySelectorAll('.panel, .kpis, .rpt-blk').length : 0
+const txt = paper ? (paper.textContent || '').replace(/\s+/g, ' ').trim() : ''
+const charts = paper ? paper.querySelectorAll('svg, .legacy-chart').length : 0
+console.log('BEFORE_ELS ' + before)
+console.log('AFTER_ELS ' + (paper ? paper.querySelectorAll('*').length : 0) + ' BLOCKS ' + blocks + ' CHART_HOSTS ' + charts)
+console.log('TEXT_LEN ' + txt.length)
+console.log('HEAD ' + JSON.stringify(txt.slice(0, 240)))
+console.log('ERRS ' + errs.length + (errs.length ? ' :: ' + errs[0] : ''))

+ 34 - 0
app_frontEnd/web/tools/_rebuild_full.mjs

@@ -0,0 +1,34 @@
+// P98:用**全量**参数重新触发重算(skip_scada:false)—— 产物被全清后必须补 SCADA 步骤,否则第 14 步读不到 temp_bins.parquet
+const GW = 'http://106.120.102.238:28084'
+const t0 = Date.now()
+const st = () => ((Date.now() - t0) / 1000).toFixed(0).padStart(5) + 's'
+async function get(path) {
+  try {
+    const r = await fetch(GW + path + (path.includes('?') ? '&' : '?') + 'ts=' + Date.now() + Math.random(), { headers: { 'Cache-Control': 'no-cache' } })
+    const b = await r.text()
+    return { status: r.status, len: b.length, txt: b }
+  } catch (e) { return { status: 0, len: 0, txt: String(e) } }
+}
+async function post(path, body) {
+  const r = await fetch(GW + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })
+  return { status: r.status, txt: (await r.text()).slice(0, 240) }
+}
+console.log(st() + ' fleet(前)=' + (await get('/api/fleet')).status)
+console.log(st() + ' 全量 rebuild: ' + JSON.stringify(await post('/ops/api/rebuild', { skip_scada: false, with_verify: false })))
+let ok = false
+for (let i = 0; i < 160; i++) {
+  await new Promise((r) => setTimeout(r, 45000))
+  const f = await get('/api/fleet')
+  const s = await get('/ops/api/state')
+  let job = {}
+  try { job = JSON.parse(s.txt).job || {} } catch { /* ignore */ }
+  const tail = String((job.log_tail || []).slice(-1)[0] || '').trim().slice(0, 96)
+  console.log(st() + ' fleet=' + f.status + '(' + f.len + 'B) job=' + (job.status || '?') + ' rc=' + (job.rc ?? '-') + ' · ' + tail)
+  if (f.status === 200 && f.len > 50000) { ok = true; console.log('✅ fleet 已恢复'); break }
+  if (job.rc !== null && job.rc !== undefined) {
+    console.log('⚠️ 结束 rc=' + job.rc)
+    for (const l of (job.log_tail || []).slice(-10)) console.log('   ' + String(l).slice(0, 190))
+    break
+  }
+}
+console.log('END ' + st() + ' ' + (ok ? 'OK' : 'NOT-OK'))

+ 1 - 1
docs/src/数据要求说明_观澜_2.85.2.md → docs/src/数据要求说明_观澜_2.86.0.md

@@ -1,4 +1,4 @@
-# 数据要求说明 · 观澜 v2 风电场智能分析系统 · 版本 2.85.2
+# 数据要求说明 · 观澜 v2 风电场智能分析系统 · 版本 2.86.0
 
 ## 1 文档说明
 

+ 10 - 10
docs/src/系统设计说明_观澜_2.85.2.md → docs/src/系统设计说明_观澜_2.86.0.md

@@ -1,4 +1,4 @@
-# 系统设计说明 · 观澜 v2 风电场智能分析系统 · 版本 2.85.2
+# 系统设计说明 · 观澜 v2 风电场智能分析系统 · 版本 2.86.0
 
 ## 1 文档说明
 
@@ -6,19 +6,19 @@
 
 ### 1.1 目的与范围
 
-本文是"观澜 v2(风电场智能分析系统)"(海上风电场智能分析离线系统)的设计说明(下称"本系统"),面向版本 2.85.2;内容以样本风电场(下称"本场")为依据,样本场实测日期为 2026-09-22,说明本系统"由哪些部分组成、各部分怎么实现、数据从哪来到哪去、判据写在哪里、怎么验证、边界在哪里"。
+本文是"观澜 v2(风电场智能分析系统)"(海上风电场智能分析离线系统)的设计说明(下称"本系统"),面向版本 2.86.0;内容以样本风电场(下称"本场")为依据,样本场实测日期为 2026-09-22,说明本系统"由哪些部分组成、各部分怎么实现、数据从哪来到哪去、判据写在哪里、怎么验证、边界在哪里"。
 
 本文覆盖十五个设计面:总体架构与分层、目录结构与路径真源、数据接入与重算链、判级与算法、时间窗口径、服务与前端、本体与知识层、本机模型接入、运维控制台与重算编排、安装与服务化与版本管理、质量保证、安全与离线边界、可移植性与资源占用、多场适用性与换场迁移、已知边界与未实现。
 
-本文不重复需求条目本身(那是《需求分析_观澜_2.85.2.docx》的职责),也不重复操作步骤的逐步手册(那是 docs/重算操作手册_v0.1.md 与《使用说明书》v0.2(随包 docs/)的职责);本文只回答"设计上为什么这样、落在哪个文件的哪一处、用什么机器守卫保证它不漂移"。
+本文不重复需求条目本身(那是《需求分析_观澜_2.86.0.docx》的职责),也不重复操作步骤的逐步手册(那是 docs/重算操作手册_v0.1.md 与《使用说明书》v0.2(随包 docs/)的职责);本文只回答"设计上为什么这样、落在哪个文件的哪一处、用什么机器守卫保证它不漂移"。
 
 ### 1.2 读者与用法
 
 现场运维与检修人员可看第 3 章、第 7 章、第 8 章、第 11 章,了解自己能点到的页面背后读的是什么口径;场站管理人员与换场交付人员可看第 6 章、第 7 章、第 16 章、第 17 章,了解判级与可靠性指标的口径与边界,以及换场时要重新标定哪些参数;研发与交付人员应通读全文,重点是第 4 章、第 5 章、第 12 章、第 13 章;验收方可直接按第 13 章的质量门与附录 A 的编写依据逐条复核。
 
-### 1.3 与《需求分析_观澜_2.85.2.docx》的对应关系
+### 1.3 与《需求分析_观澜_2.86.0.docx》的对应关系
 
-需求分析写"要什么、为谁、优先级与验收门",设计说明写"怎么实现、落在哪、如何自证"。两文的章节对应关系如表 1-1 所示;与《数据要求说明_观澜_2.85.2.docx》的数据侧口径去向见 16.6 节。
+需求分析写"要什么、为谁、优先级与验收门",设计说明写"怎么实现、落在哪、如何自证"。两文的章节对应关系如表 1-1 所示;与《数据要求说明_观澜_2.86.0.docx》的数据侧口径去向见 16.6 节。
 
 | 本文章节 | 需求分析对应章 | 对应关系说明 |
 |---|---|---|
@@ -41,7 +41,7 @@
 
 ### 1.4 口径与依据
 
-本文所有数字来自仓库文件或命令的实跑输出,不采用估算与推测;查不到、未实现的,一律写"未取证"或"未实现",并在第 17 章汇总。本文中"本次实测"与各表"实测"列一律指样本场实测(2026-09-22):即在样本风电场的一套实例上的一次实跑,换场后这些数字会变、方法与口径不变。版本号的唯一真源是 src/version.py 的 VERSION 常量,本版为 2.85.2;打包文件名由 src/version.py 的 package_name() 给出,为 app_guanlang_v2.85.2.zip。
+本文所有数字来自仓库文件或命令的实跑输出,不采用估算与推测;查不到、未实现的,一律写"未取证"或"未实现",并在第 17 章汇总。本文中"本次实测"与各表"实测"列一律指样本场实测(2026-09-22):即在样本风电场的一套实例上的一次实跑,换场后这些数字会变、方法与口径不变。版本号的唯一真源是 src/version.py 的 VERSION 常量,本版为 2.86.0;打包文件名由 src/version.py 的 package_name() 给出,为 app_guanlang_v2.86.0.zip。
 
 本文遵循四条写作口径:含"窗"且确实指时间窗口的,一律写全"时间窗"(天气窗、作业窗、预览窗、观测窗属领域词,保持原样);影响的是风电机组时写"影响机组"或"影响机组数";使用英文简写时必须写成"中文(英文简写)"形式,如平均无故障间隔(MTBF)、平均停机间隔(MTBO)、单次停机时长(MDT);全文用简体中文与半角数字与单位。
 
@@ -863,7 +863,7 @@ Windows 安装入口是 install.bat 与 install.ps1,步骤号写死在输出
 
 ### 12.3 安装记录与版本三守卫
 
-安装记录 install-info.json 是"这台机器装的是哪一版"的唯一凭据,字段如表 12-3 所示。装完写、卸载时删(删掉等于这台机器回到"没装过")。需要如实指出:本机该文件记录的版本是 2.5.0(装于 2026-09-17),落后于当前代码版本 2.85.2,因此它正是"安装前检查会提示版本差异"的活样本;重装或升级后会随之更新。
+安装记录 install-info.json 是"这台机器装的是哪一版"的唯一凭据,字段如表 12-3 所示。装完写、卸载时删(删掉等于这台机器回到"没装过")。需要如实指出:本机该文件记录的版本是 2.5.0(装于 2026-09-17),落后于当前代码版本 2.86.0,因此它正是"安装前检查会提示版本差异"的活样本;重装或升级后会随之更新。
 
 | 字段 | 含义 |
 |---|---|
@@ -1030,7 +1030,7 @@ Windows 安装入口是 install.bat 与 install.ps1,步骤号写死在输出
 
 ### 15.2 Python 与依赖 wheel
 
-解释器要求是 3.11 及以上(依赖 pandas 3,3.10 及以下没有轮子);机器上没有 3.11 时使用包内便携运行时(3.12.14,三个平台各一份)。依赖清单共 157 条固定版本:polars 1.42.0、pandas 3.0.3、pyarrow 24.0.0、numpy 2.5.0、pyyaml 6.0.3、python-frontmatter 1.3.0、matplotlib 3.11.0、plotly 6.8.0、jinja2 3.1.6、python-docx 1.2.0、docxtpl 0.20.2、python-dotenv 1.2.2、requests 2.34.2、rich 15.0.0、tqdm 4.68.3、openpyxl 3.1.5、scipy 1.18.0、xlrd 2.0.2,另有一条不锁版本的 tzdata(Windows 无系统时区库,而 pandas 3 硬依赖它)。离线件如表 15-2 所示(件数与体积为本次实测)。
+解释器要求是 3.11 及以上(依赖 pandas 3,3.10 及以下没有轮子);机器上没有 3.11 时使用包内便携运行时(3.12.14,三个平台各一份)。依赖清单共 158 条固定版本:polars 1.42.0、pandas 3.0.3、pyarrow 24.0.0、numpy 2.5.0、pyyaml 6.0.3、python-frontmatter 1.3.0、matplotlib 3.11.0、plotly 6.8.0、jinja2 3.1.6、python-docx 1.2.0、docxtpl 0.20.2、python-dotenv 1.2.2、requests 2.34.2、rich 15.0.0、tqdm 4.68.3、openpyxl 3.1.5、scipy 1.18.0、xlrd 2.0.2,另有一条不锁版本的 tzdata(Windows 无系统时区库,而 pandas 3 硬依赖它)。离线件如表 15-2 所示(件数与体积为本次实测)。
 
 | 离线件目录 | 件数(实测) | 体积(实测) | 覆盖平台 |
 |---|---|---|---|
@@ -1224,7 +1224,7 @@ Linux 侧的适配程度如表 15-4 所示,全部为"已有实现但在本次
 
 ### 16.6 与同批交付文档的对应关系
 
-本章对应《需求分析_观澜_2.85.2.docx》第 11 章(场配置化、机组与机型可替换、数据源形态可适配、阈值按场标定、术语与单位可配、跨场统一口径、按场裁剪收资、换场验收检查表)与《数据要求说明_观澜_2.85.2.docx》第 12 章(通用必选与可选的判定规则、场配置字段对照、数据源形态适配、换场收资差异清单、按场裁剪步骤);本章给的是设计侧的落点、换场作业单与检查表,数据侧的收资口径与字段要求以数据要求说明为准,需求侧的验收项以需求分析为准。
+本章对应《需求分析_观澜_2.86.0.docx》第 11 章(场配置化、机组与机型可替换、数据源形态可适配、阈值按场标定、术语与单位可配、跨场统一口径、按场裁剪收资、换场验收检查表)与《数据要求说明_观澜_2.86.0.docx》第 12 章(通用必选与可选的判定规则、场配置字段对照、数据源形态适配、换场收资差异清单、按场裁剪步骤);本章给的是设计侧的落点、换场作业单与检查表,数据侧的收资口径与字段要求以数据要求说明为准,需求侧的验收项以需求分析为准。
 
 ***
 
@@ -1290,7 +1290,7 @@ Linux 侧的适配程度如表 15-4 所示,全部为"已有实现但在本次
 
 | 章节 | 来源文件 |
 |---|---|
-| 1 文档说明 | src/version.py(VERSION 与 HISTORY 的 2.59.0 条目)、docs/需求分析_观澜_2.85.2.docx、scripts/delivery_docs_figures.py |
+| 1 文档说明 | src/version.py(VERSION 与 HISTORY 的 2.59.0 条目)、docs/需求分析_观澜_2.86.0.docx、scripts/delivery_docs_figures.py |
 | 2 设计目标与原则 | docs/系统设计说明.md(四条设计铁律)、app_ontology/.../ontology/mcp_server.py 头部、src/windscada/subsys/fusion.py 头部、src/windscada/perf/reliability.py |
 | 3 总体架构 | configs/serve.json、scripts/guanlan_gateway.py、guanlan.py(组件启动表)、run/pids.json、docs/系统设计说明.md(产物全景) |
 | 4 目录结构与路径真源 | src/paths.py、configs/registry.yaml、docs/系统设计说明.md(路径约定与统一记录)、本次目录实测 |

+ 25 - 24
docs/src/需求分析_观澜_2.85.2.md → docs/src/需求分析_观澜_2.86.0.md

@@ -1,4 +1,4 @@
-# 需求分析 · 观澜 v2 风电场智能分析系统 · 版本 2.85.2
+# 需求分析 · 观澜 v2 风电场智能分析系统 · 版本 2.86.0
 
 ## 1 文档说明
 
@@ -6,7 +6,7 @@
 
 ### 1.1 目的
 
-本文是「观澜 v2(风电场智能分析系统)」的**需求分析文档**,回答四个问题:这套系统究竟要满足谁的什么需要;这些需要从哪来、经过哪些版本变成现在的样子;每一条需要对应什么功能、什么输入、什么输出、拿什么判据验收;以及哪些事本版明确不做。本文与同批交付的《系统设计说明_观澜_2.85.2.docx》《数据要求说明_观澜_2.85.2.docx》配套:本文讲「要什么、凭什么算做到了」,设计说明讲「怎么做的」,数据要求说明讲「要哪些数据、什么形态、什么单位」。
+本文是「观澜 v2(风电场智能分析系统)」的**需求分析文档**,回答四个问题:这套系统究竟要满足谁的什么需要;这些需要从哪来、经过哪些版本变成现在的样子;每一条需要对应什么功能、什么输入、什么输出、拿什么判据验收;以及哪些事本版明确不做。本文与同批交付的《系统设计说明_观澜_2.86.0.docx》《数据要求说明_观澜_2.86.0.docx》配套:本文讲「要什么、凭什么算做到了」,设计说明讲「怎么做的」,数据要求说明讲「要哪些数据、什么形态、什么单位」。
 
 本文的写作口径是**只写能取证的事实**:每个数字、每条结论都能指到仓库里的某个文件、某次实跑输出或某条版本记录;查不到、取不到的一律写明「未取证」或「未到位」,不做推测性补全。因此文中会出现少量「未取证」的说明句,那是刻意留下的诚实边界,不是遗漏。
 
@@ -18,15 +18,15 @@
 
 ### 1.3 版本对应关系
 
-系统版本号只有一个真源:仓库内的 src/version.py 的 VERSION 一行。本版 VERSION = 2.85.2,版本记录(HISTORY)共 155 条,人读的版本表由 scripts/version_log.py 从 HISTORY 生成到 docs/版本记录.md,并由 version_log.py --check 与 guanlan.py check 双重校验「记录表与代码一致」。
+系统版本号只有一个真源:仓库内的 src/version.py 的 VERSION 一行。本版 VERSION = 2.86.0,版本记录(HISTORY)共 156 条,人读的版本表由 scripts/version_log.py 从 HISTORY 生成到 docs/版本记录.md,并由 version_log.py --check 与 guanlan.py check 双重校验「记录表与代码一致」。
 
 表 1-1 版本与文档的对应关系
 
 | 项 | 值 | 取证方式 |
 |---|---|---|
-| 系统版本 | 2.85.2 | src/version.py 的 VERSION;guanlan.py check 实跑报「版本管理: 观澜 v2(风电场智能分析系统) v2.85.2」 |
-| 交付包名 | app_guanlang_v2.85.2.zip | src/version.py 的 package_name();同一条 check 输出 |
-| 本文版本 | 2.85.2(与系统版本同号) | 本文标题与 src/version.py 的 VERSION |
+| 系统版本 | 2.86.0 | src/version.py 的 VERSION;guanlan.py check 实跑报「版本管理: 观澜 v2(风电场智能分析系统) v2.86.0」 |
+| 交付包名 | app_guanlang_v2.86.0.zip | src/version.py 的 package_name();同一条 check 输出 |
+| 本文版本 | 2.86.0(与系统版本同号) | 本文标题与 src/version.py 的 VERSION |
 | 版本史条目数 | 13 条(最新一条即本次文档交付) | src/version.py 的 HISTORY;docs/版本记录.md |
 | 版本号规则 | v大版本号.中版本号.小版本号;大改方案或架构、中改非核心功能、小为消缺 | src/version.py 的 BUMP_RULE 与 LEVEL_MEANING;docs/系统设计说明.md §15 |
 | 版本级别机器判据 | level_of(旧, 新) 返回 major 或 minor 或 patch 或 same | src/version.py 的 level_of() |
@@ -46,7 +46,7 @@
 | 只读审计 | scripts/pages_audit.py --check(2026-09-22) | 检查 21 项,不一致 0,已知缺口 19,退出码 0 |
 | 只读计划 | scripts/rebuild_all.py --dry-run(2026-09-22) | 默认 26 步;加 --skip-scada 为 23 步 |
 | 实测计数 | data/raw/<场站> 与 outputs/<场> 递归计数(2026-09-22) | 输入 26,504 件 / 224.67 GB;产物 3,549 件 / 7,157.5 MB |
-| 读源 | src/version.py、configs/portal_pages.yaml、configs/serve.json、guanlan.py、scripts/rebuild_all.py | 13 条版本史;25 条顶层页面条目(含 7 条子页共 157 条);6 个服务端口 |
+| 读源 | src/version.py、configs/portal_pages.yaml、configs/serve.json、guanlan.py、scripts/rebuild_all.py | 13 条版本史;25 条顶层页面条目(含 7 条子页共 158 条);6 个服务端口 |
 | 读文档 | docs/系统设计说明.md(56 个标题)、docs/说明书_观澜…v0.2.md、docs/输入数据放置指导_v0.1.md、docs/数据目录结构与落位约定_v0.2.md、docs/重算操作手册_v0.1.md、docs/页面输出体检_v0.1.md、README_先读我.MD | 逐章依据见附录 A |
 
 ### 1.5 术语与描述口径约定
@@ -119,10 +119,11 @@
 
 ### 3.2 需求演进时间轴
 
-表 3-1 版本史与用户令要点(依据 src/version.py 的 HISTORY,共 155 条)
+表 3-1 版本史与用户令要点(依据 src/version.py 的 HISTORY,共 156 条)
 
 | 版本 | 日期 | 用户令要点 | 变化级别 |
 |---|---|---|---|
+| 2.86.0 | 2026-10-06 | 汇报定制:章节正文渲染器从 v2.9.2 原文移植 | 中(minor) |
 | 2.85.2 | 2026-10-05 | 修「执行重算」第 ⑦ 步失败:「kb_ingest.py」 把 「_install_root」 误写成 「_p._ins | 小(patch) |
 | 2.85.1 | 2026-10-05 | 措辞政策定案:以实时接口用词为准;3 条差异改判为「基线旧快照用词」 | 小(patch) |
 | 2.85.0 | 2026-10-05 | 清产物后给友好空态 | 中(minor) |
@@ -279,7 +280,7 @@
 | 0.4.0 | 2026-09-17 | 打包默认不含输入数据与产物与日志与临时文件;无窗口启动;统一配置与日志;页面归口;服务化 | 旧编号(legacy) |
 | 0.2.0 | 2026-09-01 | 含产物的旧交付基线,历史上用作「包内无生成端」那批产物的补救源 | 旧编号(legacy) |
 
-需求的演进如图 1-1 所示。该图依据 src/version.py 的 HISTORY 绘制,覆盖上表 155 条记录(即两级旧编号 0.4.0 与 0.2.0,以及从 2.5.0 起按规则递增的153个版本)。按级别统计,这153个版本里「中」级变化81条(2.85.0、2.84.0、2.83.0、2.82.0、2.81.0、2.80.0、2.79.0、2.78.0、2.77.0、2.76.0、2.75.0、2.74.0、2.73.0、2.72.0、2.71.0、2.70.0、2.69.0、2.68.0、2.67.0、2.66.0、2.65.0、2.64.0、2.63.0、2.62.0、2.61.0、2.60.0、2.59.0、2.58.0、2.57.0、2.56.0、2.55.0、2.54.0、2.53.0、2.52.0、2.51.0、2.50.0、2.49.0、2.48.0、2.47.0、2.46.0、2.45.0、2.44.0、2.43.0、2.42.0、2.41.0、2.40.0、2.39.0、2.38.0、2.37.0、2.36.0、2.35.0、2.34.0、2.33.0、2.32.0、2.31.0、2.30.0、2.29.0、2.28.0、2.27.0、2.26.0、2.25.0、2.24.0、2.23.0、2.22.0、2.21.0、2.20.0、2.19.0、2.18.0、2.17.0、2.16.0、2.15.0、2.14.0、2.13.0、2.12.0、2.11.0、2.10.0、2.9.0、2.8.0、2.7.0、2.6.0、2.5.0),「小」级变化72条(2.85.2、2.85.1、2.84.1、2.83.3、2.83.2、2.83.1、2.82.2、2.82.1、2.81.7、2.81.6、2.81.5、2.81.4、2.81.3、2.81.2、2.81.1、2.80.2、2.80.1、2.79.1、2.78.4、2.78.3、2.78.2、2.78.1、2.76.1、2.75.1、2.66.1、2.63.1、2.62.1、2.60.1、2.57.2、2.57.1、2.56.1、2.54.1、2.53.2、2.53.1、2.52.1、2.51.1、2.47.1、2.42.1、2.40.1、2.36.2、2.36.1、2.30.1、2.29.1、2.28.1、2.24.2、2.24.1、2.23.3、2.23.2、2.23.1、2.22.1、2.21.2、2.21.1、2.15.2、2.15.1、2.12.1、2.11.9、2.11.8、2.11.7、2.11.6、2.11.5、2.11.4、2.11.3、2.11.2、2.11.1、2.10.4、2.10.3、2.10.2、2.10.1、2.9.2、2.9.1、2.8.2、2.8.1),没有大版本升级。这个分布清楚地显示出这条线的性质——**功能面在 2.5.0 到 2.7.0 之间快速铺开,之后转入以消缺与口径统一为主的密集收敛期**。
+需求的演进如图 1-1 所示。该图依据 src/version.py 的 HISTORY 绘制,覆盖上表 156 条记录(即两级旧编号 0.4.0 与 0.2.0,以及从 2.5.0 起按规则递增的154个版本)。按级别统计,这154个版本里「中」级变化82条(2.86.0、2.85.0、2.84.0、2.83.0、2.82.0、2.81.0、2.80.0、2.79.0、2.78.0、2.77.0、2.76.0、2.75.0、2.74.0、2.73.0、2.72.0、2.71.0、2.70.0、2.69.0、2.68.0、2.67.0、2.66.0、2.65.0、2.64.0、2.63.0、2.62.0、2.61.0、2.60.0、2.59.0、2.58.0、2.57.0、2.56.0、2.55.0、2.54.0、2.53.0、2.52.0、2.51.0、2.50.0、2.49.0、2.48.0、2.47.0、2.46.0、2.45.0、2.44.0、2.43.0、2.42.0、2.41.0、2.40.0、2.39.0、2.38.0、2.37.0、2.36.0、2.35.0、2.34.0、2.33.0、2.32.0、2.31.0、2.30.0、2.29.0、2.28.0、2.27.0、2.26.0、2.25.0、2.24.0、2.23.0、2.22.0、2.21.0、2.20.0、2.19.0、2.18.0、2.17.0、2.16.0、2.15.0、2.14.0、2.13.0、2.12.0、2.11.0、2.10.0、2.9.0、2.8.0、2.7.0、2.6.0、2.5.0),「小」级变化72条(2.85.2、2.85.1、2.84.1、2.83.3、2.83.2、2.83.1、2.82.2、2.82.1、2.81.7、2.81.6、2.81.5、2.81.4、2.81.3、2.81.2、2.81.1、2.80.2、2.80.1、2.79.1、2.78.4、2.78.3、2.78.2、2.78.1、2.76.1、2.75.1、2.66.1、2.63.1、2.62.1、2.60.1、2.57.2、2.57.1、2.56.1、2.54.1、2.53.2、2.53.1、2.52.1、2.51.1、2.47.1、2.42.1、2.40.1、2.36.2、2.36.1、2.30.1、2.29.1、2.28.1、2.24.2、2.24.1、2.23.3、2.23.2、2.23.1、2.22.1、2.21.2、2.21.1、2.15.2、2.15.1、2.12.1、2.11.9、2.11.8、2.11.7、2.11.6、2.11.5、2.11.4、2.11.3、2.11.2、2.11.1、2.10.4、2.10.3、2.10.2、2.10.1、2.9.2、2.9.1、2.8.2、2.8.1),没有大版本升级。这个分布清楚地显示出这条线的性质——**功能面在 2.5.0 到 2.7.0 之间快速铺开,之后转入以消缺与口径统一为主的密集收敛期**。
 
 ![图 1-1 观澜需求演进时间轴(依据 src/version.py 的 HISTORY)](figures/fig-req-01-需求演进时间轴.png)
 
@@ -298,7 +299,7 @@
 | 所有的计算均要形成观澜的源代码 | 逐族补生成端:变桨面、在升闭环、三层基线、融合面 handoff、总览页、由台账生成 claim | 反向呼应审计成立 3,548 件、不成立 0 件、未归类 0 件 |
 | 运行期一律不从交付包补齐 | 第 ⑤ 步由「补齐随包件」改为「反向呼应审计」,只报账不搬运 | rebuild_all --dry-run 计划中 ⑤ 步命令为 products_reverse_audit.py --check |
 | 清除产物不留备份 | products_state.py --off --yes 改真删除;--on 与门户恢复按钮移除 | 控制台按钮语义与 409 前置校验;docs/系统设计说明.md §7 |
-| 装成服务并在安装时检查版本 | win_service.py 以 ctypes 直连 SCM;service_main.py 与 systemd 单元;install-info.json 记录版本并比对 | check 实跑报「卸载入口在位 2 个」「版本管理 v2.85.2」 |
+| 装成服务并在安装时检查版本 | win_service.py 以 ctypes 直连 SCM;service_main.py 与 systemd 单元;install-info.json 记录版本并比对 | check 实跑报「卸载入口在位 2 个」「版本管理 v2.86.0」 |
 | 观澜改为监听所有 IP | serve.json 增 public_host 且出厂为 0.0.0.0,仅门户网关用它,组件仍绑本机 | check 实跑显示 6 个端口全部运行中;README 第二节写明无鉴权风险与回退口径 |
 | 描述口径:时间窗、影响机组、中文(英文简写) | 界面文案与报告构建器统一改写;i18n_en 键同步 | docs/系统设计说明.md §17.1 与 §17.2;语言包 845 条前后端成对 |
 | 整理需求与设计与数据接入各写一份 Word 文档 | docs/src 下 markdown 源件;渲染器输出带域目录的 docx;插图生成器生成 14 张图,数字全部从真件取 | HISTORY 2.10.0 条目;本版三份文档 |
@@ -345,7 +346,7 @@
 
 ### 5.1 功能需求编号与总览
 
-功能需求按十二个域组织,编号从 FR-01 到 FR-52,共 157 条。编号是**稳定的**:新增需求往后排号,不重排已有编号;需求作废时保留编号并标注作废,不改用它。每条需求都给出五项:需求描述、输入、输出、验收判据、依据文件。其中「验收判据」是可判定的条件,「依据文件」指向仓库内的源或文档,便于复核。
+功能需求按十二个域组织,编号从 FR-01 到 FR-52,共 158 条。编号是**稳定的**:新增需求往后排号,不重排已有编号;需求作废时保留编号并标注作废,不改用它。每条需求都给出五项:需求描述、输入、输出、验收判据、依据文件。其中「验收判据」是可判定的条件,「依据文件」指向仓库内的源或文档,便于复核。
 
 表 5-1 功能需求域与编号分配
 
@@ -492,7 +493,7 @@
 
 ### 6.1 非功能需求清单
 
-非功能需求编号 NFR-01 至 NFR-12,共 157 条。它们大多是「性质」而不是「功能」:判据往往是一次实跑的输出,或者一条机器守卫的退出码。
+非功能需求编号 NFR-01 至 NFR-12,共 158 条。它们大多是「性质」而不是「功能」:判据往往是一次实跑的输出,或者一条机器守卫的退出码。
 
 表 6-1 非功能需求(NFR-01 至 NFR-06,样本场实测(2026-09-22))
 
@@ -514,7 +515,7 @@
 | NFR-09 | 安全只读:不写回现场系统、不改原始件、不产生控制指令;对外暴露时须自行在网侧限来源 | 原始件按只读输入对待,落位冲突默认拒绝;对外只有一个网关入口;启动日志每次提醒页面无鉴权 | docs/输入数据放置指导_v0.1.md §3;README_先读我.MD 第二节 |
 | NFR-10 | 可维护性:单一真源,改一处不必改多处;关键口径有机器守卫 | 版本号只有一行真源并由三处守卫盯住;路径、配置、日志、语言各有唯一取用口与审计器 | docs/系统设计说明.md §15.2 与 §11.1;docs/系统设计说明.md §3.1 |
 | NFR-11 | 无人值守与自愈:长跑的服务要能被托管并自动拉起 | 服务体每 15 秒巡检并拉起掉线组件;崩溃重启策略;远程部署一律用服务,不用会话前台进程 | docs/系统设计说明.md §14.1 与 §14.4;README_先读我.MD 第二节 |
-| NFR-12 | 收资清单按场裁剪:以通用收资模板为底,按本场的机型与数据源形态与专题范围裁剪,逐条写明必须、建议、可选或可替代以及「不收会怎样」 | 每一族都有必须性分级与缺件后果且可复核;换场时清单随场定义与机型变化,不是照抄样本场 | 本文第 11 章;《数据要求说明_观澜_2.85.2.docx》第 8 章与第 12 章 |
+| NFR-12 | 收资清单按场裁剪:以通用收资模板为底,按本场的机型与数据源形态与专题范围裁剪,逐条写明必须、建议、可选或可替代以及「不收会怎样」 | 每一族都有必须性分级与缺件后果且可复核;换场时清单随场定义与机型变化,不是照抄样本场 | 本文第 11 章;《数据要求说明_观澜_2.86.0.docx》第 8 章与第 12 章 |
 
 ### 6.2 性能与耗时实测
 
@@ -550,7 +551,7 @@
 
 | 真源 | 管什么 | 守卫与实跑结果 |
 |---|---|---|
-| src/version.py | 名称、版本、版本规则、版本史、包名 | 版本记录一致性检查;check 实跑报 v2.85.2 且记录表与代码一致 |
+| src/version.py | 名称、版本、版本规则、版本史、包名 | 版本记录一致性检查;check 实跑报 v2.86.0 且记录表与代码一致 |
 | src/paths.py | 一切路径解析的基准与助手 | 配置审计器检查不手拼路径;实跑无不一致 |
 | configs 目录与登记表 | 端口、模型档、场配置、页面归口、配置登记 | 配置审计实跑已知缺口与白名单 14 条、提示 7 条 |
 | src/logfile.py | 日志目录、命名、行格式、保留策略 | 日志审计实跑无不一致、提示 87 条 |
@@ -561,7 +562,7 @@
 
 ### 7.1 数据族与功能映射
 
-系统的数据需求可以概括成一句话:**七类现场源件加一类共享机理资料,喂出九个产物仓,页面只读产物**。每个源类目录名就是摄入接口,改名等于换接口。数据族与功能的对应关系如下表;逐类的字段、单位、必须性与质量要求见同批交付的《数据要求说明_观澜_2.85.2.docx》。
+系统的数据需求可以概括成一句话:**七类现场源件加一类共享机理资料,喂出九个产物仓,页面只读产物**。每个源类目录名就是摄入接口,改名等于换接口。数据族与功能的对应关系如下表;逐类的字段、单位、必须性与质量要求见同批交付的《数据要求说明_观澜_2.86.0.docx》。
 
 表 7-1 数据族到功能的映射(样本场实测(2026-09-22))
 
@@ -594,7 +595,7 @@
 
 ### 7.3 与数据要求说明的分工
 
-本文只回答「要哪些数据、这些数据支撑什么功能」。数据的字段级要求(核心测点的名称、单位、必须性、缺失替代、对齐规则、质量门与核对锚点)以及面向现场的收资清单,写在《数据要求说明_观澜_2.85.2.docx》里。该文档按用户令要求与现场收资文件逐条对照,并对现场收资层面的已知缺失逐条如实记录,例如测风塔数据为零交付、故障录波只有 4 台、振动侧 handoff 正本缺失由观澜自算件顶上、远端机器未安装本机模型等。
+本文只回答「要哪些数据、这些数据支撑什么功能」。数据的字段级要求(核心测点的名称、单位、必须性、缺失替代、对齐规则、质量门与核对锚点)以及面向现场的收资清单,写在《数据要求说明_观澜_2.86.0.docx》里。该文档按用户令要求与现场收资文件逐条对照,并对现场收资层面的已知缺失逐条如实记录,例如测风塔数据为零交付、故障录波只有 4 台、振动侧 handoff 正本缺失由观澜自算件顶上、远端机器未安装本机模型等。
 
 ## 8 页面与信息架构需求
 
@@ -614,7 +615,7 @@
 
 ### 8.2 门户页面归口表
 
-表 8-2 按登记表逐条列出页面与子页的归口结论。登记表顶层条目 25 条,含 7 条子页共 157 条;归口审计实跑输出为「检查 21 项:不一致 0、已知缺口 19」,其中 21 项是审计的**检查行数**(19 条为引用悬空或溯源缺失的已知缺口项,2 条为两个数据派生页面的溯源检查项,均一致),与登记表的条目数不是同一个口径,两者都不矛盾,此处一并写明以免混淆。
+表 8-2 按登记表逐条列出页面与子页的归口结论。登记表顶层条目 25 条,含 7 条子页共 158 条;归口审计实跑输出为「检查 21 项:不一致 0、已知缺口 19」,其中 21 项是审计的**检查行数**(19 条为引用悬空或溯源缺失的已知缺口项,2 条为两个数据派生页面的溯源检查项,均一致),与登记表的条目数不是同一个口径,两者都不矛盾,此处一并写明以免混淆。
 
 表 8-2 门户页面与子页归口表(25 条顶层条目加 7 条子页,样本场实测(2026-09-22))
 
@@ -676,14 +677,14 @@
 | 运行环境与依赖 | 10 | Python 版本不低于 3.11(实测 3.12.10);九个第三方依赖逐个导入 | 全 OK |
 | 静态质量门 | 4 | 源码可编译 229 个文件;语言包 845 条成对;入口引用闭合 48 条;入口脚本编码守则 | 全 OK |
 | 子进程口径 | 3 | 捕获输出可用;无窗口启动输出进日志;无窗口位已设,标志位 0x8000200 | 全 OK |
-| 版本与卸载入口 | 2 | 版本管理与记录表一致(v2.85.2,包名 app_guanlang_v2.85.2.zip);卸载入口两个都在位 | 全 OK |
+| 版本与卸载入口 | 2 | 版本管理与记录表一致(v2.86.0,包名 app_guanlang_v2.86.0.zip);卸载入口两个都在位 | 全 OK |
 | 制品与台账审计 | 5 | 反向呼应 3,548 件成立;页面归口检查 21 项;配置统一;日志统一;输入数据放置体检 26,503 件结构合规 | 全 OK |
 | 产物与发布件在位 | 10 | 标准仓、本体对象库、findings、事实契约、门户、仿真合页服务与资料包、三维资产、仿真回放资产、治理清单交付件 | 全 OK |
 | 原始件目录 | 1 | 原始件目录存在(无数据时此项不影响页面) | 全 OK |
 | 模型闸 | 6 | 默认档探针 0.77 秒;四个模型档逐个在位;模型可用性正常并记录审计日志 | 全 OK |
 | 合计 | 41 | 另有 6 条端口状态行不计入判定 | 结论全绿,可 serve,退出码 0 |
 
-验收门的分类与数量如图 7-1 所示。该图与上表同源,都取自本次自检实跑的 41 条判定行;按上表的分类口径,环境与依赖一类共 157 条、静态质量门 4 条、子进程口径 3 条、版本与卸载入口 2 条、制品与台账审计 5 条、产物与发布件在位 10 条、原始件目录 1 条、模型闸 6 条。其中真正体现「这个系统有没有把口径管住」的是制品与台账审计那五条门与四条静态质量门。
+验收门的分类与数量如图 7-1 所示。该图与上表同源,都取自本次自检实跑的 41 条判定行;按上表的分类口径,环境与依赖一类共 158 条、静态质量门 4 条、子进程口径 3 条、版本与卸载入口 2 条、制品与台账审计 5 条、产物与发布件在位 10 条、原始件目录 1 条、模型闸 6 条。其中真正体现「这个系统有没有把口径管住」的是制品与台账审计那五条门与四条静态质量门。
 
 ![图 7-1 验收门分类与数量(依据 guanlan.py check 实跑输出)](figures/fig-req-04-验收门分类.png)
 
@@ -802,7 +803,7 @@
 
 本章有两个词要先定义。「本场」指**当前被选中的场**,由配置选定,不必然等于本文取数的样本风电场;「换场」指从一场切到另一场运行的整套动作(改配置、放数据、重跑、按检查表复验),不是把两场的数据混在一棵树里。本章沿用的占位符与附录 A 一致:」<场>「、」<场站>「、」<机型>「 按实际风电场替换。
 
-本章需求与《数据要求说明_观澜_2.85.2.docx》《系统设计说明_观澜_2.85.2.docx》配套使用:本章讲「换场要满足什么」,数据要求说明讲「要向新场收哪些数据、哪些可以裁剪」,《设计说明》讲「配置在哪一层生效、哪些参数属于场相关层」。
+本章需求与《数据要求说明_观澜_2.86.0.docx》《系统设计说明_观澜_2.86.0.docx》配套使用:本章讲「换场要满足什么」,数据要求说明讲「要向新场收哪些数据、哪些可以裁剪」,《设计说明》讲「配置在哪一层生效、哪些参数属于场相关层」。
 
 取证边界要如实写一句:FR-44 至 FR-51 与 NFR-12 有既有的场配置层、摄入层与场级字段语义登记作依据;FR-52 的跨场横向对比是本次新增的口径要求,其实跑记录与机器守卫随后续版本补齐——本文不谎称已经通过。
 
@@ -881,7 +882,7 @@
 
 | 编号 | 需求 | 判据 | 依据文件 |
 |---|---|---|---|
-| NFR-12 | 收资清单按场裁剪:以通用收资模板为底,按本场的机型、数据源形态与专题范围裁剪,逐条写明必须、建议、可选或可替代以及「不收会怎样」 | 每一族都有必须性分级与缺件后果且可复核;换场时清单随场定义与机型变化,不是照抄样本场 | 《数据要求说明_观澜_2.85.2.docx》第 8 章与第 12 章;docs/输入数据放置指导_v0.1.md |
+| NFR-12 | 收资清单按场裁剪:以通用收资模板为底,按本场的机型、数据源形态与专题范围裁剪,逐条写明必须、建议、可选或可替代以及「不收会怎样」 | 每一族都有必须性分级与缺件后果且可复核;换场时清单随场定义与机型变化,不是照抄样本场 | 《数据要求说明_观澜_2.86.0.docx》第 8 章与第 12 章;docs/输入数据放置指导_v0.1.md |
 
 ### 11.9 换场验收需求
 
@@ -1024,8 +1025,8 @@
 | 场定义与机型与物理约束参数的写法样例 | app_ETL/configs/farms/<场>.yaml |
 | 机型—场站数据契约与字段单位真源 | app_ETL/configs/contracts/<机型>_<场>.yaml 与 app_ETL/configs/canonical/dictionary.yaml |
 | 场级字段语义登记与其它厂商导出形态的对齐 | app_ETL/configs/farms/<场>/field_semantic_registry.yaml |
-| 按场裁剪的收资清单与必须性分级 | docs/src/数据要求说明_观澜_2.85.2.md |
-| 场无关引擎与场相关参数的分层与换场作业单 | docs/src/系统设计说明_观澜_2.85.2.md |
+| 按场裁剪的收资清单与必须性分级 | docs/src/数据要求说明_观澜_2.86.0.md |
+| 场无关引擎与场相关参数的分层与换场作业单 | docs/src/系统设计说明_观澜_2.86.0.md |
 
 表中 」<场>「、」<场站>「、」<机型>「 为占位符,按实际风电场替换。
 
@@ -1068,4 +1069,4 @@
 | 换场 | — | 从一场切到另一场运行的整套动作:改配置、放数据、重跑、按检查表复验 | 本文 §11.9 |
 | 缺族降级 | — | 某族数据缺失时按缺件如实标注并降低该族功能,不造数、不用他场数字顶替 | 本文 FR-49 |
 | 按场标定 | — | 把阈值、时间窗锚点、限电窗、温度档带宽等场相关参数按本场取值 | 本文 FR-50 |
-| 通用收资模板 | — | 与场无关的收资条目底稿,按场裁剪后作为现场收资清单 | docs/src/数据要求说明_观澜_2.85.2.md |
+| 通用收资模板 | — | 与场无关的收资条目底稿,按场裁剪后作为现场收资清单 | docs/src/数据要求说明_观澜_2.86.0.md |

BIN
docs/数据要求说明_观澜_2.85.2.docx → docs/数据要求说明_观澜_2.86.0.docx


+ 6 - 5
docs/版本记录.md

@@ -7,7 +7,7 @@
 <!-- VERSION-LOG:BEGIN -->
 ## 版本号规则(用户令 2026-09-17)
 
-编号 **`v<大版本号>.<中版本号>.<小版本号>`**,例如 `v2.85.2`。
+编号 **`v<大版本号>.<中版本号>.<小版本号>`**,例如 `v2.86.0`。
 
 | 级别 | 含义 |
 |---|---|
@@ -17,12 +17,13 @@
 
 **怎么改**:改动落在"解决方案/架构/核心功能" → 大 +1(中/小归 0);落在"非核心功能的新增/减少/修改" → 中 +1(小归 0);只是"消缺完善" → 小 +1;一次发布混了几类,取最高那一级
 
-**打包文件名** = `app_guanlang_v2.85.2.zip`(规则:`app_guanlang_v<版本号>.zip`)。
+**打包文件名** = `app_guanlang_v2.86.0.zip`(规则:`app_guanlang_v<版本号>.zip`)。
 
-版本号只写在 `src/version.py` 一行(`VERSION = '2.85.2'`);打包器、安装脚本、安装记录 `install-info.json`、自检都读它,不另设真源。
+版本号只写在 `src/version.py` 一行(`VERSION = '2.86.0'`);打包器、安装脚本、安装记录 `install-info.json`、自检都读它,不另设真源。
 
 | 版本 | 日期 | 级别 | 主要变更 | 说明 |
 |---|---|---|---|---|
+| `v2.86.0` | 2026-10-06 | 中 | 汇报定制:章节正文渲染器从 v2.9.2 原文移植(此前为空实现) | 用户反馈:「汇报定制」页面与原页面不一致。**实逮根因**:Vue 版里两块是**空实现** —— `factsHtml = computed(() => "")` 与 `blockHtml = () => ""` ⇒ 选中任何章节,纸面上**什么都不渲染** (而一致性判据只比默认视图「预览为空」⇒ 两边一致 ⇒ 判据 15/15 也没发现)。**修**:把 v2.9.2 的 `rptFacts` / `rptK` / `rptBlock`(app.js 第 531–571 行)**原文移植**为 `src/lib/reportBlocks.ts`(工厂形式接收注入的 t/fmt/esc/dv/displayText/chart/SYS_KEY/sysName/stateChip/unitNo/STEP/winName/语言包/RPT/FACTS),仅做机械适配:`CH.x(` → `chart("x", `、`D.` → `d.`、`BD` 明确为 `d.fus.链盘`(旧页 app.js:541 的原始定义)。同时在 Report.vue 接线 `blockHtml/factsHtml`,并让 App 为 report 页签补取 `/api/facts`(事实卡契约来源)。**过程实逮(自身缺陷,均已修)**:① 抽取旧码时少取一行 ⇒ 括号不闭合(构建报 Unexpected end of file);② 重复导入 winName(Identifier already declared);③ 语言包导出名是 `ZH` 而非 `zh`。**验证(真应用 jsdom · 点「月报」预设)**:`BEFORE_ELS 1` ⇒ `AFTER_ELS 575 · BLOCKS 10 · CHART_HOSTS 4 · TEXT_LEN 4180`,正文首段为「观澜 · 运行分析报告 / 统计期 2026年 | 38 台 / 9 个章节 / 250,744MWh 上网电量 / 理论可发 287,713 / 1,650h 等效满发小时 / 94.8% 时间可用率 / 12.8% 损失率 / 36,968 MWh / 172h 平均停机间隔 …」,`ERRS 0` ✓。 |
 | `v2.85.2` | 2026-10-05 | 小 | 修「执行重算」第 ⑦ 步失败:「kb_ingest.py」 把 「_install_root」 误写成 「_p._install_root」 | 用户令(选 A):等"数据重算"跑完后复跑判据。**过程中实逮一个后端代码 bug**:整轮 「rebuild」 耗时 2,701 s、21 步里**只有第 ⑦ 步「本体: 码表/手册/文档」失败** ⇒ 「app_ontology/app_ontology_guanlan/ontology/kb_ingest.py:74」 调用了 **「_p._install_root(__file__)」**,而 「_install_root」 是本模块第 20 行 「from app_common.app_common_guanlan.api import install_root as _install_root」 (或第 23 行的兜底定义)导入的**模块级函数**;「_p」 在此处解析成了别的东西(报错:「AttributeError: module 'pathlib' has no attribute '_install_root'」)⇒ 该步失败并**中断整轮重算**(后面步骤依赖它 ⇒ 「/api/fleet」 持续 500 · 页面呈"无数据")。**修**:全仓检索同写法,仅此 1 处 ⇒ 改为 **「_install_root(__file__)」**;并在远端**单独跑该步验证通过**:「python -m src.ontology.kb_ingest」 ⇒ 产出 「{AlarmCode: 555, WorkInstruction: 559, FailureMode: 150, MaintTask: 559, Doc: 303, Component: 9, causedBy: 60, …}」 ✓。随后重新触发整轮 「rebuild」(各步幂等)以补齐后续步骤,待 「/api/fleet」 恢复 200 后再复跑结构/内容两套判据(15/15 目标)。 |
 | `v2.85.1` | 2026-10-05 | 小 | 措辞政策定案:以实时接口(当前算法)用词为准;3 条差异改判为「基线旧快照用词」 | 承接上一轮"建议第 2 步":统一 「链判级」 / 「窗末月?」 / 「平均停机间隔(MTBO)」 三处措辞差异。**先查清来源(实测)**:这**不是**同一系统两条路径互相打架,而是「**v2.9.2 基线页面自带的旧快照用词**」与「**当前算法新词**」之差 —— 实时 「/api/fleet」 里新词**内部一致**(「MTBO口径」 1 处 · 「变化口径」 1 处 · 「纯故障口径」 1 处;旧词 0 处),而基线 「detail_v2.html」 里旧词 7 处、新词 0 处。**用户裁定(2026-10-05):选 A —— 以当前算法新词为准**(「口径」 等),旧快照用词视为已过时。**落地**:判据工具 「tools/render_parity_v292.mjs」 中三条例外的 「why」 由"待后端统一口径"改判为「**基线旧快照用词 vs 当前算法新词 · 以实时接口为准 · 预期差异(非缺陷)**」,并在版本记录留档。**现状说明(重要)**:本轮复跑对拍时发现 「/api/fleet」 返回 **500(110 B)** —— 因为**产物已被用户"清除产物"清除**,故基线页与 Vue 侧都取不到数据、对拍必然 [X](**预期现象,不是回归** ✓)。待用户执行「执行重算」后,需复跑结构/内容两套判据确认仍为 15/15(本轮之前刚跑过:结构 15/15 · 内容 15/15 ✓)。 |
 | `v2.85.0` | 2026-10-05 | 中 | 清产物后给友好空态(不暴露 HTTP 500) | 用户令:清除产物后相应页面不显示数据,**提示要友好(不要报 HTTP 500)**。**修**:「App.load()」 对 4xx/5xx/异常**一律视为"暂无数据"** ⇒ 「d = {}」(空快照 ⇒ 各页照常渲染空态、不崩)+ 「dataMissing = true」,页面顶部给友好可操作提示(i18n 「app.data_missing」:「暂无数据:产物尚未生成或已被清除。请到「系统维护」页执行「数据重算」(或在 /ops 控制台点「执行重算」),完成后本页会自动刷新。」),**不出现任何 HTTP 状态码**。**过程实逮(两处自身缺陷,均已修)**:① 脚本因锚点不匹配漏插 「const dataMissing」 ⇒ 打包产物报 「ReferenceError: dataMissing is not defined」;② 「System.vue」 的 vue import 漏补 「onMounted」 ⇒ 「ReferenceError: onMounted is not defined」(system 页签渲染失败)。**验证(模拟 「/api/fleet」 返回 500)**:七个页签(overview/energy/component/vibration/generation/fault/system)逐一 ⇒ 「errs=0」 · 「friendly=true」 · 「leaks500=false」 ✓。 |
@@ -181,8 +182,8 @@
 
 ## 本次交付
 
-* 版本:`v2.85.2`(观澜·如东样板 v2)
-* 交付包:`app_guanlang_v2.85.2.zip`
+* 版本:`v2.86.0`(观澜·如东样板 v2)
+* 交付包:`app_guanlang_v2.86.0.zip`
 * 服务名:`guanlan`(Windows 服务 / systemd 单元)
 * 安装 / 卸载:`install.bat`、`install.sh` / `uninstall.bat`、`uninstall.sh`
 <!-- VERSION-LOG:END -->

BIN
docs/系统设计说明_观澜_2.85.2.docx → docs/系统设计说明_观澜_2.86.0.docx


BIN
docs/需求分析_观澜_2.85.2.docx → docs/需求分析_观澜_2.86.0.docx


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
release/web/assets/index-ByKwC0f-.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 4
release/web/assets/index-CjdARNCg.js


+ 1 - 1
release/web/index.html

@@ -4,7 +4,7 @@
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1" />
   <title>观澜 · 风电数据分析与决策支持</title>
-  <script type="module" crossorigin src="/web/assets/index-CjdARNCg.js"></script>
+  <script type="module" crossorigin src="/web/assets/index-ByKwC0f-.js"></script>
   <link rel="stylesheet" crossorigin href="/web/assets/index-DuQ4wHja.css">
 </head>
 <body>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است