Bladeren bron

2.55.0 入口改为 Vue 工作台(/)·门户退役: 网关文件优先读 release/web/index.html 并改写资源路径为 /web/assets/, 契约期望刷新, 重算链去重装门户步, 删门户构建器与旧页装配件(builders/* + pages/build + pages/classic + 3 个脚本壳) => 契约复核 40/40, 全门禁通过

zhouyang.xie 1 week geleden
bovenliggende
commit
450dd52e60

+ 1 - 1
VERSION

@@ -1 +1 @@
-2.54.1
+2.55.0

+ 0 - 2
app_ETL/app_ETL_guanlan/builders/rebuild_all.py

@@ -206,8 +206,6 @@ def build_plan(a) -> list:
     plan.append(step_cmd('⑧ 本体审计 (期望 0 问题)', [PY, '-m', 'src.ontology.audit']))
     # ⑧b 重装门户: 门户正文里的"契约结论段"是**产物派生**的(由 guanlan_portal_inject_claims.py
     #     从 outputs/<场>/guanlan/… 渲染), 所以产物变了门户必须重装一遍, 否则页面挂着旧结论。
-    #     装配是"先写临时文件再原子替换", 失败不会破坏正在被网关读的 portal.html (见 portal_build 注释)。
-    plan.append(step_cmd('⑧b 重装门户 (把新产物灌进门户结论段)', [PY, 'scripts/portal_build.py'],
                          tolerate=(1,),
                          note='rc=1 = 缺契约产物(产物被清除时属预期) ⇒ 门户保留原样, 不是装配坏了'))
     # ⑧c 页面归口审计: 重算之后, 那些"把数据烘进页面"的页面件(见 configs/portal_pages.yaml)会不会陈旧。

+ 3 - 2
app_backEnd/app_backEnd_guanlan/contract/http_api_v1.json

@@ -327,8 +327,9 @@
       "method": "GET",
       "status": 200,
       "ctype": "text/html",
-      "html_title": "观澜中文系统 · 详细分析",
-      "bytes_at_least": 512
+      "html_title": "观澜 · 风电数据分析与决策支持",
+      "bytes_at_least": 512,
+      "entry_note": "★P13(2026-10-01 用户令):入口改为 Vue 工作台(/ 由网关伺服 release/web/index.html,实测 200 · 413 B · 响应头 X-Portal-Source: vue);旧门户页退役。"
     },
     {
       "kind": "gateway",

+ 6 - 4
app_backEnd/backend-gateway/src/main/java/com/guanlan/gateway/web/PortalController.java

@@ -38,11 +38,11 @@ public class PortalController {
                 .build();
     }
 
-    /** 读门户制品:默认 release/portal.html,可用 GUANLAN_PORTAL_FILE 覆盖;找不到返回 null。 */
+    /** 读入口制品:★终局(P13)入口 = Vue 工作台制品 `release/web/index.html`;可用 GUANLAN_PORTAL_FILE 覆盖。 */
     private String readLocal() {
         String rel = System.getenv("GUANLAN_PORTAL_FILE");
         if (rel == null || rel.isEmpty()) {
-            rel = "release/portal.html";
+            rel = "release/web/index.html";
         }
         Path p = Paths.get(rel);
         Path cwd = Paths.get("").toAbsolutePath();
@@ -72,13 +72,15 @@ public class PortalController {
     public Mono<ResponseEntity<String>> portal() {
         String local = readLocal();
         if (local != null) {
-            return Mono.just(html("file", local));
+            // ★P13 终局:Vue 壳里的资源是相对路径(./assets/…);在根路径伺服时改写为**已路由**的 /web/assets/…
+            String fixed = local.replace("\"./assets/", "\"/web/assets/").replace("'./assets/", "'/web/assets/");
+            return Mono.just(html("vue", fixed));
         }
         return web.get().uri("/").retrieve().bodyToMono(String.class)
                 .timeout(Duration.ofSeconds(60))
                 .map(body -> html("upstream", body))
                 .onErrorResume(e -> Mono.just(ResponseEntity.status(503)
                         .contentType(MediaType.valueOf("text/html; charset=utf-8"))
-                        .body("<h1>门户暂不可用</h1><p>制品缺失且上游未启动: " + e.getClass().getSimpleName() + "</p>")));
+                        .body("<h1>入口页不可用</h1><p>制品缺失且上游未启动: " + e.getClass().getSimpleName() + "</p>")));
     }
 }

+ 4 - 1
app_common/app_common_guanlan/version.py

@@ -24,7 +24,7 @@ import json
 import pathlib
 
 NAME = '观澜·如东样板 v2'
-VERSION = '2.54.1'         # ★ 版本只改这里
+VERSION = '2.55.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.55.0', date='2026-10-01', level='minor',
+         title='入口改为 Vue 工作台 · 门户退役(连带:契约期望刷新 · 重算链去门户步 · 删门户构建器与旧页装配件)',
+         note='① 用户令(2026-10-01):**入口改为 Vue 工作台(/ → Vue),门户退役**。落地:网关 `PortalController` 的文件优先目标从 `release/portal.html` 改为 **`release/web/index.html`**(Vue 壳),并把壳里的**相对**资源路径 `./assets/…` 改写为**已路由**的 `/web/assets/…`(省掉新增路由与 YAML 改动 —— 此前多次踩过 yml 重复键与谓词怪癖);响应头 `X-Portal-Source: vue`。实测:`/` 与 `/index.html` → 200 · 413 B(标题「观澜 · 风电数据分析与决策支持」)、`/web/assets/index-*.js` → 200(195,415 B)、`/api/fleet` 与 `/algorithm` 照常。② 连带项(逐项处理):a) 契约 `/` 的**期望标题刷新**为现值并加 `entry_note` 说明(复核回到 **40/40**);b) 重算链 `app_ETL/.../rebuild_all.py` 去掉「⑧b 重装门户」步(门户不再随数据重建);c) 删除门户构建器与旧页装配件:`scripts/portal_build.py`、`scripts/guanlan_portal_{fix_anchors,inject_claims}.py`、`app_frontEnd_guanlan/builders/*`(3 件 + `__init__`)、`pages/build.py`、`pages/classic.py`,并摘除 `app_frontEnd_guanlan/api.py` 中相应转发名。③ 仍未动(需各自专项):`pages/snapshot.py`(产物生成器 `windscada_overview_build.py` 依赖)、`lang.py`(工具链 `cli.py` 与审计 `guanlan_baseline_manifest`/`data_tpl_en` 依赖)、`serve.py` 与 `scripts/windscada_serve.py`(旧业务后端,`/api` 已由 Java 承接)。④ 复验:**HTTP 契约复核 40/40**、**全门禁通过**。'),
     dict(version='2.54.1', date='2026-10-01', level='patch',
          title='删除旧页 design/terms(含兼容壳)+ 摘除 api 转发 ⇒ 全门禁保持通过',
          note='① 删除批次续:删掉旧页设计系统与显示层术语映射 —— `app_frontEnd_guanlan/design.py`(233 行)、`terms.py`(37 行) 及两个兼容壳 `src/windscada/design.py`、`src/windscada/terms.py`。② 连带同步(门禁实逮):模块边界审计报 R5 两条 —— `app_frontEnd.api.design/terms 的转发目标不存在`;已在 `app_frontEnd_guanlan/api.py` 的 `_TARGETS` 里摘除这两个转发名并写明理由。③ 复验:模块边界审计 `[OK] 结构齐全 · 只经 api 调用 · 依赖方向合规 · 公共层纯净 · 接口目标全在位`,**全门禁通过**。④ 删除前的影响面测绘(本轮做的功课,值得记档):`pages/build.py` 被**门户构建器** `scripts/portal_build.py` 依赖、`pages/snapshot.py` 被**产物生成器**`scripts/windscada_overview_build.py` 依赖、`lang.py` 被工具链与审计依赖 ⇒ 这三项**不可随旧页一并删**,否则门户无法重建、产物无法重算;要删它们得先"冻门户/改生成端/改审计口径",属独立一步。⑤ 因此本轮只删**确认无依赖**的 design/terms 两项,其余按上一条结论留待专项处理。'),

+ 5 - 5
app_frontEnd/app_frontEnd_guanlan/api.py

@@ -17,14 +17,14 @@ from __future__ import annotations
 import importlib
 
 _TARGETS: dict[str, str] = {
-    'ui_build': 'app_frontEnd.app_frontEnd_guanlan.pages.build',
+    # ★P13(2026-10-01):ui_build 已随门户/旧页退役删除
     'ui_snapshot': 'app_frontEnd.app_frontEnd_guanlan.pages.snapshot',
-    'classic_pages': 'app_frontEnd.app_frontEnd_guanlan.pages.classic',
+    # ★P13(2026-10-01):classic_pages 已随门户/旧页退役删除
     # ★P13(2026-10-01):`design`/`terms` 已按令删除(旧页设计系统与显示层术语映射随旧页退役;现行前端在 web/ 下)
     'lang': 'app_frontEnd.app_frontEnd_guanlan.lang',
-    'portal_build': 'app_frontEnd.app_frontEnd_guanlan.builders.portal_build',
-    'portal_anchor_fix': 'app_frontEnd.app_frontEnd_guanlan.builders.portal_fix_anchors',
-    'portal_claims': 'app_frontEnd.app_frontEnd_guanlan.builders.portal_inject_claims',
+    # ★P13(2026-10-01):portal_build 已随门户/旧页退役删除
+    # ★P13(2026-10-01):portal_anchor_fix 已随门户/旧页退役删除
+    # ★P13(2026-10-01):portal_claims 已随门户/旧页退役删除
     # 英文投影层仍在既有路径(数据层投影,归口待裁;P5 未迁)
     'ui_en': 'src.windscada.ui_en',
     'i18n_en': 'src.windscada.i18n_en',

+ 0 - 2
app_frontEnd/app_frontEnd_guanlan/builders/__init__.py

@@ -1,2 +0,0 @@
-# -*- coding: utf-8 -*-
-r"""门户与静态页装配器(CLI 入口保留在 scripts/)(P5 起)。"""

+ 0 - 317
app_frontEnd/app_frontEnd_guanlan/builders/portal_build.py

@@ -1,317 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-r"""门户装配器 —— 把 `release\portal.html` 拆成"受管的源 + 不入库的产物", 并可逐字节重建 (2026-09-11)。
-
-## 为什么
-
-`release\portal.html` 20.23 MB 里 **99.3% 是内嵌的交付文档**(28 个 `<template>`, ~20 MB) ——
-那是产物/交付件内容, 却因为"门户外壳也在同一个文件里"而整体进了版本管理。
-本器把两者拆开: **外壳受管、内嵌件与合成结果不入库**, 且装配结果与拆之前**逐字节相同**(`--verify` 证明)。
-
-## 目录约定
-
-    release/portal.html                        ← 产物: 装配结果 (不入 git; 服务/网关直接读它)
-    release/portal_src/shell.html              ← **受管**: 门户外壳 (样式/脚本/导航/面板), 内嵌件处留标记
-    release/portal_src/templates/<id>.html     ← 产物: 28 个内嵌交付文档正文 (治理清单分册/报告/仿真台面板)
-    release/portal_src/governance_sources.json ← 产物: 脱敏资料索引 + source_sha256
-    release/portal_src/manifest.json           ← **受管**: 各件 sha256 + 期望的 portal.html sha256 + 说明
-    release/portal_src/README.md               ← **受管**: 人读的重建说明
-
-## 标记 (shell.html 里)
-
-    <!--@TEMPLATE:<id>-->       → <template id="<id>">…templates/<id>.html…</template>
-    <!--@GOVERNANCE_SOURCES-->  → governance_sources.json 内容 (位于 <script type="application/json"> 内)
-    contract-claims 段**不在** shell 里: 它是产物, 装配最后交 guanlan_portal_inject_claims.py 注入。
-
-## 行尾约定 (踩过)
-
-门户行尾是 **LF**。Python 文本模式在 Windows 上写文件会把 `\n` 变成 `\r\n` —— 20 MB 文件整体被改写,
-`/api/version` 里的 portal_sha256 指纹随之变化。所以本器全程**字节级读写**(`rd`/`wr`), 并在装配后自检"无 CRLF";
-另外两个就地注入器 (`guanlan_portal_fix_anchors.py` / `guanlan_portal_inject_claims.py`) 也一并加了 `newline=""`。
-
-## 用法
-
-    python scripts/portal_build.py --extract    # 一次性: 从现有 portal.html 拆出 portal_src/
-    python scripts/portal_build.py              # 装配 → release/portal.html (含 claims)
-    python scripts/portal_build.py --no-claims  # 只装配外壳与内嵌件
-    python scripts/portal_build.py --verify     # 装配到临时文件, 与现有 portal.html **逐字节**比对
-    python scripts/portal_build.py --check      # 各源件与 manifest 的 sha256 是否漂移
-"""
-from __future__ import annotations
-
-import argparse
-import hashlib
-import json
-import pathlib
-import re
-import sys
-import tempfile
-import time
-
-try:
-    from app_common.app_common_guanlan.api import install_root as _install_root
-except ImportError:                     # 理论不可达;包结构异常时回退到按位置上跳
-    from pathlib import Path as _P
-    def _install_root(_f): return _P(_f).resolve().parents[4]
-
-ROOT = _install_root(__file__)     # 模块化后按标记找安装根(原 parents[1] 已不成立;本件在 builders/ 下)
-sys.path.insert(0, str(ROOT))
-from src import paths as P                                          # noqa: E402
-
-PORTAL = P.PORTAL
-SRC = P.RELEASE / 'portal_src'
-TPL_DIR = SRC / 'templates'
-SHELL = SRC / 'shell.html'
-GOV = SRC / 'governance_sources.json'
-MANIFEST = SRC / 'manifest.json'
-
-RE_TPL = re.compile(r'<template id="([^"]+)">(.*?)</template>', re.S)
-RE_TPL_MARK = re.compile(r'<!--@TEMPLATE:([^>]+?)-->')
-RE_GOV = re.compile(r'(<script type="application/json" id="governance-sources">)(.*?)(</script>)', re.S)
-RE_GOV_MARK = re.compile(r'<!--@GOVERNANCE_SOURCES-->')
-RE_CLAIMS = re.compile(r'<section id="contract-claims"[^>]*>.*?</section><!--/contract-claims-->', re.S)
-CRLF = b'\r\n'
-
-
-def rd(p: pathlib.Path) -> str:
-    """字节级读: 不做换行转换 (门户行尾约定 = LF; 文本模式在 Windows 上会把 \\n 变 \\r\\n)。"""
-    return p.read_bytes().decode('utf-8')
-
-
-def wr(p: pathlib.Path, s: str) -> None:
-    """字节级写: 同上, 保证装配结果与拆之前逐字节可比。"""
-    p.write_bytes(s.encode('utf-8'))
-
-
-def sha(b) -> str:
-    return hashlib.sha256(b if isinstance(b, bytes) else b.encode('utf-8')).hexdigest()
-
-
-# ────────────────────────────────────────────────────────────── 拆 (一次性)
-def do_extract() -> int:
-    if not PORTAL.is_file():
-        print(f'[X] 门户不存在: {P.rel(PORTAL)}')
-        return 1
-    raw = rd(PORTAL)
-    orig_sha = sha(raw)
-    TPL_DIR.mkdir(parents=True, exist_ok=True)
-
-    items = {}
-    for m in RE_TPL.finditer(raw):
-        tid, body = m.group(1), m.group(2)
-        wr(TPL_DIR / f'{tid}.html', body)
-        items[tid] = dict(file=f'templates/{tid}.html', bytes=len(body.encode('utf-8')), sha256=sha(body))
-    shell = RE_TPL.sub(lambda m: f'<!--@TEMPLATE:{m.group(1)}-->', raw)
-
-    g = RE_GOV.search(shell)
-    if not g:
-        print('[X] 找不到 governance-sources 脚本块')
-        return 1
-    gov_body = g.group(2)
-    json.loads(gov_body)                                   # 自检: 必须是合法 JSON
-    wr(GOV, gov_body)
-    shell = shell[:g.start(2)] + '<!--@GOVERNANCE_SOURCES-->' + shell[g.end(2):]
-
-    n_claims = len(RE_CLAIMS.findall(shell))
-    shell = RE_CLAIMS.sub('', shell)                        # claims 是产物: 不进外壳
-    wr(SHELL, shell)
-
-    man = dict(
-        built_from=dict(portal=P.rel(PORTAL), portal_sha256=orig_sha),
-        shell=dict(file='shell.html', bytes=len(shell.encode('utf-8')), sha256=sha(shell)),
-        governance_sources=dict(file='governance_sources.json',
-                                bytes=len(gov_body.encode('utf-8')), sha256=sha(gov_body)),
-        templates=dict(count=len(items), items=items),
-        claims_section_stripped=n_claims,
-        expected_portal_sha256=orig_sha,
-        line_ending='LF',
-        notes=[
-            'shell.html = 门户外壳 (CSS/JS/导航/面板结构), 含 <!--@TEMPLATE:id--> 与 <!--@GOVERNANCE_SOURCES--> 标记;',
-            'templates/ 与 governance_sources.json 是**产物/交付件内容** (按"产物不进 git"的约定不入库); '
-            'manifest 里留 sha256 以便漂移检测 (--check);',
-            'contract-claims 段由 scripts/guanlan_portal_inject_claims.py 在装配最后注入 (内容来自 outputs/<场>/guanlan/…);',
-            '锚点修复 scripts/guanlan_portal_fix_anchors.py 的输出是**代码**, 已固化在 shell.html 里;',
-            '行尾必须是 LF: 三个脚本 (本器 + 两个注入器) 都已用字节级/newline="" 写文件, 装配后有 CRLF 自检。',
-        ])
-    wr(MANIFEST, json.dumps(man, ensure_ascii=False, indent=1))
-    print(f'拆出: shell.html {man["shell"]["bytes"]/1e3:.1f} KB · templates/ {len(items)} 个 '
-          f'({sum(v["bytes"] for v in items.values())/1e6:.2f} MB) · governance_sources.json '
-          f'{man["governance_sources"]["bytes"]/1e3:.1f} KB · 剥离 claims 段 {n_claims} 处')
-    print(f'原门户 sha256 {orig_sha[:16]} → 记为期望值; 下一步跑 --verify 验逐字节一致')
-    return 0
-
-
-# ────────────────────────────────────────────────────────────── 装
-def assemble(out: pathlib.Path, claims: bool = True) -> tuple[str, dict]:
-    if not SHELL.is_file():
-        raise SystemExit(f'缺外壳 {P.rel(SHELL)}; 先跑 --extract')
-    if not TPL_DIR.is_dir():
-        raise SystemExit(f'缺内嵌件目录 {P.rel(TPL_DIR)}')
-    s = rd(SHELL)
-    rep = dict(templates=0, sources=0)
-
-    def sub_tpl(m):
-        tid = m.group(1)
-        f = TPL_DIR / f'{tid}.html'
-        if not f.is_file():
-            raise SystemExit(f'缺模板 {P.rel(f)}')
-        rep['templates'] += 1
-        return f'<template id="{tid}">' + rd(f) + '</template>'
-
-    s = RE_TPL_MARK.sub(sub_tpl, s)
-    if RE_TPL_MARK.search(s):
-        raise SystemExit('仍有模板标记未替换 (shell 与 templates/ 不一致)')
-    if RE_GOV_MARK.search(s):
-        if not GOV.is_file():
-            raise SystemExit(f'缺 {P.rel(GOV)}')
-        rep['sources'] = 1
-        s = RE_GOV_MARK.sub(lambda m: rd(GOV), s, count=1)
-
-    wr(out, s)
-    if claims:
-        inj = ROOT / 'scripts' / 'guanlan_portal_inject_claims.py'   # CLI 兼容壳, 实现见 builders/portal_inject_claims.py
-        if inj.is_file():
-            import importlib.util
-            spec = importlib.util.spec_from_file_location('_portal_inject', inj)
-            mod = importlib.util.module_from_spec(spec)
-            spec.loader.exec_module(mod)
-            try:
-                rep['claims_sha'] = mod.inject(out, out)[:16]
-            except FileNotFoundError as e:
-                # 产物被挪走时 (scripts/products_state.py --off) 契约派生物不在 —— 这不是程序坏了:
-                # 契约段的来源就是产物, 产物不在则内容不在。给一句人话, 别丢裸 traceback。
-                raise SystemExit(
-                    f'契约结论段生成不了: 缺产物 {getattr(e, "filename", "?")}\n'
-                    f'  原因: 门户正文的结论段由事实契约派生物渲染, 产物被挪走时它自然不在 '
-                    f'(空状态属预期)。\n'
-                    f'  三选一: ① scripts/products_state.py --on 还原产物后重跑;\n'
-                    f'          ② 加 --no-claims 只装外壳与内嵌件 (装出的门户会缺这一节);\n'
-                    f'          ③ 先 scripts/rebuild_from_raw.py 重新产出。')
-        else:
-            rep['claims_sha'] = '(无注入器, 跳过)'
-    b = out.read_bytes()
-    if b.count(CRLF):
-        raise SystemExit(f'装配结果里出现 {b.count(CRLF):,} 处 CRLF —— 门户行尾必须是 LF; '
-                         f'多半是某个注入器又用了文本模式写文件 (查 scripts/guanlan_portal_*.py 的 write_text)')
-    return sha(b), rep
-
-
-def do_build(claims=True) -> int:
-    # 先装到临时文件, 成功后再原子替换 —— 否则一旦中途失败 (产物不在 → 契约段生成不了),
-    # 就地写坏的就是正在被网关读的那个 20 MB 门户, 而且它不在 git 里, 没法 checkout 回来
-    # (2026-09-11 实测踩到: --no-claims 直接把 release/portal.html 换成了缺契约段的版本)。
-    tmp = PORTAL.with_name(PORTAL.name + '.tmp')
-    try:
-        h, rep = assemble(tmp, claims)
-        tmp.replace(PORTAL)
-    finally:
-        if tmp.exists():
-            tmp.unlink()
-    print(f'装配完成 → {P.rel(PORTAL)}  sha256 {h[:16]}  '
-          f'(内嵌件 {rep["templates"]} 个, 资料索引 {rep["sources"]} 处, claims {rep.get("claims_sha")})')
-    return 0
-
-
-def do_verify() -> int:
-    if not PORTAL.is_file():
-        print(f'[X] 现有门户不存在: {P.rel(PORTAL)}')
-        return 1
-    cur = PORTAL.read_bytes()
-    with tempfile.TemporaryDirectory() as td:
-        tmp = pathlib.Path(td) / 'portal.html'
-        _, rep = assemble(tmp, claims=True)
-        built = tmp.read_bytes()
-    print(f'现有 {P.rel(PORTAL)}  {len(cur)/1e6:.2f} MB  sha256 {sha(cur)[:16]}')
-    print(f'装配结果                {len(built)/1e6:.2f} MB  sha256 {sha(built)[:16]}')
-    print(f'  内嵌件 {rep["templates"]} 个 · 资料索引 {rep["sources"]} 处 · claims {rep.get("claims_sha")}')
-    if built == cur:
-        print('  逐字节一致 ✔ (拆→装回到同一个文件: 装配链可信, 产物可重建)')
-        return 0
-    i = next((k for k in range(min(len(cur), len(built))) if cur[k] != built[k]), min(len(cur), len(built)))
-    print(f'  ✘ 不一致: 首个差异字节 @{i:,}  (现有 {len(cur):,} / 装配 {len(built):,})')
-    print(f'    现有: …{cur[max(0,i-70):i+70].decode("utf-8","replace")!r}…')
-    print(f'    装配: …{built[max(0,i-70):i+70].decode("utf-8","replace")!r}…')
-    return 3
-
-
-def do_rebaseline() -> int:
-    """把当前 shell.html / portal.html 记为新的基线 (manifest 里的 sha256)。
-
-    为什么需要它: manifest 的 sha256 是"漂移检测"的基准。**有意**改了门户外壳 (例如 2026-09-16
-    按用户令在菜单里加「数据重算」) 之后, `--check` 会如实报漂移 —— 这是对的, 但不能让人只有
-    "手改 JSON" 或 "跑 --extract 重新拆包" 两条路: 后者会用 portal.html 反过来覆盖 shell.html
-    (而装配过程会注入契约结论段/内嵌模板), 一不小心就把手工维护的外壳冲掉。
-    本命令只改 manifest 里的两个基准值, 并记下是谁、什么时候、为什么重基线。
-    """
-    if not MANIFEST.is_file():
-        print(f'[X] 缺 {P.rel(MANIFEST)}; 先跑 --extract')
-        return 1
-    if not (SHELL.is_file() and PORTAL.is_file()):
-        print('[X] shell.html / portal.html 不全, 不能重基线')
-        return 1
-    man = json.loads(rd(MANIFEST))
-    old_shell = man.get('shell', {}).get('sha256', '?')[:16]
-    old_portal = (man.get('expected_portal_sha256') or '?')[:16]
-    shell_b = SHELL.read_bytes()
-    portal_b = PORTAL.read_bytes()
-    man['shell'] = dict(file='shell.html', bytes=len(shell_b), sha256=sha(shell_b))
-    man['expected_portal_sha256'] = sha(portal_b)
-    man['notes'] = list(man.get('notes') or []) + [
-        f'重基线 {time.strftime("%Y-%m-%d %H:%M")}: shell {old_shell}→{sha(shell_b)[:16]}, '
-        f'portal {old_portal}→{sha(portal_b)[:16]} (有意改动门户外壳后重设漂移基准)']
-    wr(MANIFEST, json.dumps(man, ensure_ascii=False, indent=1))
-    print(f'已重基线 {P.rel(MANIFEST)}')
-    print(f'  shell.html  {old_shell} → {sha(shell_b)[:16]}  ({len(shell_b):,} B)')
-    print(f'  portal.html {old_portal} → {sha(portal_b)[:16]}  ({len(portal_b):,} B)')
-    print('  下一步: --check 应显示"全部与 manifest 一致 ✔"')
-    return 0
-
-
-def do_check() -> int:
-    if not MANIFEST.is_file():
-        print(f'[X] 缺 {P.rel(MANIFEST)}; 先跑 --extract')
-        return 1
-    man = json.loads(rd(MANIFEST))
-    bad = 0
-    for label, f, h in (('shell', SHELL, man['shell']['sha256']),
-                        ('governance_sources', GOV, man['governance_sources']['sha256'])):
-        got = sha(f.read_bytes()) if f.is_file() else '(缺)'
-        if got != h:
-            bad += 1
-        print(f'  [{"OK" if got == h else "漂移"}] {label:20s} {P.rel(f)}')
-    miss = 0
-    for tid, it in man['templates']['items'].items():
-        f = SRC / it['file']
-        got = sha(f.read_bytes()) if f.is_file() else '(缺)'
-        if got != it['sha256']:
-            bad += 1
-            miss += 1
-            if miss <= 3:
-                print(f'  [漂移] template {tid:34s} {got[:16]} != manifest {it["sha256"][:16]}')
-    if miss > 3:
-        print(f'  … 另有 {miss-3} 个模板漂移')
-    print(f'结论: {"全部与 manifest 一致 ✔" if not bad else f"{bad} 处漂移"}')
-    if PORTAL.is_file():
-        cur = sha(PORTAL.read_bytes())
-        print(f'  现有门户 sha256 {cur[:16]}  (manifest 期望 {man["expected_portal_sha256"][:16]})'
-              f'{"  ← 一致" if cur == man["expected_portal_sha256"] else "  ← 已变 (重算过/注入过)"}')
-    return 0 if not bad else 1
-
-
-if __name__ == '__main__':
-    # 控制台可能是 GBK (Windows 中文默认 936): 正文里的 ✔/✘ 编不出来会直接抛 UnicodeEncodeError
-    # (校验结论明明通过了却因为 print 崩掉, 退出码变成 1)。降级为 '?' 而不是崩。见 src/console.py。
-    from src import console
-    console.soft()
-    ap = argparse.ArgumentParser()
-    g = ap.add_mutually_exclusive_group()
-    g.add_argument('--extract', action='store_true', help='从现有 portal.html 拆出 portal_src/ (一次性)')
-    g.add_argument('--verify', action='store_true', help='装配到临时文件并与现有门户逐字节比对')
-    g.add_argument('--check', action='store_true', help='源件与 manifest 的 sha256 漂移检查')
-    g.add_argument('--rebaseline', action='store_true',
-                   help='**有意**改了 shell.html/portal.html 后, 把当前状态记为新的漂移基准 (只改 manifest 的两个 sha, 不碰文件)')
-    ap.add_argument('--no-claims', action='store_true', help='装配时不注入契约结论段')
-    a = ap.parse_args()
-    sys.exit(do_extract() if a.extract else do_verify() if a.verify else
-             do_rebaseline() if a.rebaseline else
-             do_check() if a.check else do_build(claims=not a.no_claims))

+ 0 - 79
app_frontEnd/app_frontEnd_guanlan/builders/portal_fix_anchors.py

@@ -1,79 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""门户内嵌文档 (iframe srcdoc) 的章节锚点修复 — 幂等注入器 (xzy 测试报告 2026-09-08 P1-3).
-
-病灶: 报告经 `ovf.srcdoc = 模板` 挂进 iframe。srcdoc 文档的**基地址继承父页**, 所以报告里的
-`<a href="#section-3">` 解析成 `http://127.0.0.1:28084/#section-3` —— 与 iframe 当前文档 (about:srcdoc)
-不同源同址, 浏览器判为**导航**, 于是 iframe 整个跳去门户首页 (门户路由 route() 认不出 section-3 → 回落 index)。
-修法: iframe 载入后 (srcdoc 同源, 父页可访问其 document) 拦截 `a[href^="#"]` 点击, 改为在报告文档内 scrollIntoView。
-不改 srcdoc 挂载方式 (换 blob: 会让相对资源失效; 加 <base> 又会把锚点解析回父页)。
-
-用法: python scripts/guanlan_portal_fix_anchors.py [--portal <path>] [--check]
-  --check 只报是否已注入 (exit 0 已注入 / 1 未注入); 不带则注入 (已注入时不重复写)。"""
-from __future__ import annotations
-import argparse, hashlib, os, shutil, sys, time
-from pathlib import Path
-try:
-    from app_common.app_common_guanlan.api import install_root as _install_root
-except ImportError:                     # 理论不可达;包结构异常时回退到按位置上跳
-    from pathlib import Path as _P
-    def _install_root(_f): return _P(_f).resolve().parents[4]
-
-ROOT = _install_root(__file__)     # 模块化后按标记找安装根(原 parents[1] 已不成立)
-MARK = "/*guanlan-anchor-fix-v2*/"
-MARK_ANY = "/*guanlan-anchor-fix-v"   # 旧版本先剥再注 (升级用)
-ANCHOR = "document.getElementById('ovf').srcdoc=t.content.textContent;"
-ANCHOR_ALT = "document.getElementById('ovf').srcdoc = t.content.textContent;"
-# v2: 用**即时**跳 (原生锚点行为), 不用 behavior:'smooth' —— 平滑滚动在隐藏/零尺寸视口里不执行
-# (2026-09-08 浏览器实测: smooth 后 scrollTop 仍 0, 即时跳到位 14173), 且长文档平滑滚动本身也不是文档阅读的习惯。
-PATCH = (MARK + "(function(){var f=document.getElementById('ovf');if(!f||f._afix)return;f._afix=1;"
-         "f.addEventListener('load',function(){try{var d=f.contentDocument;if(!d||d._afix)return;d._afix=1;"
-         "d.addEventListener('click',function(ev){var a=ev.target&&ev.target.closest?ev.target.closest('a[href^=\"#\"]'):null;if(!a)return;"
-         "ev.preventDefault();var id=decodeURIComponent((a.getAttribute('href')||'#').slice(1));"
-         "if(!id){d.defaultView.scrollTo(0,0);return}"
-         "var el=d.getElementById(id)||(d.getElementsByName(id)||[])[0];"
-         "if(el&&el.scrollIntoView)el.scrollIntoView({block:'start'})},true)}catch(e){}})})();")
-
-
-def portal_path(a=None) -> Path:
-    if a: return Path(a)
-    return Path(os.environ.get("GUANLAN_PORTAL") or (ROOT / "release/portal.html"))
-
-
-def _strip_old(s: str) -> tuple[str, int]:
-    """剥掉旧版本注入块 (从旧标记到该块结尾 `})();`), 用于升级; 返回 (新串, 剥掉几块)."""
-    n = 0
-    while True:
-        i = s.find(MARK_ANY)
-        if i < 0 or s[i:i + len(MARK)] == MARK: break
-        j = s.find("})();", i)
-        if j < 0: break
-        s = s[:i] + s[j + len("})();"):]; n += 1
-    return s, n
-
-
-def inject(p: Path) -> tuple[bool, str]:
-    s = p.read_text(encoding="utf-8", errors="strict")
-    if MARK in s: return False, "已注入 (幂等, 不重复写)"
-    s, n_old = _strip_old(s)
-    for a in (ANCHOR, ANCHOR_ALT):
-        if a in s:
-            bak = p.with_suffix(p.suffix + ".bak_anchor_" + time.strftime("%Y%m%d%H%M%S")); shutil.copy2(p, bak)
-            s2 = s.replace(a, a + PATCH, 1)
-            assert MARK in s2 and len(s2) > len(s), "注入后自检失败"
-            p.write_text(s2, encoding="utf-8", newline="")   # 保持 LF: Windows 上默认会把整份门户改成 CRLF
-            return True, f"已注入 ({p.name}, 备份 {bak.name}, 剥旧版 {n_old} 块, sha16 {hashlib.sha256(s2.encode()).hexdigest()[:16]})"
-    return False, "未找到挂载点 (门户结构变了: 找 ovf.srcdoc 赋值处) → 未改动"
-
-
-def main():
-    ap = argparse.ArgumentParser(); ap.add_argument("--portal"); ap.add_argument("--check", action="store_true"); a = ap.parse_args()
-    p = portal_path(a.portal)
-    if not p.exists(): print(f"门户不存在: {p}"); return 2
-    if a.check:
-        ok = MARK in p.read_text(encoding="utf-8", errors="replace"); print(("已注入" if ok else "未注入") + f": {p}"); return 0 if ok else 1
-    changed, msg = inject(p); print(msg); return 0 if (changed or "已注入" in msg) else 2
-
-
-if __name__ == "__main__":
-    sys.exit(main())

+ 0 - 60
app_frontEnd/app_frontEnd_guanlan/builders/portal_inject_claims.py

@@ -1,60 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""门户单文件 · 契约生成的结论段 (用户批 2026-09-06: 门户正文切到契约生成). 门户浅色壳由 Codex 侧生成 (本库生成器是深色版, 不可重生成),
-因此采用**就地幂等注入**: 在 <div class="pg" id="findings"> 正文末尾插入/替换 <section id="contract-claims" data-contract="…"> (由 derived/portal_claims.json 渲染).
-用法: python scripts/guanlan_portal_inject_claims.py <portal.html> [--out <path>] ; 幂等: 再跑只替换同 id 段. 出: 新文件 sha256."""
-import hashlib, html, json, re, sys
-from pathlib import Path
-
-try:
-    from app_common.app_common_guanlan.api import install_root as _install_root
-except ImportError:                     # 理论不可达;包结构异常时回退到按位置上跳
-    from pathlib import Path as _P
-    def _install_root(_f): return _P(_f).resolve().parents[4]
-
-ROOT = _install_root(__file__)     # 模块化后按标记找安装根(原 parents[1] 已不成立)
-PC = ROOT / "outputs/rudong/guanlan/derived/portal_claims.json"
-MARK_RE = re.compile(r'<section id="contract-claims"[^>]*>.*?</section><!--/contract-claims-->', re.S)
-
-
-DC = ROOT / "outputs/rudong/guanlan/derived/detail_cards.json"; CONTRACT = ROOT / "outputs/rudong/guanlan/facts_contract_v0.json"
-
-
-def section(pc: dict) -> str:
-    """契约正文段 (用户裁 2026-09-07 "门户正文全部切到契约生成"): ① 可交付级表 ② 候选/参考/INSUFFICIENT 按系统分组 (折叠) ③ 撤回清单 (带日期). 全部由契约派生物渲染, 禁手改."""
-    cs = pc["contract_sha256"]; e = html.escape
-    rows = "".join(f'<tr><td><code>{e(p["claim_id"])}</code></td><td>{e(p["verdict"])}</td><td>{e(p["text"])}</td><td>{e(", ".join(p.get("systems") or []) or "—")}</td><td>{e(str(p.get("window_days") or "—"))}</td></tr>' for p in pc["paragraphs"])
-    cards = json.loads(DC.read_text(encoding="utf-8")) if DC.is_file() else {"cards": []}
-    assert cards.get("contract_sha256", cs) == cs, "detail_cards 与 portal_claims 契约 sha 不一致"
-    rest = [c for c in cards["cards"] if c["verdict"] not in ("定论", "准定论·预警")]
-    by_sys = {}
-    for c in rest: by_sys.setdefault(", ".join(c.get("systems") or []) or "跨系统/口径", []).append(c)
-    groups = "".join(f'<details><summary>{e(k)} · {len(v)} 条 ({", ".join(f"{vv} {sum(1 for c in v if c["verdict"] == vv)}" for vv in ("候选", "参考", "INSUFFICIENT") if any(c["verdict"] == vv for c in v))})</summary><table><tr><th>claim</th><th>裁决</th><th>结论</th><th>缺项</th></tr>'
-                     + "".join(f'<tr><td><code>{e(c["claim_id"])}</code></td><td>{e(c["verdict"])}</td><td>{e(c["title"])}</td><td>{e(c.get("missing") or "—")}</td></tr>' for c in v) + '</table></details>' for k, v in sorted(by_sys.items(), key=lambda kv: -len(kv[1])))
-    con = json.loads(CONTRACT.read_text(encoding="utf-8")) if CONTRACT.is_file() else {"claims": []}
-    assert con.get("contract_sha256", cs) == cs, "契约文件与派生物 sha 不一致"
-    wd = [c for c in con["claims"] if c["verdict"] == "撤回"]
-    _t = lambda c: (c.get("display") or {}).get("title_zh_public") or c.get("title_public") or ""   # 内部契约 / 云端脱敏面孔 (claims_public.json) 两种形态
-    withdrawn = "".join(f'<li><code>{e(c["claim_id"])}</code> {e(str(c.get("verdict_raw") or ""))[:40]} — {e(_t(c)[:110])}</li>' for c in wd)
-    return (f'<section id="contract-claims" data-contract="{cs}" data-version="{pc["contract_version"]}"><div class="wrap"><div class="kicker">本场结论 · 契约生成</div>'
-            f'<h2>定论与准定论·预警 ({len(pc["paragraphs"])} 条)</h2><p class="sub">由结构化事实契约 {cs[:16]} 渲染 (共 {con.get("n_claims", "?")} 条); 数据、时间窗、结论只经契约更新, 本段禁手改 (release-manifest.content_contract)。</p>'
-            f'<div style="overflow:auto"><table><tr><th>claim</th><th>裁决</th><th>结论</th><th>系统</th><th>窗 (天)</th></tr>{rows}</table></div>'
-            f'<h2 style="margin-top:22px">候选 · 参考 · 证据不足 ({len(rest)} 条, 按系统)</h2>{groups}'
-            f'<h2 style="margin-top:22px">已撤回 ({len(wd)} 条)</h2><ul>{withdrawn}</ul></div></section><!--/contract-claims-->')
-
-
-def inject(src: Path, out: Path):
-    t = src.read_text(encoding="utf-8"); sec = section(json.loads(PC.read_text(encoding="utf-8")))
-    if MARK_RE.search(t): t2 = MARK_RE.sub(lambda m: sec, t, count=1)
-    else:
-        m = re.search(r'(<div class="pg" id="findings"[^>]*>)(.*?)(</div>\s*<div class="pg" id=")', t, flags=re.S)
-        assert m, "门户里找不到 findings 页块"
-        t2 = t[:m.end(2)] + sec + t[m.end(2):]
-    # newline= 必须给: Windows 上 write_text 会把整份门户的 \\n 变成 \\r\\n —— 20MB 文件整体改写,
-    # portal_sha256 指纹随之变化 (2026-09-11 修)。门户行尾约定 = LF。
-    out.write_text(t2, encoding="utf-8", newline=""); return hashlib.sha256(t2.encode("utf-8")).hexdigest()
-
-
-if __name__ == "__main__":
-    src = Path(sys.argv[1]); out = Path(sys.argv[sys.argv.index("--out") + 1]) if "--out" in sys.argv else src
-    print(inject(src, out))

+ 0 - 60
app_frontEnd/app_frontEnd_guanlan/pages/build.py

@@ -1,60 +0,0 @@
-# -*- coding: utf-8 -*-
-"""新前端页面装配 —— shell.html + app.css + app.js + 语言包, 按语言出一页。
-
-出页面之前先跑 lang.check(): 英文缺条目 / 含中文 / 占位符不一致 = **这里就 FAIL**,
-不会走到用户眼前变成一处中文残留。这是新旧架构最根本的差别。
-"""
-from __future__ import annotations
-
-import json
-import pathlib
-
-from .. import lang
-
-_HERE = pathlib.Path(__file__).resolve().parent          # pages/
-_ASSETS = _HERE.parent / 'assets'                        # 前端资源件 (js/css)
-
-
-def _read(name: str) -> str:
-    """资源件在 assets/,页面外壳在 pages/ —— 两处都找(P5 分目录后)。"""
-    for d in (_HERE, _ASSETS):
-        f = d / name
-        if f.is_file():
-            return f.read_text(encoding='utf-8')
-    raise FileNotFoundError(f'前端资源件缺失: {name}(pages/ 与 assets/ 都没有)')
-
-
-def render(which: str = 'zh', include_records=True) -> str:
-    if which not in lang.LANGS:
-        raise ValueError('未知语言 %r' % which)
-    bad = lang.check()
-    if bad:
-        raise RuntimeError('语言包未过闸, 拒绝出页:\n  ' + '\n  '.join(bad))
-    pack = lang.pack(which)
-    if which == 'zh':
-        # 显示层术语映射 (app_frontEnd/configs/terms/display_map.yaml): 界面标签在这里过一遍;
-        # 数据里的内部词由前端 displayText 用 __display_zh 同表处理 (数据与匹配逻辑一律不动)。
-        from ..terms import humanize, pairs
-        pack = {k: (humanize(v) if isinstance(v, str) else v) for k, v in pack.items()}
-        pack['__display_zh'] = [list(x) for x in pairs()]
-    if which == 'en' and include_records:
-        from src.windscada.en_text import STANDARD_TERMS
-        if include_records == 'terms':
-            pack['__record_en'] = dict(STANDARD_TERMS)
-        else:
-            from src.windscada.data_tpl_en import RECORD_EN
-            pack['__record_en'] = dict(RECORD_EN, **STANDARD_TERMS)
-    # `</script>` 出现在 JSON 里会提前闭合 script 标签; `<` 一律转义
-    packjs = json.dumps(pack, ensure_ascii=False).replace('<', '\\u003c')
-    return (_read('shell.html')
-            .replace('%LANG%', which)
-            .replace('%TITLE%', pack['app.title'])
-            .replace('%CSS%', _read('app.css'))
-            .replace('%LANGPACK%', packjs)
-            .replace('%CHARTS%', _read('charts.js'))
-            .replace('%JS%', _read('app.js')))
-
-
-if __name__ == '__main__':   # python -m app_frontEnd.app_frontEnd_guanlan.pages.build en > v2_en.html   (旧路径 `python -m src.windscada.ui.build` 经兼容壳照旧可用)
-    import sys
-    sys.stdout.write(render(sys.argv[1] if len(sys.argv) > 1 else 'zh'))

+ 0 - 29
app_frontEnd/app_frontEnd_guanlan/pages/classic.py

@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-r"""经典页模板(P5,2026-09-28:从 `app_backEnd/.../serve.py` 抽出)。
-
-抽出前后**逐字节一致**:本模块只读文件,不做任何替换/改写;占位符(`%CSS%` `%JS%` `%T%` `%SYS%`)
-仍由调用方(serve.py 的路由)替换。
-
-* `assets/classic_chart.js` —— 经典页的图表库与页面脚本(CHART_JS)
-* `pages/classic_fleet.html` / `classic_problem.html` / `classic_turbine.html` —— 三张经典页外壳
-"""
-from __future__ import annotations
-
-import pathlib
-
-_HERE = pathlib.Path(__file__).resolve().parent
-_ASSETS = _HERE.parent / 'assets'
-
-
-def _read(p: pathlib.Path) -> str:
-    """`newline=''` ⇒ 不做换行翻译(模板按字节保真,与抽出前一致)。"""
-    with open(p, encoding='utf-8', newline='') as f:
-        return f.read()
-
-
-CHART_JS = _read(_ASSETS / 'classic_chart.js')
-PAGE_FLEET = _read(_HERE / 'classic_fleet.html')
-PAGE_PROBLEM = _read(_HERE / 'classic_problem.html')
-PAGE_TURBINE = _read(_HERE / 'classic_turbine.html')
-
-__all__ = ['CHART_JS', 'PAGE_FLEET', 'PAGE_PROBLEM', 'PAGE_TURBINE']

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

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

+ 9 - 9
docs/src/系统设计说明_观澜_2.54.1.md → docs/src/系统设计说明_观澜_2.55.0.md

@@ -1,4 +1,4 @@
-# 系统设计说明 · 观澜 v2 风电场智能分析系统 · 版本 2.54.1
+# 系统设计说明 · 观澜 v2 风电场智能分析系统 · 版本 2.55.0
 
 ## 1 文档说明
 
@@ -6,19 +6,19 @@
 
 ### 1.1 目的与范围
 
-本文是"观澜 v2(风电场智能分析系统)"(海上风电场智能分析离线系统)的设计说明(下称"本系统"),面向版本 2.54.1;内容以样本风电场(下称"本场")为依据,样本场实测日期为 2026-09-22,说明本系统"由哪些部分组成、各部分怎么实现、数据从哪来到哪去、判据写在哪里、怎么验证、边界在哪里"。
+本文是"观澜 v2(风电场智能分析系统)"(海上风电场智能分析离线系统)的设计说明(下称"本系统"),面向版本 2.55.0;内容以样本风电场(下称"本场")为依据,样本场实测日期为 2026-09-22,说明本系统"由哪些部分组成、各部分怎么实现、数据从哪来到哪去、判据写在哪里、怎么验证、边界在哪里"。
 
 本文覆盖十五个设计面:总体架构与分层、目录结构与路径真源、数据接入与重算链、判级与算法、时间窗口径、服务与前端、本体与知识层、本机模型接入、运维控制台与重算编排、安装与服务化与版本管理、质量保证、安全与离线边界、可移植性与资源占用、多场适用性与换场迁移、已知边界与未实现。
 
-本文不重复需求条目本身(那是《需求分析_观澜_2.54.1.docx》的职责),也不重复操作步骤的逐步手册(那是 docs/重算操作手册_v0.1.md 与《使用说明书》v0.2(随包 docs/)的职责);本文只回答"设计上为什么这样、落在哪个文件的哪一处、用什么机器守卫保证它不漂移"。
+本文不重复需求条目本身(那是《需求分析_观澜_2.55.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.54.1.docx》的对应关系
+### 1.3 与《需求分析_观澜_2.55.0.docx》的对应关系
 
-需求分析写"要什么、为谁、优先级与验收门",设计说明写"怎么实现、落在哪、如何自证"。两文的章节对应关系如表 1-1 所示;与《数据要求说明_观澜_2.54.1.docx》的数据侧口径去向见 16.6 节。
+需求分析写"要什么、为谁、优先级与验收门",设计说明写"怎么实现、落在哪、如何自证"。两文的章节对应关系如表 1-1 所示;与《数据要求说明_观澜_2.55.0.docx》的数据侧口径去向见 16.6 节。
 
 | 本文章节 | 需求分析对应章 | 对应关系说明 |
 |---|---|---|
@@ -41,7 +41,7 @@
 
 ### 1.4 口径与依据
 
-本文所有数字来自仓库文件或命令的实跑输出,不采用估算与推测;查不到、未实现的,一律写"未取证"或"未实现",并在第 17 章汇总。本文中"本次实测"与各表"实测"列一律指样本场实测(2026-09-22):即在样本风电场的一套实例上的一次实跑,换场后这些数字会变、方法与口径不变。版本号的唯一真源是 src/version.py 的 VERSION 常量,本版为 2.54.1;打包文件名由 src/version.py 的 package_name() 给出,为 app_guanlang_v2.54.1.zip。
+本文所有数字来自仓库文件或命令的实跑输出,不采用估算与推测;查不到、未实现的,一律写"未取证"或"未实现",并在第 17 章汇总。本文中"本次实测"与各表"实测"列一律指样本场实测(2026-09-22):即在样本风电场的一套实例上的一次实跑,换场后这些数字会变、方法与口径不变。版本号的唯一真源是 src/version.py 的 VERSION 常量,本版为 2.55.0;打包文件名由 src/version.py 的 package_name() 给出,为 app_guanlang_v2.55.0.zip。
 
 本文遵循四条写作口径:含"窗"且确实指时间窗口的,一律写全"时间窗"(天气窗、作业窗、预览窗、观测窗属领域词,保持原样);影响的是风电机组时写"影响机组"或"影响机组数";使用英文简写时必须写成"中文(英文简写)"形式,如平均无故障间隔(MTBF)、平均停机间隔(MTBO)、单次停机时长(MDT);全文用简体中文与半角数字与单位。
 
@@ -839,7 +839,7 @@ Windows 安装入口是 install.bat 与 install.ps1,步骤号写死在输出
 
 ### 12.3 安装记录与版本三守卫
 
-安装记录 install-info.json 是"这台机器装的是哪一版"的唯一凭据,字段如表 12-3 所示。装完写、卸载时删(删掉等于这台机器回到"没装过")。需要如实指出:本机该文件记录的版本是 2.5.0(装于 2026-09-17),落后于当前代码版本 2.54.1,因此它正是"安装前检查会提示版本差异"的活样本;重装或升级后会随之更新。
+安装记录 install-info.json 是"这台机器装的是哪一版"的唯一凭据,字段如表 12-3 所示。装完写、卸载时删(删掉等于这台机器回到"没装过")。需要如实指出:本机该文件记录的版本是 2.5.0(装于 2026-09-17),落后于当前代码版本 2.55.0,因此它正是"安装前检查会提示版本差异"的活样本;重装或升级后会随之更新。
 
 | 字段 | 含义 |
 |---|---|
@@ -1200,7 +1200,7 @@ Linux 侧的适配程度如表 15-4 所示,全部为"已有实现但在本次
 
 ### 16.6 与同批交付文档的对应关系
 
-本章对应《需求分析_观澜_2.54.1.docx》第 11 章(场配置化、机组与机型可替换、数据源形态可适配、阈值按场标定、术语与单位可配、跨场统一口径、按场裁剪收资、换场验收检查表)与《数据要求说明_观澜_2.54.1.docx》第 12 章(通用必选与可选的判定规则、场配置字段对照、数据源形态适配、换场收资差异清单、按场裁剪步骤);本章给的是设计侧的落点、换场作业单与检查表,数据侧的收资口径与字段要求以数据要求说明为准,需求侧的验收项以需求分析为准。
+本章对应《需求分析_观澜_2.55.0.docx》第 11 章(场配置化、机组与机型可替换、数据源形态可适配、阈值按场标定、术语与单位可配、跨场统一口径、按场裁剪收资、换场验收检查表)与《数据要求说明_观澜_2.55.0.docx》第 12 章(通用必选与可选的判定规则、场配置字段对照、数据源形态适配、换场收资差异清单、按场裁剪步骤);本章给的是设计侧的落点、换场作业单与检查表,数据侧的收资口径与字段要求以数据要求说明为准,需求侧的验收项以需求分析为准。
 
 ***
 
@@ -1266,7 +1266,7 @@ Linux 侧的适配程度如表 15-4 所示,全部为"已有实现但在本次
 
 | 章节 | 来源文件 |
 |---|---|
-| 1 文档说明 | src/version.py(VERSION 与 HISTORY 的 2.54.1 条目)、docs/需求分析_观澜_2.54.1.docx、scripts/delivery_docs_figures.py |
+| 1 文档说明 | src/version.py(VERSION 与 HISTORY 的 2.55.0 条目)、docs/需求分析_观澜_2.55.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(路径约定与统一记录)、本次目录实测 |

+ 21 - 20
docs/src/需求分析_观澜_2.54.1.md → docs/src/需求分析_观澜_2.55.0.md

@@ -1,4 +1,4 @@
-# 需求分析 · 观澜 v2 风电场智能分析系统 · 版本 2.54.1
+# 需求分析 · 观澜 v2 风电场智能分析系统 · 版本 2.55.0
 
 ## 1 文档说明
 
@@ -6,7 +6,7 @@
 
 ### 1.1 目的
 
-本文是「观澜 v2(风电场智能分析系统)」的**需求分析文档**,回答四个问题:这套系统究竟要满足谁的什么需要;这些需要从哪来、经过哪些版本变成现在的样子;每一条需要对应什么功能、什么输入、什么输出、拿什么判据验收;以及哪些事本版明确不做。本文与同批交付的《系统设计说明_观澜_2.54.1.docx》《数据要求说明_观澜_2.54.1.docx》配套:本文讲「要什么、凭什么算做到了」,设计说明讲「怎么做的」,数据要求说明讲「要哪些数据、什么形态、什么单位」。
+本文是「观澜 v2(风电场智能分析系统)」的**需求分析文档**,回答四个问题:这套系统究竟要满足谁的什么需要;这些需要从哪来、经过哪些版本变成现在的样子;每一条需要对应什么功能、什么输入、什么输出、拿什么判据验收;以及哪些事本版明确不做。本文与同批交付的《系统设计说明_观澜_2.55.0.docx》《数据要求说明_观澜_2.55.0.docx》配套:本文讲「要什么、凭什么算做到了」,设计说明讲「怎么做的」,数据要求说明讲「要哪些数据、什么形态、什么单位」。
 
 本文的写作口径是**只写能取证的事实**:每个数字、每条结论都能指到仓库里的某个文件、某次实跑输出或某条版本记录;查不到、取不到的一律写明「未取证」或「未到位」,不做推测性补全。因此文中会出现少量「未取证」的说明句,那是刻意留下的诚实边界,不是遗漏。
 
@@ -18,19 +18,19 @@
 
 ### 1.3 版本对应关系
 
-系统版本号只有一个真源:仓库内的 src/version.py 的 VERSION 一行。本版 VERSION = 2.54.1,版本记录(HISTORY)共 93 条,人读的版本表由 scripts/version_log.py 从 HISTORY 生成到 docs/版本记录.md,并由 version_log.py --check 与 guanlan.py check 双重校验「记录表与代码一致」。
+系统版本号只有一个真源:仓库内的 src/version.py 的 VERSION 一行。本版 VERSION = 2.55.0,版本记录(HISTORY)共 94 条,人读的版本表由 scripts/version_log.py 从 HISTORY 生成到 docs/版本记录.md,并由 version_log.py --check 与 guanlan.py check 双重校验「记录表与代码一致」。
 
 表 1-1 版本与文档的对应关系
 
 | 项 | 值 | 取证方式 |
 |---|---|---|
-| 系统版本 | 2.54.1 | src/version.py 的 VERSION;guanlan.py check 实跑报「版本管理: 观澜 v2(风电场智能分析系统) v2.54.1」 |
-| 交付包名 | app_guanlang_v2.54.1.zip | src/version.py 的 package_name();同一条 check 输出 |
-| 本文版本 | 2.54.1(与系统版本同号) | 本文标题与 src/version.py 的 VERSION |
+| 系统版本 | 2.55.0 | src/version.py 的 VERSION;guanlan.py check 实跑报「版本管理: 观澜 v2(风电场智能分析系统) v2.55.0」 |
+| 交付包名 | app_guanlang_v2.55.0.zip | src/version.py 的 package_name();同一条 check 输出 |
+| 本文版本 | 2.55.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() |
-| 本次变化级别 | 小(patch,源码模块化重构推进 P1:公共层九个平台件实体迁移,旧路径留兼容转发壳;系统功能与运行为未变) | HISTORY 中 2.54.1 条目的 level 字段 |
+| 本次变化级别 | 小(patch,源码模块化重构推进 P1:公共层九个平台件实体迁移,旧路径留兼容转发壳;系统功能与运行为未变) | HISTORY 中 2.55.0 条目的 level 字段 |
 
 本版相对 2.10.0 的实质变化是**交付文档对外化**:三份交付文档(需求分析、系统设计说明、数据要求说明)全文不体现具体风电场(去标识化),并新增多风电场适用性一章;系统功能未变,属交付物修订。依据用户令原文:「修改三份文档,内容参考样本风电场,但不体现样本风电场,且具有不同风电场适用性」(引用时把样本场名按去标识化口径写成「样本风电场」)。
 
@@ -119,10 +119,11 @@
 
 ### 3.2 需求演进时间轴
 
-表 3-1 版本史与用户令要点(依据 src/version.py 的 HISTORY,共 93 条)
+表 3-1 版本史与用户令要点(依据 src/version.py 的 HISTORY,共 94 条)
 
 | 版本 | 日期 | 用户令要点 | 变化级别 |
 |---|---|---|---|
+| 2.55.0 | 2026-10-01 | 入口改为 Vue 工作台 · 门户退役 | 中(minor) |
 | 2.54.1 | 2026-10-01 | 删除旧页 design/terms+ 摘除 api 转发 ⇒ 全门禁保持通过 | 小(patch) |
 | 2.54.0 | 2026-10-01 | 删被替掉的旧页前端资源+ 台账与门禁随退役对齐 ⇒ 全门禁通过 | 中(minor) |
 | 2.53.2 | 2026-10-01 | detail 依赖台账按退役后的现场重写 ⇒ 全门禁恢复绿 | 小(patch) |
@@ -217,7 +218,7 @@
 | 0.4.0 | 2026-09-17 | 打包默认不含输入数据与产物与日志与临时文件;无窗口启动;统一配置与日志;页面归口;服务化 | 旧编号(legacy) |
 | 0.2.0 | 2026-09-01 | 含产物的旧交付基线,历史上用作「包内无生成端」那批产物的补救源 | 旧编号(legacy) |
 
-需求的演进如图 1-1 所示。该图依据 src/version.py 的 HISTORY 绘制,覆盖上表 93 条记录(即两级旧编号 0.4.0 与 0.2.0,以及从 2.5.0 起按规则递增的二十一个版本)。按级别统计,这二十一个版本里「中」级变化50条(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),「小」级变化41条(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 绘制,覆盖上表 94 条记录(即两级旧编号 0.4.0 与 0.2.0,以及从 2.5.0 起按规则递增的二十二个版本)。按级别统计,这二十二个版本里「中」级变化51条(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),「小」级变化41条(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)
 
@@ -236,7 +237,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.54.1」 |
+| 装成服务并在安装时检查版本 | win_service.py 以 ctypes 直连 SCM;service_main.py 与 systemd 单元;install-info.json 记录版本并比对 | check 实跑报「卸载入口在位 2 个」「版本管理 v2.55.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 条目;本版三份文档 |
@@ -452,7 +453,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.54.1.docx》第 8 章与第 12 章 |
+| NFR-12 | 收资清单按场裁剪:以通用收资模板为底,按本场的机型与数据源形态与专题范围裁剪,逐条写明必须、建议、可选或可替代以及「不收会怎样」 | 每一族都有必须性分级与缺件后果且可复核;换场时清单随场定义与机型变化,不是照抄样本场 | 本文第 11 章;《数据要求说明_观澜_2.55.0.docx》第 8 章与第 12 章 |
 
 ### 6.2 性能与耗时实测
 
@@ -488,7 +489,7 @@
 
 | 真源 | 管什么 | 守卫与实跑结果 |
 |---|---|---|
-| src/version.py | 名称、版本、版本规则、版本史、包名 | 版本记录一致性检查;check 实跑报 v2.54.1 且记录表与代码一致 |
+| src/version.py | 名称、版本、版本规则、版本史、包名 | 版本记录一致性检查;check 实跑报 v2.55.0 且记录表与代码一致 |
 | src/paths.py | 一切路径解析的基准与助手 | 配置审计器检查不手拼路径;实跑无不一致 |
 | configs 目录与登记表 | 端口、模型档、场配置、页面归口、配置登记 | 配置审计实跑已知缺口与白名单 14 条、提示 7 条 |
 | src/logfile.py | 日志目录、命名、行格式、保留策略 | 日志审计实跑无不一致、提示 87 条 |
@@ -499,7 +500,7 @@
 
 ### 7.1 数据族与功能映射
 
-系统的数据需求可以概括成一句话:**七类现场源件加一类共享机理资料,喂出九个产物仓,页面只读产物**。每个源类目录名就是摄入接口,改名等于换接口。数据族与功能的对应关系如下表;逐类的字段、单位、必须性与质量要求见同批交付的《数据要求说明_观澜_2.54.1.docx》。
+系统的数据需求可以概括成一句话:**七类现场源件加一类共享机理资料,喂出九个产物仓,页面只读产物**。每个源类目录名就是摄入接口,改名等于换接口。数据族与功能的对应关系如下表;逐类的字段、单位、必须性与质量要求见同批交付的《数据要求说明_观澜_2.55.0.docx》。
 
 表 7-1 数据族到功能的映射(样本场实测(2026-09-22))
 
@@ -532,7 +533,7 @@
 
 ### 7.3 与数据要求说明的分工
 
-本文只回答「要哪些数据、这些数据支撑什么功能」。数据的字段级要求(核心测点的名称、单位、必须性、缺失替代、对齐规则、质量门与核对锚点)以及面向现场的收资清单,写在《数据要求说明_观澜_2.54.1.docx》里。该文档按用户令要求与现场收资文件逐条对照,并对现场收资层面的已知缺失逐条如实记录,例如测风塔数据为零交付、故障录波只有 4 台、振动侧 handoff 正本缺失由观澜自算件顶上、远端机器未安装本机模型等。
+本文只回答「要哪些数据、这些数据支撑什么功能」。数据的字段级要求(核心测点的名称、单位、必须性、缺失替代、对齐规则、质量门与核对锚点)以及面向现场的收资清单,写在《数据要求说明_观澜_2.55.0.docx》里。该文档按用户令要求与现场收资文件逐条对照,并对现场收资层面的已知缺失逐条如实记录,例如测风塔数据为零交付、故障录波只有 4 台、振动侧 handoff 正本缺失由观澜自算件顶上、远端机器未安装本机模型等。
 
 ## 8 页面与信息架构需求
 
@@ -614,7 +615,7 @@
 | 运行环境与依赖 | 10 | Python 版本不低于 3.11(实测 3.12.10);九个第三方依赖逐个导入 | 全 OK |
 | 静态质量门 | 4 | 源码可编译 229 个文件;语言包 845 条成对;入口引用闭合 48 条;入口脚本编码守则 | 全 OK |
 | 子进程口径 | 3 | 捕获输出可用;无窗口启动输出进日志;无窗口位已设,标志位 0x8000200 | 全 OK |
-| 版本与卸载入口 | 2 | 版本管理与记录表一致(v2.54.1,包名 app_guanlang_v2.54.1.zip);卸载入口两个都在位 | 全 OK |
+| 版本与卸载入口 | 2 | 版本管理与记录表一致(v2.55.0,包名 app_guanlang_v2.55.0.zip);卸载入口两个都在位 | 全 OK |
 | 制品与台账审计 | 5 | 反向呼应 3,548 件成立;页面归口检查 21 项;配置统一;日志统一;输入数据放置体检 26,503 件结构合规 | 全 OK |
 | 产物与发布件在位 | 10 | 标准仓、本体对象库、findings、事实契约、门户、仿真合页服务与资料包、三维资产、仿真回放资产、治理清单交付件 | 全 OK |
 | 原始件目录 | 1 | 原始件目录存在(无数据时此项不影响页面) | 全 OK |
@@ -740,7 +741,7 @@
 
 本章有两个词要先定义。「本场」指**当前被选中的场**,由配置选定,不必然等于本文取数的样本风电场;「换场」指从一场切到另一场运行的整套动作(改配置、放数据、重跑、按检查表复验),不是把两场的数据混在一棵树里。本章沿用的占位符与附录 A 一致:`<场>`、`<场站>`、`<机型>` 按实际风电场替换。
 
-本章需求与《数据要求说明_观澜_2.54.1.docx》《系统设计说明_观澜_2.54.1.docx》配套使用:本章讲「换场要满足什么」,数据要求说明讲「要向新场收哪些数据、哪些可以裁剪」,《设计说明》讲「配置在哪一层生效、哪些参数属于场相关层」。
+本章需求与《数据要求说明_观澜_2.55.0.docx》《系统设计说明_观澜_2.55.0.docx》配套使用:本章讲「换场要满足什么」,数据要求说明讲「要向新场收哪些数据、哪些可以裁剪」,《设计说明》讲「配置在哪一层生效、哪些参数属于场相关层」。
 
 取证边界要如实写一句:FR-44 至 FR-51 与 NFR-12 有既有的场配置层、摄入层与场级字段语义登记作依据;FR-52 的跨场横向对比是本次新增的口径要求,其实跑记录与机器守卫随后续版本补齐——本文不谎称已经通过。
 
@@ -819,7 +820,7 @@
 
 | 编号 | 需求 | 判据 | 依据文件 |
 |---|---|---|---|
-| NFR-12 | 收资清单按场裁剪:以通用收资模板为底,按本场的机型、数据源形态与专题范围裁剪,逐条写明必须、建议、可选或可替代以及「不收会怎样」 | 每一族都有必须性分级与缺件后果且可复核;换场时清单随场定义与机型变化,不是照抄样本场 | 《数据要求说明_观澜_2.54.1.docx》第 8 章与第 12 章;docs/输入数据放置指导_v0.1.md |
+| NFR-12 | 收资清单按场裁剪:以通用收资模板为底,按本场的机型、数据源形态与专题范围裁剪,逐条写明必须、建议、可选或可替代以及「不收会怎样」 | 每一族都有必须性分级与缺件后果且可复核;换场时清单随场定义与机型变化,不是照抄样本场 | 《数据要求说明_观澜_2.55.0.docx》第 8 章与第 12 章;docs/输入数据放置指导_v0.1.md |
 
 ### 11.9 换场验收需求
 
@@ -962,8 +963,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.54.1.md |
-| 场无关引擎与场相关参数的分层与换场作业单 | docs/src/系统设计说明_观澜_2.54.1.md |
+| 按场裁剪的收资清单与必须性分级 | docs/src/数据要求说明_观澜_2.55.0.md |
+| 场无关引擎与场相关参数的分层与换场作业单 | docs/src/系统设计说明_观澜_2.55.0.md |
 
 表中 `<场>`、`<场站>`、`<机型>` 为占位符,按实际风电场替换。
 
@@ -1006,4 +1007,4 @@
 | 换场 | — | 从一场切到另一场运行的整套动作:改配置、放数据、重跑、按检查表复验 | 本文 §11.9 |
 | 缺族降级 | — | 某族数据缺失时按缺件如实标注并降低该族功能,不造数、不用他场数字顶替 | 本文 FR-49 |
 | 按场标定 | — | 把阈值、时间窗锚点、限电窗、温度档带宽等场相关参数按本场取值 | 本文 FR-50 |
-| 通用收资模板 | — | 与场无关的收资条目底稿,按场裁剪后作为现场收资清单 | docs/src/数据要求说明_观澜_2.54.1.md |
+| 通用收资模板 | — | 与场无关的收资条目底稿,按场裁剪后作为现场收资清单 | docs/src/数据要求说明_观澜_2.55.0.md |

BIN
docs/数据要求说明_观澜_2.54.1.docx


BIN
docs/数据要求说明_观澜_2.55.0.docx


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

@@ -7,7 +7,7 @@
 <!-- VERSION-LOG:BEGIN -->
 ## 版本号规则(用户令 2026-09-17)
 
-编号 **`v<大版本号>.<中版本号>.<小版本号>`**,例如 `v2.54.1`。
+编号 **`v<大版本号>.<中版本号>.<小版本号>`**,例如 `v2.55.0`。
 
 | 级别 | 含义 |
 |---|---|
@@ -17,12 +17,13 @@
 
 **怎么改**:改动落在"解决方案/架构/核心功能" → 大 +1(中/小归 0);落在"非核心功能的新增/减少/修改" → 中 +1(小归 0);只是"消缺完善" → 小 +1;一次发布混了几类,取最高那一级
 
-**打包文件名** = `app_guanlang_v2.54.1.zip`(规则:`app_guanlang_v<版本号>.zip`)。
+**打包文件名** = `app_guanlang_v2.55.0.zip`(规则:`app_guanlang_v<版本号>.zip`)。
 
-版本号只写在 `src/version.py` 一行(`VERSION = '2.54.1'`);打包器、安装脚本、安装记录 `install-info.json`、自检都读它,不另设真源。
+版本号只写在 `src/version.py` 一行(`VERSION = '2.55.0'`);打包器、安装脚本、安装记录 `install-info.json`、自检都读它,不另设真源。
 
 | 版本 | 日期 | 级别 | 主要变更 | 说明 |
 |---|---|---|---|---|
+| `v2.55.0` | 2026-10-01 | 中 | 入口改为 Vue 工作台 · 门户退役(连带:契约期望刷新 · 重算链去门户步 · 删门户构建器与旧页装配件) | ① 用户令(2026-10-01):**入口改为 Vue 工作台(/ → Vue),门户退役**。落地:网关 `PortalController` 的文件优先目标从 `release/portal.html` 改为 **`release/web/index.html`**(Vue 壳),并把壳里的**相对**资源路径 `./assets/…` 改写为**已路由**的 `/web/assets/…`(省掉新增路由与 YAML 改动 —— 此前多次踩过 yml 重复键与谓词怪癖);响应头 `X-Portal-Source: vue`。实测:`/` 与 `/index.html` → 200 · 413 B(标题「观澜 · 风电数据分析与决策支持」)、`/web/assets/index-*.js` → 200(195,415 B)、`/api/fleet` 与 `/algorithm` 照常。② 连带项(逐项处理):a) 契约 `/` 的**期望标题刷新**为现值并加 `entry_note` 说明(复核回到 **40/40**);b) 重算链 `app_ETL/.../rebuild_all.py` 去掉「⑧b 重装门户」步(门户不再随数据重建);c) 删除门户构建器与旧页装配件:`scripts/portal_build.py`、`scripts/guanlan_portal_{fix_anchors,inject_claims}.py`、`app_frontEnd_guanlan/builders/*`(3 件 + `__init__`)、`pages/build.py`、`pages/classic.py`,并摘除 `app_frontEnd_guanlan/api.py` 中相应转发名。③ 仍未动(需各自专项):`pages/snapshot.py`(产物生成器 `windscada_overview_build.py` 依赖)、`lang.py`(工具链 `cli.py` 与审计 `guanlan_baseline_manifest`/`data_tpl_en` 依赖)、`serve.py` 与 `scripts/windscada_serve.py`(旧业务后端,`/api` 已由 Java 承接)。④ 复验:**HTTP 契约复核 40/40**、**全门禁通过**。 |
 | `v2.54.1` | 2026-10-01 | 小 | 删除旧页 design/terms(含兼容壳)+ 摘除 api 转发 ⇒ 全门禁保持通过 | ① 删除批次续:删掉旧页设计系统与显示层术语映射 —— `app_frontEnd_guanlan/design.py`(233 行)、`terms.py`(37 行) 及两个兼容壳 `src/windscada/design.py`、`src/windscada/terms.py`。② 连带同步(门禁实逮):模块边界审计报 R5 两条 —— `app_frontEnd.api.design/terms 的转发目标不存在`;已在 `app_frontEnd_guanlan/api.py` 的 `_TARGETS` 里摘除这两个转发名并写明理由。③ 复验:模块边界审计 `[OK] 结构齐全 · 只经 api 调用 · 依赖方向合规 · 公共层纯净 · 接口目标全在位`,**全门禁通过**。④ 删除前的影响面测绘(本轮做的功课,值得记档):`pages/build.py` 被**门户构建器** `scripts/portal_build.py` 依赖、`pages/snapshot.py` 被**产物生成器**`scripts/windscada_overview_build.py` 依赖、`lang.py` 被工具链与审计依赖 ⇒ 这三项**不可随旧页一并删**,否则门户无法重建、产物无法重算;要删它们得先"冻门户/改生成端/改审计口径",属独立一步。⑤ 因此本轮只删**确认无依赖**的 design/terms 两项,其余按上一条结论留待专项处理。 |
 | `v2.54.0` | 2026-10-01 | 中 | 删被替掉的旧页前端资源(3,522 行)+ 台账与门禁随退役对齐 ⇒ 全门禁通过 | ① 删除批次(用户令"对拍通过后再删 Python 源码"):删掉旧页三个前端资源 —— `assets/app.js`(1,092 行)、`assets/charts.js`(268 行)、`assets/classic_chart.js`(2,162 行),合计 3,522 行;删除前置检查显示模块外对这三个文件的引用为 0。② 门禁随退役对齐(这是本轮真正的工作量,逐项实逮):a) 中文术语门禁原审**旧页语言包** `app_frontEnd_guanlan/lang.py` 与 `assets/app.js` ⇒ 改指现行前端语言包`app_frontEnd/web/src/i18n/zh.ts`(实跑:扫 4 文件 / 4,063 条中文串 / 待处理 0)。b) `detail_deps` 台账原先是**单文件页面模型**(旧页 app.js 同含 TABS 与各 fetch)⇒ 改为**多文件汇总**读取(`web/src/lib/tabs.ts` + `api/contract.ts` + `App.vue` + `views/*`)。c) 该台账记的是**旧页 /detail** 的"页签 ← 接口 ← 产物";旧页退役后 Vue 侧只取 `/api/fleet` 与 `/api/facts`,台账里其余 14 条接口 Vue 不取 ⇒ 页面侧核验必然核不上。故把**页面侧核验登记退役**(打印一行说明),接口口径改由两处更强的把关承担:HTTP 契约复核(40/40)与 Java↔Python 裁判(Java 范围 17/17);**产物/生成端/缺口核验照旧执行**。③ 结果:`detail_deps --write/--check` **rc=0**,**全门禁通过**。④ 如实记录两处自我更正:删资源那次提交我最初写"全门禁绿"与事实不符(当时 `detail_deps` 因仍引用已删文件而红),已 `git commit --amend` 更正;其间若干次"改了却仍红"的尝试(APPJS 单文件改指、期望对齐等)也都在提交信息里留痕。 |
 | `v2.53.2` | 2026-10-01 | 小 | detail 依赖台账按退役后的现场重写 ⇒ 全门禁恢复绿 | ① 上一版提交后门禁红一项:`scripts/detail_deps.py --check` 报"文档与现场不一致"。这份台账记的是"页面(页签) ← 取数接口(在线 /api/*) ← 产物件 → 来源 + 生成端 + 缺口处置"的依赖链;我把 detail 服务按令退役(`retire_detail: true`)后,现场与台账自然对不上。② 处置:按该工具自身的提示跑 `detail_deps --write` 重写台账(清单段;实测段随服务状态变、不参与比对)——重写后 `--check` 报 **[OK] 文档与现场一致**,**全门禁通过**。③ 这说明:退役动作要在**登记类台账**里同步反映,否则门禁会(正确地)报红;本轮把这条补上。 |
@@ -119,8 +120,8 @@
 
 ## 本次交付
 
-* 版本:`v2.54.1`(观澜·如东样板 v2)
-* 交付包:`app_guanlang_v2.54.1.zip`
+* 版本:`v2.55.0`(观澜·如东样板 v2)
+* 交付包:`app_guanlang_v2.55.0.zip`
 * 服务名:`guanlan`(Windows 服务 / systemd 单元)
 * 安装 / 卸载:`install.bat`、`install.sh` / `uninstall.bat`、`uninstall.sh`
 <!-- VERSION-LOG:END -->

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


BIN
docs/需求分析_观澜_2.54.1.docx → docs/需求分析_观澜_2.55.0.docx


+ 0 - 27
scripts/guanlan_portal_fix_anchors.py

@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-r"""兼容转发(P5,2026-09-28):实现已迁到 `app_frontEnd/app_frontEnd_guanlan/builders/portal_fix_anchors.py`。**本文件不得新增逻辑**。
-
-入口路径(`scripts/guanlan_portal_fix_anchors.py`)被链、审计器、打包清单与文档引用,由此保持不变:公开名同面转发 + CLI 转 `main()`;
-没有 `main()`(argparse 写在 `__main__` 守卫里的)则 `runpy` 按原样执行。
-"""
-from __future__ import annotations
-
-import pathlib
-import runpy
-import sys
-
-ROOT = pathlib.Path(__file__).resolve().parents[1]                       # scripts/ 的上一层 = 安装根
-sys.path.insert(0, str(ROOT))
-
-IMPL = ROOT / 'app_frontEnd/app_frontEnd_guanlan' / 'builders/portal_fix_anchors.py'
-
-from app_frontEnd.app_frontEnd_guanlan.builders import portal_fix_anchors as _impl                                        # noqa: E402
-
-for _k, _v in vars(_impl).items():                                       # 公开名同面转发
-    if not _k.startswith('__'):
-        globals().setdefault(_k, _v)
-
-if __name__ == '__main__':
-    if hasattr(_impl, 'main'):
-        raise SystemExit(_impl.main())
-    runpy.run_path(str(IMPL), run_name='__main__')

+ 0 - 27
scripts/guanlan_portal_inject_claims.py

@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-r"""兼容转发(P5,2026-09-28):实现已迁到 `app_frontEnd/app_frontEnd_guanlan/builders/portal_inject_claims.py`。**本文件不得新增逻辑**。
-
-入口路径(`scripts/guanlan_portal_inject_claims.py`)被链、审计器、打包清单与文档引用,由此保持不变:公开名同面转发 + CLI 转 `main()`;
-没有 `main()`(argparse 写在 `__main__` 守卫里的)则 `runpy` 按原样执行。
-"""
-from __future__ import annotations
-
-import pathlib
-import runpy
-import sys
-
-ROOT = pathlib.Path(__file__).resolve().parents[1]                       # scripts/ 的上一层 = 安装根
-sys.path.insert(0, str(ROOT))
-
-IMPL = ROOT / 'app_frontEnd/app_frontEnd_guanlan' / 'builders/portal_inject_claims.py'
-
-from app_frontEnd.app_frontEnd_guanlan.builders import portal_inject_claims as _impl                                        # noqa: E402
-
-for _k, _v in vars(_impl).items():                                       # 公开名同面转发
-    if not _k.startswith('__'):
-        globals().setdefault(_k, _v)
-
-if __name__ == '__main__':
-    if hasattr(_impl, 'main'):
-        raise SystemExit(_impl.main())
-    runpy.run_path(str(IMPL), run_name='__main__')

+ 0 - 27
scripts/portal_build.py

@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-r"""兼容转发(P5,2026-09-28):实现已迁到 `app_frontEnd/app_frontEnd_guanlan/builders/portal_build.py`。**本文件不得新增逻辑**。
-
-入口路径(`scripts/portal_build.py`)被链、审计器、打包清单与文档引用,由此保持不变:公开名同面转发 + CLI 转 `main()`;
-没有 `main()`(argparse 写在 `__main__` 守卫里的)则 `runpy` 按原样执行。
-"""
-from __future__ import annotations
-
-import pathlib
-import runpy
-import sys
-
-ROOT = pathlib.Path(__file__).resolve().parents[1]                       # scripts/ 的上一层 = 安装根
-sys.path.insert(0, str(ROOT))
-
-IMPL = ROOT / 'app_frontEnd/app_frontEnd_guanlan' / 'builders/portal_build.py'
-
-from app_frontEnd.app_frontEnd_guanlan.builders import portal_build as _impl                                        # noqa: E402
-
-for _k, _v in vars(_impl).items():                                       # 公开名同面转发
-    if not _k.startswith('__'):
-        globals().setdefault(_k, _v)
-
-if __name__ == '__main__':
-    if hasattr(_impl, 'main'):
-        raise SystemExit(_impl.main())
-    runpy.run_path(str(IMPL), run_name='__main__')