# -*- coding: utf-8 -*- """版本与安装信息的**唯一真源**(2026-09-17 用户令 1:安装时检查已装版本、提示差异、问是否重装)。 为什么单开一个模块:以前版本号散在三处(`scripts/pack_dist.py` 的 `VERSION`、README 抬头、说明书标题), 改一处忘一处就会出现"包说 0.4.0、安装记录说 0.2.0"这种对不上的事。现在: src/version.py ← 唯一真源(本文件) scripts/pack_dist.py ← 读它写进 dist-manifest.json 与默认包名 install.ps1 / install.sh ← 读它写进 <安装目录>/install-info.json,并与已装的比对 guanlan.py / 各文档 ← 读它(文档里的人工版本号以它为准) `install-info.json` 落在安装根本身(不在 run/ 里):它是**交付物级别的安装记录**, 卸载/重装/拷机都要跟着走,所以和 `configs/`、`dist-manifest.json` 同级。 """ from __future__ import annotations try: from ._root import install_root as _install_root except ImportError: # 直接当脚本跑(python <本文件>)时没有包上下文 from _root import install_root as _install_root import datetime as dt import json import pathlib NAME = '观澜·如东样板 v2' VERSION = '2.85.1' # ★ 版本只改这里 EDITION = 'offline-single-package' PACKAGE_STEM = 'app_guanlang' # 交付包文件名前缀(用户令 2026-09-17) INSTALL_INFO = 'install-info.json' # 相对安装根 SERVICE_NAME = 'guanlan' # Windows 服务名 / systemd 单元名(用户令 1) SERVICE_DISPLAY = '观澜·如东样板 v2 (Guanlan Wind Asset Intelligence)' # ── 版本号规则(用户令 2026-09-17)──────────────────────────────────────────────── # 编号: v<大版本号>.<中版本号>.<小版本号> 例: v2.5.0 # 定义: 大版本号 —— 系统解决方案、架构或核心功能改变 # 中版本号 —— 非核心功能新增、减少、修改 # 小版本号 —— 消缺完善 # 包名: 打包文件名 = app_guanlang_v<版本号>.zip 例: app_guanlang_v2.5.0.zip # 用法: 用户/发布者定"这次算哪一级"→ VERSION 改一行 → 打包器与安装脚本自动跟着变 # (级别判据见 BUMP_RULE;一次发布混了几类就取最高那一级) RULE = 'v<大版本号>.<中版本号>.<小版本号>' LEVEL_MEANING = { 'major': '大版本号 —— 系统解决方案、架构或核心功能改变', 'minor': '中版本号 —— 非核心功能新增、减少、修改', 'patch': '小版本号 —— 消缺完善', } BUMP_RULE = ('改动落在"解决方案/架构/核心功能" → 大 +1(中/小归 0);' '落在"非核心功能的新增/减少/修改" → 中 +1(小归 0);' '只是"消缺完善" → 小 +1;一次发布混了几类,取最高那一级') # 版本记录(人读的那份由 scripts/version_log.py 生成到 docs/版本记录.md;这里只有事实,不重复描述) # level: major/minor/patch 表示这一版**相对上一版**是哪一级变化;legacy 表示该版用的是 # 旧编号体系(0.x,未按本规则),仅作历史对账用。 HISTORY: tuple[dict, ...] = ( dict(version='2.85.1', date='2026-10-05', level='patch', title='措辞政策定案:以实时接口(当前算法)用词为准;3 条差异改判为「基线旧快照用词」', note='承接上一轮"建议第 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 ✓)。'), dict(version='2.85.0', date='2026-10-05', level='minor', title='清产物后给友好空态(不暴露 HTTP 500)', note='用户令:清除产物后相应页面不显示数据,**提示要友好(不要报 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` ✓。'), dict(version='2.84.1', date='2026-10-05', level='patch', title='清产物/重算后工作台自动重取数(并加「刷新数据」);实测空数据各页显示空态', note='用户要求:**「清除产物」完成后,工作台相应功能(总览/电量算账/部件问题/振动融合分析/发电性能/故障统计)不应再有数据展示**。**实逮的问题**:这些页的数据来自 `/api/fleet`(按窗实时 ✓),但清除产物是在 `/ops` 控制台里点的 ⇒ **Vue 应用不会自动重新取数** ⇒ 页面仍显示清除前的数据。**修**:① `System.vue` 在 `/ops` iframe `load` 后(同源 ✓ 可直接访问其文档)监听 `#b_off`(清除产物)/`#b_rebuild`(执行重算)/`#b_start`/`#b_stop`/`#b_restart` 的点击,动作后 **4 秒**触发 `refresh`;② 重算块上方加**「刷新数据」**按钮(手动兜底);③ `Workbench` 透传、`App` 新增 `reload()` —— **清空页签级缓存 `apis`/`curves` 后重新 `load()` 与 `ensureTabApis(当前页签)`**。**验证(用空数据模拟"清完产物",不做破坏性操作)**:把 `/api/fleet` 返回置为空对象后,六个页签逐一渲染 ⇒ `overview elements=113 · energy elements=5(正文为「电量算账 无数据」)· component 21 · vibration 82 · generation 9 · fault 43`,**全部 `errs=0`(不崩)** ✓ —— 即清除后重取数时,各页会如实呈现"无数据/占位符"状态 ✓。'), dict(version='2.84.0', date='2026-10-05', level='minor', title='修「清除产物 404」:网关加 Path=/ops/api/** 专用路由(并实逮网关一直未真正重启)', note='用户反馈「系统维护」内嵌的 /ops 控制台里点「清除产物」报 **404 操作被拒绝**。**排查链**:该按钮调 **`/ops/api/<动作>`**(该页 API 前缀 `/ops/api/`);**直连 Python 网关 28086** ⇒ `/ops/api/state` **200 · 1,155 B**(正常);**经 Java 网关 28084** ⇒ **404**,响应体是 **Spring 的 `{"timestamp":…,"status":404}`** ⇒ 说明**没有路由匹配**、请求没到 28086。**实逮根因(两个叠加)**:① 路由表里 `ops → 28086` 的谓词是 `Path=/ops/**` + `Path=/ops`,但运行中的进程用的仍是**旧配置** ⇒ **网关根本没有真正重启**(旧 pid 50588 一直存活;此前几轮 `sc stop/start` 的回显都是空的、实际未生效);② 为稳妥起见还在路由表**最前**加了一条专用路由 **`ops-api` `Path=/ops/api/**` → 28086**。**修法**:改 classpath `application.yml` ⇒ **先 `taskkill` 旧 pid、再 `sc stop/start guanlan`、轮询确认端口换新 pid** ⇒ 实测 **旧 pid 50588 → 新 pid 53904** ✓,随后 `/ops/api/state` 经 28084 ⇒ **200 · 1,155 B** ✓(`/ops` 仍 200 ✓)。**判据**:`/ops/api/state` 经网关必须 200(直连本就是 200)✓ —— 达标。**教训(已记入)**:改配置后必须**确认进程换新 pid** 才算重启成功,回显为空不能当作已重启。'), dict(version='2.83.3', date='2026-10-05', level='patch', title='修「系统维护」数据重算按钮消失:内嵌改为直接 iframe src=/ops', note='用户反馈「系统维护」里的「清除产物」「数据重算」按钮又没了。**根因**:这两个按钮位于**内嵌的 `/ops` 运维控制台**内;该块此前用的是 Blob-URL iframe(与「振动 CMS」同一批尝试),在你的浏览器上不显示。而 `/ops` 的 `X-Frame-Options` 早在 **2.81.4** 已放开(网关 `frameOptions().disable()`,实测响应头无 DENY ✓)。**修**:把该块改回**最朴素、与 v2.9.2 门户同款**的写法 —— 直接 `