Przeglądaj źródła

2.14.0 分别打包: pack_dist --module(四模块各自成包, 真实依赖闭包=全模块) + MODULE_PACKAGE.json 标记 + 模块 install/run/uninstall 双模 + app_common bootstrap.py(独立装/检/跑/卸) + --verify --module; 根 VERSION 镜像纳入版本门; 文档与版本表同步

zhouyang.xie 1 tydzień temu
rodzic
commit
899d17147f
54 zmienionych plików z 1048 dodań i 170 usunięć
  1. 1 1
      VERSION
  2. 15 3
      app_ETL/install.bat
  3. 8 3
      app_ETL/install.sh
  4. 15 3
      app_ETL/run.bat
  5. 9 5
      app_ETL/run.sh
  6. 13 3
      app_ETL/uninstall.bat
  7. 7 2
      app_ETL/uninstall.sh
  8. 15 3
      app_algorithmModel/install.bat
  9. 8 3
      app_algorithmModel/install.sh
  10. 15 3
      app_algorithmModel/run.bat
  11. 9 5
      app_algorithmModel/run.sh
  12. 13 3
      app_algorithmModel/uninstall.bat
  13. 7 2
      app_algorithmModel/uninstall.sh
  14. 15 3
      app_backEnd/install.bat
  15. 8 3
      app_backEnd/install.sh
  16. 15 3
      app_backEnd/run.bat
  17. 9 5
      app_backEnd/run.sh
  18. 13 3
      app_backEnd/uninstall.bat
  19. 7 2
      app_backEnd/uninstall.sh
  20. 321 0
      app_common/app_common_guanlan/bootstrap.py
  21. 4 1
      app_common/app_common_guanlan/version.py
  22. 15 3
      app_common/install.bat
  23. 8 3
      app_common/install.sh
  24. 15 3
      app_common/run.bat
  25. 9 5
      app_common/run.sh
  26. 13 3
      app_common/uninstall.bat
  27. 7 2
      app_common/uninstall.sh
  28. 15 3
      app_frontEnd/install.bat
  29. 8 3
      app_frontEnd/install.sh
  30. 15 3
      app_frontEnd/run.bat
  31. 9 5
      app_frontEnd/run.sh
  32. 13 3
      app_frontEnd/uninstall.bat
  33. 7 2
      app_frontEnd/uninstall.sh
  34. 15 3
      app_ontology/install.bat
  35. 8 3
      app_ontology/install.sh
  36. 15 3
      app_ontology/run.bat
  37. 9 5
      app_ontology/run.sh
  38. 13 3
      app_ontology/uninstall.bat
  39. 7 2
      app_ontology/uninstall.sh
  40. 10 0
      app_qualityGate/app_qualityGate_guanlan/docs/version_log.py
  41. 206 0
      app_qualityGate/app_qualityGate_guanlan/pack/pack_dist.py
  42. 15 3
      app_qualityGate/install.bat
  43. 8 3
      app_qualityGate/install.sh
  44. 15 3
      app_qualityGate/run.bat
  45. 9 5
      app_qualityGate/run.sh
  46. 13 3
      app_qualityGate/uninstall.bat
  47. 7 2
      app_qualityGate/uninstall.sh
  48. 1 1
      docs/src/数据要求说明_观澜_2.14.0.md
  49. 9 9
      docs/src/系统设计说明_观澜_2.14.0.md
  50. 21 20
      docs/src/需求分析_观澜_2.14.0.md
  51. BIN
      docs/数据要求说明_观澜_2.14.0.docx
  52. 6 5
      docs/版本记录.md
  53. BIN
      docs/系统设计说明_观澜_2.14.0.docx
  54. BIN
      docs/需求分析_观澜_2.14.0.docx

+ 1 - 1
VERSION

@@ -1 +1 @@
-0.2.0
+2.14.0

+ 15 - 3
app_ETL/install.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified installer (single-package delivery).
-rem Module: app_ETL -- see ..\README.MD for install / run / uninstall notes.
-call "%~dp0..\install.bat" %*
+rem Guanlan module entry (P9 dual-mode). Module: app_ETL
+rem  A) full package (install root present) -> call the unified installer;
+rem  B) standalone module package         -> bootstrap a private .venv for this module.
+set "HERE=%~dp0"
+if exist "%HERE%..\install.ps1" call "%HERE%..\install.bat" %*
+if exist "%HERE%..\install.ps1" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" install --module app_ETL %*
+exit /b %errorlevel%

+ 8 - 3
app_ETL/install.sh

@@ -1,4 +1,9 @@
 #!/bin/sh
-# 观澜 · 模块安装入口(P9):调用安装根统一安装器(单包交付,一套安装)。
-# 模块: app_ETL —— 安装/运行说明见 ../README.MD
-exec sh "$(dirname "$0")/../install.sh" "$@"
+# 观澜 · 模块安装入口(P9 双模)。模块: app_ETL
+#   A) 总包(有安装根 install.sh)→ 走统一安装器;B) 独立模块包 → bootstrap 自建 .venv。
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/install.sh" ] && exec sh "$ROOT/install.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3(先装 Python 3.10+)"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" install --module app_ETL "$@"

+ 15 - 3
app_ETL/run.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): start the whole system via the install-root launcher.
-rem Module: app_ETL -- see ..\README.MD
-call "%~dp0..\start.bat" %*
+rem Guanlan module run entry (P9 dual-mode). Module: app_ETL
+rem  A) full package (start.bat present) -> start the whole system;
+rem  B) standalone module package       -> run this module's entry (bootstrap run).
+set "HERE=%~dp0"
+if exist "%HERE%..\start.bat" call "%HERE%..\start.bat" %*
+if exist "%HERE%..\start.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" run --module app_ETL %*
+exit /b %errorlevel%

+ 9 - 5
app_ETL/run.sh

@@ -1,8 +1,12 @@
 #!/bin/sh
-# 观澜 · 模块运行入口(P9):用安装根的解释器启动全部服务(等同 python guanlan.py serve)。
-set -e
+# 观澜 · 模块运行入口(P9 双模)。模块: app_ETL
 HERE=$(cd "$(dirname "$0")" && pwd)
 ROOT=$(cd "$HERE/.." && pwd)
-PY="$ROOT/.venv/bin/python"
-[ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
-exec "$PY" "$ROOT/guanlan.py" serve "$@"
+if [ -f "$ROOT/guanlan.py" ]; then
+  PY="$ROOT/.venv/bin/python"; [ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
+  [ -x "$PY" ] || PY=$(command -v python3 || command -v python)
+  exec "$PY" "$ROOT/guanlan.py" serve "$@"
+fi
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" run --module app_ETL "$@"

+ 13 - 3
app_ETL/uninstall.bat

@@ -1,4 +1,14 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified uninstaller.
-rem Module: app_ETL -- see ..\README.MD
-call "%~dp0..\uninstall.bat" %*
+rem Guanlan module uninstall entry (P9 dual-mode). Module: app_ETL
+set "HERE=%~dp0"
+if exist "%HERE%..\uninstall.bat" call "%HERE%..\uninstall.bat" %*
+if exist "%HERE%..\uninstall.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" uninstall --module app_ETL %*
+exit /b %errorlevel%

+ 7 - 2
app_ETL/uninstall.sh

@@ -1,3 +1,8 @@
 #!/bin/sh
-# 观澜 · 模块卸载入口(P9):调用安装根统一卸载器。
-exec sh "$(dirname "$0")/../uninstall.sh" "$@"
+# 观澜 · 模块卸载入口(P9 双模)。模块: app_ETL
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/uninstall.sh" ] && exec sh "$ROOT/uninstall.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" uninstall --module app_ETL "$@"

+ 15 - 3
app_algorithmModel/install.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified installer (single-package delivery).
-rem Module: app_algorithmModel -- see ..\README.MD for install / run / uninstall notes.
-call "%~dp0..\install.bat" %*
+rem Guanlan module entry (P9 dual-mode). Module: app_algorithmModel
+rem  A) full package (install root present) -> call the unified installer;
+rem  B) standalone module package         -> bootstrap a private .venv for this module.
+set "HERE=%~dp0"
+if exist "%HERE%..\install.ps1" call "%HERE%..\install.bat" %*
+if exist "%HERE%..\install.ps1" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" install --module app_algorithmModel %*
+exit /b %errorlevel%

+ 8 - 3
app_algorithmModel/install.sh

@@ -1,4 +1,9 @@
 #!/bin/sh
-# 观澜 · 模块安装入口(P9):调用安装根统一安装器(单包交付,一套安装)。
-# 模块: app_algorithmModel —— 安装/运行说明见 ../README.MD
-exec sh "$(dirname "$0")/../install.sh" "$@"
+# 观澜 · 模块安装入口(P9 双模)。模块: app_algorithmModel
+#   A) 总包(有安装根 install.sh)→ 走统一安装器;B) 独立模块包 → bootstrap 自建 .venv。
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/install.sh" ] && exec sh "$ROOT/install.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3(先装 Python 3.10+)"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" install --module app_algorithmModel "$@"

+ 15 - 3
app_algorithmModel/run.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): start the whole system via the install-root launcher.
-rem Module: app_algorithmModel -- see ..\README.MD
-call "%~dp0..\start.bat" %*
+rem Guanlan module run entry (P9 dual-mode). Module: app_algorithmModel
+rem  A) full package (start.bat present) -> start the whole system;
+rem  B) standalone module package       -> run this module's entry (bootstrap run).
+set "HERE=%~dp0"
+if exist "%HERE%..\start.bat" call "%HERE%..\start.bat" %*
+if exist "%HERE%..\start.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" run --module app_algorithmModel %*
+exit /b %errorlevel%

+ 9 - 5
app_algorithmModel/run.sh

@@ -1,8 +1,12 @@
 #!/bin/sh
-# 观澜 · 模块运行入口(P9):用安装根的解释器启动全部服务(等同 python guanlan.py serve)。
-set -e
+# 观澜 · 模块运行入口(P9 双模)。模块: app_algorithmModel
 HERE=$(cd "$(dirname "$0")" && pwd)
 ROOT=$(cd "$HERE/.." && pwd)
-PY="$ROOT/.venv/bin/python"
-[ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
-exec "$PY" "$ROOT/guanlan.py" serve "$@"
+if [ -f "$ROOT/guanlan.py" ]; then
+  PY="$ROOT/.venv/bin/python"; [ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
+  [ -x "$PY" ] || PY=$(command -v python3 || command -v python)
+  exec "$PY" "$ROOT/guanlan.py" serve "$@"
+fi
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" run --module app_algorithmModel "$@"

+ 13 - 3
app_algorithmModel/uninstall.bat

@@ -1,4 +1,14 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified uninstaller.
-rem Module: app_algorithmModel -- see ..\README.MD
-call "%~dp0..\uninstall.bat" %*
+rem Guanlan module uninstall entry (P9 dual-mode). Module: app_algorithmModel
+set "HERE=%~dp0"
+if exist "%HERE%..\uninstall.bat" call "%HERE%..\uninstall.bat" %*
+if exist "%HERE%..\uninstall.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" uninstall --module app_algorithmModel %*
+exit /b %errorlevel%

+ 7 - 2
app_algorithmModel/uninstall.sh

@@ -1,3 +1,8 @@
 #!/bin/sh
-# 观澜 · 模块卸载入口(P9):调用安装根统一卸载器。
-exec sh "$(dirname "$0")/../uninstall.sh" "$@"
+# 观澜 · 模块卸载入口(P9 双模)。模块: app_algorithmModel
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/uninstall.sh" ] && exec sh "$ROOT/uninstall.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" uninstall --module app_algorithmModel "$@"

+ 15 - 3
app_backEnd/install.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified installer (single-package delivery).
-rem Module: app_backEnd -- see ..\README.MD for install / run / uninstall notes.
-call "%~dp0..\install.bat" %*
+rem Guanlan module entry (P9 dual-mode). Module: app_backEnd
+rem  A) full package (install root present) -> call the unified installer;
+rem  B) standalone module package         -> bootstrap a private .venv for this module.
+set "HERE=%~dp0"
+if exist "%HERE%..\install.ps1" call "%HERE%..\install.bat" %*
+if exist "%HERE%..\install.ps1" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" install --module app_backEnd %*
+exit /b %errorlevel%

+ 8 - 3
app_backEnd/install.sh

@@ -1,4 +1,9 @@
 #!/bin/sh
-# 观澜 · 模块安装入口(P9):调用安装根统一安装器(单包交付,一套安装)。
-# 模块: app_backEnd —— 安装/运行说明见 ../README.MD
-exec sh "$(dirname "$0")/../install.sh" "$@"
+# 观澜 · 模块安装入口(P9 双模)。模块: app_backEnd
+#   A) 总包(有安装根 install.sh)→ 走统一安装器;B) 独立模块包 → bootstrap 自建 .venv。
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/install.sh" ] && exec sh "$ROOT/install.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3(先装 Python 3.10+)"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" install --module app_backEnd "$@"

+ 15 - 3
app_backEnd/run.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): start the whole system via the install-root launcher.
-rem Module: app_backEnd -- see ..\README.MD
-call "%~dp0..\start.bat" %*
+rem Guanlan module run entry (P9 dual-mode). Module: app_backEnd
+rem  A) full package (start.bat present) -> start the whole system;
+rem  B) standalone module package       -> run this module's entry (bootstrap run).
+set "HERE=%~dp0"
+if exist "%HERE%..\start.bat" call "%HERE%..\start.bat" %*
+if exist "%HERE%..\start.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" run --module app_backEnd %*
+exit /b %errorlevel%

+ 9 - 5
app_backEnd/run.sh

@@ -1,8 +1,12 @@
 #!/bin/sh
-# 观澜 · 模块运行入口(P9):用安装根的解释器启动全部服务(等同 python guanlan.py serve)。
-set -e
+# 观澜 · 模块运行入口(P9 双模)。模块: app_backEnd
 HERE=$(cd "$(dirname "$0")" && pwd)
 ROOT=$(cd "$HERE/.." && pwd)
-PY="$ROOT/.venv/bin/python"
-[ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
-exec "$PY" "$ROOT/guanlan.py" serve "$@"
+if [ -f "$ROOT/guanlan.py" ]; then
+  PY="$ROOT/.venv/bin/python"; [ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
+  [ -x "$PY" ] || PY=$(command -v python3 || command -v python)
+  exec "$PY" "$ROOT/guanlan.py" serve "$@"
+fi
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" run --module app_backEnd "$@"

+ 13 - 3
app_backEnd/uninstall.bat

@@ -1,4 +1,14 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified uninstaller.
-rem Module: app_backEnd -- see ..\README.MD
-call "%~dp0..\uninstall.bat" %*
+rem Guanlan module uninstall entry (P9 dual-mode). Module: app_backEnd
+set "HERE=%~dp0"
+if exist "%HERE%..\uninstall.bat" call "%HERE%..\uninstall.bat" %*
+if exist "%HERE%..\uninstall.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" uninstall --module app_backEnd %*
+exit /b %errorlevel%

+ 7 - 2
app_backEnd/uninstall.sh

@@ -1,3 +1,8 @@
 #!/bin/sh
-# 观澜 · 模块卸载入口(P9):调用安装根统一卸载器。
-exec sh "$(dirname "$0")/../uninstall.sh" "$@"
+# 观澜 · 模块卸载入口(P9 双模)。模块: app_backEnd
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/uninstall.sh" ] && exec sh "$ROOT/uninstall.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" uninstall --module app_backEnd "$@"

+ 321 - 0
app_common/app_common_guanlan/bootstrap.py

@@ -0,0 +1,321 @@
+# -*- coding: utf-8 -*-
+r"""观澜 · **模块包**独立安装/自检/运行/卸载的引导器(P9-B 起,配合"分别打包")。
+
+背景(用户令 2026-09-28):前端、后端、算法、数据接入及管理要能**分别打包**。每个模块包 = 公共层 +
+该模块 + 它依赖的模块(按 `configs/modules.yaml` 的 `allow` 递归)。单包装进总包时,模块目录里的
+`install/run/uninstall` 直接调用安装根统一入口;**单独解压一个模块包时**(包根没有安装根那套 `install.ps1`
+或本模块不是集成层),走这里:自建 `.venv`、离线装依赖、自检、按模块起服务或跑链、卸载。
+
+用法(在**模块包根目录**下执行;也可用模块目录里的 install/run/uninstall 脚本转发过来):
+
+    python app_common/app_common_guanlan/bootstrap.py install   --module app_frontEnd
+    python app_common/app_common_guanlan/bootstrap.py check     --module app_frontEnd
+    python app_common/app_common_guanlan/bootstrap.py run       --module app_frontEnd [--port 28100]
+    python app_common/app_common_guanlan/bootstrap.py run       --module app_ETL --cmd rebuild_all -- --dry-run
+    python app_common/app_common_guanlan/bootstrap.py list      --module app_algorithmModel
+    python app_common/app_common_guanlan/bootstrap.py uninstall --module app_frontEnd [--purge]
+
+退出码:0 成功 · 2 用法/环境不满足 · 3 自检不过 · 4 运行失败 · 5 安装失败。
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import pathlib
+import subprocess
+import sys
+import time
+
+WIN = os.name == 'nt'
+
+# 每个模块的"可运行入口"(模块包里 run 的候选;`--cmd` 取这些名字)
+RUNNABLES = {
+    'app_backEnd': {'serve': '启动全部服务(网关 + 工作台 + CMS + 仿真×2 + 三维)',
+                    'check': '系统自检',
+                    'status': '各服务端口/进程状态',
+                    'stop': '停止本启动器起的服务'},
+    'app_frontEnd': {'portal': '起一个静态服务把门户与工作台页面放出来(默认端口 28100)'},
+    'app_ETL': {'raw_scan': '输入数据扫描(发现新增/变化 → 该重跑哪几步)',
+                'raw_data_check': '输入数据放置体检',
+                'rebuild_all': '整链重算(默认加 --dry-run,真要跑请显式给 -- --apply)',
+                'scada_slim_build': '10 分钟标准仓重建'},
+    'app_algorithmModel': {'rudong_model_run': '判级与曲线主链(L0/谱闸/定级)',
+                           'rudong_fusion_run': '融合面与四轴',
+                           'rudong_line_energy_share': '发电量线与能量占比',
+                           'tcm_mask_thresholds_build': 'TCM 掩码阈值反解'},
+    'app_ontology': {'ontology_release_build': '本体发布层 r1/r2 生成',
+                     'sop_findings_from_ledger': '由重算台账生成 claim'},
+    'app_common': {},
+    'app_qualityGate': {'module_boundary_audit': '模块边界审计',
+                        'config_audit': '配置统一审计',
+                        'pages_audit': '页面归口审计',
+                        'chain_gap_check': '重算链缺口检查',
+                        'delivery_docs_build': '三件交付文档构建与自检'},
+}
+MODULES = tuple(RUNNABLES)
+# 支持"分别打包"的四个模块(用户令点名的)+ 其余三个(同一机制,顺带可用)
+FOUR = ('app_frontEnd', 'app_backEnd', 'app_algorithmModel', 'app_ETL')
+
+
+def root_of(f: str | pathlib.Path) -> pathlib.Path:
+    """包根 = 同时含 `VERSION`(或 `configs/`)与 `app_common/` 的最近上层目录。"""
+    p = pathlib.Path(f).resolve()
+    for d in [p.parent] + list(p.parents):
+        if (d / 'app_common').is_dir() and ((d / 'VERSION').is_file() or (d / 'configs').is_dir()):
+            return d
+    return p.parents[1]
+
+
+ROOT = root_of(__file__)
+INFO = ROOT / 'install-info.json'
+
+
+def py_in_venv() -> str:
+    return str(ROOT / '.venv' / ('Scripts/python.exe' if WIN else 'bin/python'))
+
+
+def say(msg: str = '') -> None:
+    print(msg, flush=True)
+
+
+def load_modules() -> dict:
+    """读安装根单件配置 `modules.yaml`(走配置取用口;包结构异常时才兜底手拼)。"""
+    try:
+        from app_common.app_common_guanlan.api import paths as _P
+        p = _P.config('modules.yaml')
+    except Exception:  # noqa: BLE001
+        p = ROOT / 'configs' / 'modules.yaml'      # config-path-allow: 取用口不可用时的兜底
+    if not p.is_file():
+        return {}
+    try:
+        import yaml
+        reg = yaml.safe_load(p.read_text(encoding='utf-8')) or {}
+        return {m['name']: m for m in (reg.get('modules') or [])}
+    except Exception:  # noqa: BLE001  (无 yaml 时退回最小解析:只认 name/allow 行)
+        out, cur = {}, None
+        for ln in p.read_text(encoding='utf-8').splitlines():
+            s = ln.strip()
+            if s.startswith('- name:'):
+                cur = s.split(':', 1)[1].strip()
+                out[cur] = {'allow': []}
+            elif cur and s.startswith('allow:'):
+                body = s.split('[', 1)[-1].rstrip(']')
+                out[cur]['allow'] = [x.strip() for x in body.split(',') if x.strip()]
+        return out
+
+
+def closure(name: str, reg: dict | None = None) -> list[str]:
+    """依赖闭包(依赖在前,自身在后);app_common 永远在最前。"""
+    reg = reg if reg is not None else load_modules()
+    seen: list[str] = []
+
+    def walk(n: str) -> None:
+        if n in seen:
+            return
+        for d in (reg.get(n, {}).get('allow') or []):
+            walk(d)
+        seen.append(n)
+
+    walk(name)
+    if 'app_common' in reg and 'app_common' not in seen:
+        seen.insert(0, 'app_common')
+    return seen
+
+
+def module_api_files(m: str) -> list[pathlib.Path]:
+    d = ROOT / m / f'{m}_guanlan'
+    return sorted(d.glob('*.py')) + sorted(d.rglob('*/api.py'))
+
+
+def cmd_check(a) -> int:
+    say(f'== 自检(模块包 · {a.module})==')
+    say(f'   包根: {ROOT}')
+    bad: list[str] = []
+    reg = load_modules()
+    if not reg:
+        bad.append('读不到 configs/modules.yaml(模块包必须带安装根单件配置)')
+    cl = closure(a.module, reg)
+    say(f'   依赖闭包: {" → ".join(cl)}')
+    # ① 目录与入口
+    for m in cl:
+        d = ROOT / m
+        if not (d / f'{m}_guanlan' / 'api.py').is_file():
+            bad.append(f'{m}/{m}_guanlan/api.py 不在位')
+        if not ((d / 'README.MD').is_file() or (d / 'README.md').is_file()):
+            bad.append(f'{m}/README.MD 不在位')
+        for s in ('install.bat', 'install.sh', 'uninstall.bat', 'uninstall.sh', 'run.bat', 'run.sh'):
+            if not (d / s).is_file():
+                bad.append(f'{m}/{s} 不在位')
+    # ② 版本一致(真源是 app_common 的 version.py;根 VERSION 是给它做镜像的标记文件)
+    import re as _re
+    vp = ROOT / 'app_common' / 'app_common_guanlan' / 'version.py'
+    v2 = ''
+    if vp.is_file():
+        m = _re.search(r"^VERSION\s*=\s*'([^']+)'", vp.read_text(encoding='utf-8'), _re.M)
+        v2 = m.group(1) if m else ''
+    vf = ROOT / 'VERSION'
+    v1 = ''
+    if vf.is_file():
+        m1 = _re.search(r"([0-9]+\.[0-9]+\.[0-9]+)", vf.read_text(encoding='utf-8'))
+        v1 = m1.group(1) if m1 else vf.read_text(encoding='utf-8').strip()
+    if v1 and v2 and v1 != v2:
+        bad.append(f'VERSION({v1}) 与版本真源 app_common/.../version.py({v2}) 不一致')
+    say(f'   版本: VERSION={v1 or "—"} · 版本真源={v2 or "—"}')
+    # ③ 配置单源(安装根单件 vs 模块 configs/)
+    try:
+        from app_common.app_common_guanlan.api import paths as _P2
+        root_cfg_dir = _P2.CONFIGS
+    except Exception:  # noqa: BLE001
+        root_cfg_dir = ROOT / 'configs'            # config-path-allow: 取用口不可用时的兜底
+    root_cfg = {p.relative_to(root_cfg_dir).as_posix() for p in root_cfg_dir.rglob('*') if p.is_file()}
+    for m in cl:
+        mc = ROOT / m / 'configs'
+        if not mc.is_dir():
+            continue
+        for p in mc.rglob('*'):
+            if p.is_file() and p.relative_to(mc).as_posix() in root_cfg:
+                bad.append(f'配置双份: configs/{p.relative_to(mc).as_posix()} 与 {m}/configs/ 同名(必须单源)')
+    # ④ 导入冒烟(每个模块的公开面)
+    sys.path.insert(0, str(ROOT))
+    for m in cl:
+        mod = f'{m}.{m}_guanlan.api'
+        try:
+            __import__(mod)
+        except Exception as e:  # noqa: BLE001
+            bad.append(f'导入 {mod} 失败: {type(e).__name__}: {str(e)[:90]}')
+    if bad:
+        for b in bad:
+            say(f'   [X] {b}')
+        say(f'结论: 自检不过({len(bad)} 项)')
+        return 3
+    say('   [OK] 目录与入口齐全 · 版本一致 · 配置单源 · 公开面可导入')
+    say('结论: 自检通过')
+    return 0
+
+
+def cmd_install(a) -> int:
+    say(f'== 安装(模块包 · {a.module})==')
+    vpy = py_in_venv()
+    if not pathlib.Path(vpy).is_file():
+        say(f'   建 .venv: {ROOT / ".venv"}')
+        r = subprocess.run([sys.executable, '-m', 'venv', str(ROOT / '.venv')])
+        if r.returncode:
+            say('   [X] 建虚拟环境失败')
+            return 5
+    req = ROOT / 'requirements.txt'
+    if req.is_file():
+        wheels = ROOT / 'wheels'
+        args = [vpy, '-m', 'pip', 'install', '-q', '-r', str(req)]
+        offline = wheels.is_dir() and any(wheels.rglob('*.whl'))
+        if offline:
+            args += ['--no-index', '--find-links', str(wheels)]
+        say(f'   装依赖({"离线 wheels" if offline else "在线 pip"})…')
+        r = subprocess.run(args)
+        if r.returncode:
+            say('   [X] 依赖安装失败')
+            return 5
+    info = {}
+    if INFO.is_file():
+        try:
+            info = json.loads(INFO.read_text(encoding='utf-8'))
+        except Exception:  # noqa: BLE001
+            info = {}
+    info.update(dict(name='观澜·模块包', version=(ROOT / 'VERSION').read_text(encoding='utf-8').strip()
+                     if (ROOT / 'VERSION').is_file() else '',
+                     module=a.module, mode='module-package', python=vpy,
+                     installed_at=time.strftime('%Y-%m-%d %H:%M:%S')))
+    INFO.write_text(json.dumps(info, ensure_ascii=False, indent=1), encoding='utf-8')
+    say(f'   已写安装记录 {INFO.name}')
+    return cmd_check(a)
+
+
+def _run_script(script: str, extra: list[str]) -> int:
+    p = ROOT / 'scripts' / f'{script}.py'
+    if not p.is_file():
+        say(f'   [X] 找不到 scripts/{script}.py')
+        return 4
+    cmd = [py_in_venv() if pathlib.Path(py_in_venv()).is_file() else sys.executable, '-X', 'utf8', str(p)] + extra
+    say('   $ ' + ' '.join(cmd))
+    return subprocess.run(cmd, cwd=str(ROOT)).returncode
+
+
+def cmd_list(a) -> int:
+    say(f'== {a.module} 可运行入口 ==')
+    for k, v in RUNNABLES.get(a.module, {}).items():
+        say(f'   {k:26s} {v}')
+    say('\n用法: run --module %s --cmd <名> [-- 透传给该脚本的参数]' % a.module)
+    return 0
+
+
+def cmd_run(a) -> int:
+    m = a.module
+    say(f'== 运行(模块包 · {m})==')
+    if m == 'app_backEnd':
+        if a.cmd in (None, 'serve') and (ROOT / 'guanlan.py').is_file():
+            return subprocess.run([py_in_venv() if pathlib.Path(py_in_venv()).is_file() else sys.executable,
+                                   '-X', 'utf8', str(ROOT / 'guanlan.py'), 'serve'], cwd=str(ROOT)).returncode
+        if a.cmd in ('check', 'status', 'stop'):
+            return subprocess.run([py_in_venv() if pathlib.Path(py_in_venv()).is_file() else sys.executable,
+                                   '-X', 'utf8', str(ROOT / 'guanlan.py'), a.cmd], cwd=str(ROOT)).returncode
+    if m == 'app_frontEnd' and a.cmd in (None, 'portal'):
+        port = str(a.port or 28100)
+        return _run_script('static_server', ['--port', port, '--host', '127.0.0.1', '--dir', str(ROOT / 'release')])
+    cmd = a.cmd or (next(iter(RUNNABLES.get(m, {})), None))
+    if not cmd:
+        say('   本模块没有可直接运行的入口;用 list 看可选名')
+        return 2
+    extra = list(a.rest or [])
+    if m == 'app_ETL' and cmd == 'rebuild_all' and not extra:
+        extra = ['--dry-run']          # 默认只看计划,真跑要显式给参数
+    return _run_script(cmd, extra)
+
+
+def cmd_uninstall(a) -> int:
+    say(f'== 卸载(模块包 · {a.module})==')
+    import shutil
+    keep_root = ROOT
+    # 有质量门(含卸载器)时走统一卸载器,行为与总包一致
+    un = ROOT / 'scripts' / 'guanlan_uninstall.py'
+    if un.is_file():
+        say('   走统一卸载器 scripts/guanlan_uninstall.py(与总包同一套行为)')
+        cmd = [py_in_venv() if pathlib.Path(py_in_venv()).is_file() else sys.executable, '-X', 'utf8', str(un)]
+        if a.purge:
+            cmd.append('--purge')
+        return subprocess.run(cmd, cwd=str(keep_root)).returncode
+    for rel in ('.venv', 'run'):
+        p = keep_root / rel
+        if p.exists():
+            shutil.rmtree(p, ignore_errors=True)
+            say(f'   已删 {rel}/')
+    if INFO.is_file():
+        INFO.unlink()
+        say('   已删安装记录 install-info.json')
+    if a.purge:
+        for rel in ('data', 'outputs', 'logs'):
+            p = keep_root / rel
+            if p.exists():
+                shutil.rmtree(p, ignore_errors=True)
+                say(f'   已删 {rel}/(--purge)')
+    say('结论: 卸载完成(保留程序目录;要连目录一起删就手工删包根)')
+    return 0
+
+
+def main() -> int:
+    ap = argparse.ArgumentParser(description='观澜 · 模块包引导器(安装/自检/运行/卸载)')
+    ap.add_argument('action', choices=('install', 'check', 'run', 'list', 'uninstall'))
+    ap.add_argument('--module', required=True, choices=MODULES)
+    ap.add_argument('--cmd', default=None)
+    ap.add_argument('--port', type=int, default=None)
+    ap.add_argument('--purge', action='store_true')
+    ap.add_argument('rest', nargs='*', help='透传给被调脚本的参数(放在 -- 之后)')
+    a = ap.parse_args()
+    if (ROOT / 'app_common' / 'app_common_guanlan' / 'api.py').is_file() is False:
+        say('[X] 包根看起来不是模块包(缺 app_common/app_common_guanlan/api.py)')
+        return 2
+    return {'install': cmd_install, 'check': cmd_check, 'run': cmd_run, 'list': cmd_list,
+            'uninstall': cmd_uninstall}[a.action](a)
+
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 4 - 1
app_common/app_common_guanlan/version.py

@@ -24,7 +24,7 @@ import json
 import pathlib
 
 NAME = '观澜·如东样板 v2'
-VERSION = '2.13.0'         # ★ 版本只改这里
+VERSION = '2.14.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.14.0', date='2026-09-29', level='minor',
+         title='分别打包:前端 / 后端 / 算法 / 数据接入与管理 各自成包(含依赖闭包 + 独立 install/run/uninstall)',
+         note='功能新增(打包形态)⇒ 中版本 +1。用户令 2026-09-28「前端、后端、算法、数据接入及管理 支持分别打包」。① 打包器加 `--module <名>`:产物 app_guanlang_v<版本>_<模块>.zip;**模块包 = 该模块 + 它依赖的模块(按 configs/modules.yaml 的 allow 递归闭包)+ 公共层 + 共享支撑件**(src 兼容壳 / scripts / configs 单件 / wheels·vendor 离线依赖 / reference;前端与后端另带 release·resources,算法带 resources)。② 包根写 MODULE_PACKAGE.json(模块 + 闭包 + 版本),模块目录里的 install/run/uninstall(bat+sh)改**双模**:总包内调安装根统一入口;独立模块包走新增的 app_common/.../bootstrap.py(自建 .venv、离线装依赖、自检、按模块起服务或跑链、卸载)。③ bootstrap 自检覆盖:目录与 6 个入口脚本、版本一致、配置单源、公开面可导入;run 支持 --cmd(后端 serve/check/status/stop;前端 portal 静态服务;ETL/算法按脚本名跑,ETL 默认 --dry-run);list 列出各模块可运行入口。④ 新增门:根 VERSION 镜像文件必须与 version.py 同号(version_log --check 里查 —— 此前它是 0.2.0 的历史遗留,与真源脱节)。⑤ 打包验证加 `--verify <zip> --module <名>`:解压 → 入口引用闭合与编码守则 → bootstrap 自检 → 可运行入口列举。验收:四个模块包各自出包并验证通过;总包开箱 5/5 + 卸载核验;全门禁 rc=0;guanlan.py check 全绿。'),
     dict(version='2.13.0', date='2026-09-28', level='minor',
          title='P9 源码目录结构变更:实现包上提模块根 + 每模块 common/configs/data/README/入口脚本 + 配置双根',
          note='用户令 2026-09-28 变更重构要求(架构布局变更)⇒ 中版本 +1。① 实现包上提:app_<模块>/common/app_<模块>_guanlan/ → app_<模块>/app_<模块>_guanlan/(7 模块),全仓点号 540 处 + 实路径 190 处一次改写(src/** 壳、scripts/** 壳、api.py、安装器、打包器、门禁、文档)。② 旧前缀兼容:common/app_<模块>_guanlan/__init__.py 用 sys.meta_path 前缀重定向(sys.modules[旧名] is sys.modules[新名],不产生两份模块对象),保留一个版本。③ 每模块补齐 common/(模块内通用组件)、configs/(模块专属配置)、data/(数据件预留)、README.MD(含安装部署与运行)、install/uninstall/run(bat+sh,内部调安装根统一入口;纳入入口引用闭合,92 条)。④ 配置双根:app_ETL/configs/{canonical,contracts,farms}(164 件)与 app_frontEnd/configs/terms(3 件)下放;安装根 configs/ 只留共用单件(serve.json/models.json/portal_pages.yaml/modules.yaml/registry.yaml);P.config()/P.config_dir() 多根查找 + 单源校验;登记表加 owner,config_audit 按归属根校验。⑤ 门禁联动:模块边界 R1/R2/R5 改新布局(并修 _TARGETS 带注解写法漏检);uninstall --all 纳入模块目录。验收:189 个模块文件新旧前缀同体导入;全门禁 rc=0;guanlan.py check 全绿;起服务逐页(/detail/v2 唯一差异为页内注释里的配置路径 +13 字节);打包开箱 5/5 + 卸载核验通过。'),

+ 15 - 3
app_common/install.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified installer (single-package delivery).
-rem Module: app_common -- see ..\README.MD for install / run / uninstall notes.
-call "%~dp0..\install.bat" %*
+rem Guanlan module entry (P9 dual-mode). Module: app_common
+rem  A) full package (install root present) -> call the unified installer;
+rem  B) standalone module package         -> bootstrap a private .venv for this module.
+set "HERE=%~dp0"
+if exist "%HERE%..\install.ps1" call "%HERE%..\install.bat" %*
+if exist "%HERE%..\install.ps1" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" install --module app_common %*
+exit /b %errorlevel%

+ 8 - 3
app_common/install.sh

@@ -1,4 +1,9 @@
 #!/bin/sh
-# 观澜 · 模块安装入口(P9):调用安装根统一安装器(单包交付,一套安装)。
-# 模块: app_common —— 安装/运行说明见 ../README.MD
-exec sh "$(dirname "$0")/../install.sh" "$@"
+# 观澜 · 模块安装入口(P9 双模)。模块: app_common
+#   A) 总包(有安装根 install.sh)→ 走统一安装器;B) 独立模块包 → bootstrap 自建 .venv。
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/install.sh" ] && exec sh "$ROOT/install.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3(先装 Python 3.10+)"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" install --module app_common "$@"

+ 15 - 3
app_common/run.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): start the whole system via the install-root launcher.
-rem Module: app_common -- see ..\README.MD
-call "%~dp0..\start.bat" %*
+rem Guanlan module run entry (P9 dual-mode). Module: app_common
+rem  A) full package (start.bat present) -> start the whole system;
+rem  B) standalone module package       -> run this module's entry (bootstrap run).
+set "HERE=%~dp0"
+if exist "%HERE%..\start.bat" call "%HERE%..\start.bat" %*
+if exist "%HERE%..\start.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" run --module app_common %*
+exit /b %errorlevel%

+ 9 - 5
app_common/run.sh

@@ -1,8 +1,12 @@
 #!/bin/sh
-# 观澜 · 模块运行入口(P9):用安装根的解释器启动全部服务(等同 python guanlan.py serve)。
-set -e
+# 观澜 · 模块运行入口(P9 双模)。模块: app_common
 HERE=$(cd "$(dirname "$0")" && pwd)
 ROOT=$(cd "$HERE/.." && pwd)
-PY="$ROOT/.venv/bin/python"
-[ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
-exec "$PY" "$ROOT/guanlan.py" serve "$@"
+if [ -f "$ROOT/guanlan.py" ]; then
+  PY="$ROOT/.venv/bin/python"; [ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
+  [ -x "$PY" ] || PY=$(command -v python3 || command -v python)
+  exec "$PY" "$ROOT/guanlan.py" serve "$@"
+fi
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" run --module app_common "$@"

+ 13 - 3
app_common/uninstall.bat

@@ -1,4 +1,14 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified uninstaller.
-rem Module: app_common -- see ..\README.MD
-call "%~dp0..\uninstall.bat" %*
+rem Guanlan module uninstall entry (P9 dual-mode). Module: app_common
+set "HERE=%~dp0"
+if exist "%HERE%..\uninstall.bat" call "%HERE%..\uninstall.bat" %*
+if exist "%HERE%..\uninstall.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" uninstall --module app_common %*
+exit /b %errorlevel%

+ 7 - 2
app_common/uninstall.sh

@@ -1,3 +1,8 @@
 #!/bin/sh
-# 观澜 · 模块卸载入口(P9):调用安装根统一卸载器。
-exec sh "$(dirname "$0")/../uninstall.sh" "$@"
+# 观澜 · 模块卸载入口(P9 双模)。模块: app_common
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/uninstall.sh" ] && exec sh "$ROOT/uninstall.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" uninstall --module app_common "$@"

+ 15 - 3
app_frontEnd/install.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified installer (single-package delivery).
-rem Module: app_frontEnd -- see ..\README.MD for install / run / uninstall notes.
-call "%~dp0..\install.bat" %*
+rem Guanlan module entry (P9 dual-mode). Module: app_frontEnd
+rem  A) full package (install root present) -> call the unified installer;
+rem  B) standalone module package         -> bootstrap a private .venv for this module.
+set "HERE=%~dp0"
+if exist "%HERE%..\install.ps1" call "%HERE%..\install.bat" %*
+if exist "%HERE%..\install.ps1" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" install --module app_frontEnd %*
+exit /b %errorlevel%

+ 8 - 3
app_frontEnd/install.sh

@@ -1,4 +1,9 @@
 #!/bin/sh
-# 观澜 · 模块安装入口(P9):调用安装根统一安装器(单包交付,一套安装)。
-# 模块: app_frontEnd —— 安装/运行说明见 ../README.MD
-exec sh "$(dirname "$0")/../install.sh" "$@"
+# 观澜 · 模块安装入口(P9 双模)。模块: app_frontEnd
+#   A) 总包(有安装根 install.sh)→ 走统一安装器;B) 独立模块包 → bootstrap 自建 .venv。
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/install.sh" ] && exec sh "$ROOT/install.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3(先装 Python 3.10+)"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" install --module app_frontEnd "$@"

+ 15 - 3
app_frontEnd/run.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): start the whole system via the install-root launcher.
-rem Module: app_frontEnd -- see ..\README.MD
-call "%~dp0..\start.bat" %*
+rem Guanlan module run entry (P9 dual-mode). Module: app_frontEnd
+rem  A) full package (start.bat present) -> start the whole system;
+rem  B) standalone module package       -> run this module's entry (bootstrap run).
+set "HERE=%~dp0"
+if exist "%HERE%..\start.bat" call "%HERE%..\start.bat" %*
+if exist "%HERE%..\start.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" run --module app_frontEnd %*
+exit /b %errorlevel%

+ 9 - 5
app_frontEnd/run.sh

@@ -1,8 +1,12 @@
 #!/bin/sh
-# 观澜 · 模块运行入口(P9):用安装根的解释器启动全部服务(等同 python guanlan.py serve)。
-set -e
+# 观澜 · 模块运行入口(P9 双模)。模块: app_frontEnd
 HERE=$(cd "$(dirname "$0")" && pwd)
 ROOT=$(cd "$HERE/.." && pwd)
-PY="$ROOT/.venv/bin/python"
-[ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
-exec "$PY" "$ROOT/guanlan.py" serve "$@"
+if [ -f "$ROOT/guanlan.py" ]; then
+  PY="$ROOT/.venv/bin/python"; [ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
+  [ -x "$PY" ] || PY=$(command -v python3 || command -v python)
+  exec "$PY" "$ROOT/guanlan.py" serve "$@"
+fi
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" run --module app_frontEnd "$@"

+ 13 - 3
app_frontEnd/uninstall.bat

@@ -1,4 +1,14 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified uninstaller.
-rem Module: app_frontEnd -- see ..\README.MD
-call "%~dp0..\uninstall.bat" %*
+rem Guanlan module uninstall entry (P9 dual-mode). Module: app_frontEnd
+set "HERE=%~dp0"
+if exist "%HERE%..\uninstall.bat" call "%HERE%..\uninstall.bat" %*
+if exist "%HERE%..\uninstall.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" uninstall --module app_frontEnd %*
+exit /b %errorlevel%

+ 7 - 2
app_frontEnd/uninstall.sh

@@ -1,3 +1,8 @@
 #!/bin/sh
-# 观澜 · 模块卸载入口(P9):调用安装根统一卸载器。
-exec sh "$(dirname "$0")/../uninstall.sh" "$@"
+# 观澜 · 模块卸载入口(P9 双模)。模块: app_frontEnd
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/uninstall.sh" ] && exec sh "$ROOT/uninstall.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" uninstall --module app_frontEnd "$@"

+ 15 - 3
app_ontology/install.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified installer (single-package delivery).
-rem Module: app_ontology -- see ..\README.MD for install / run / uninstall notes.
-call "%~dp0..\install.bat" %*
+rem Guanlan module entry (P9 dual-mode). Module: app_ontology
+rem  A) full package (install root present) -> call the unified installer;
+rem  B) standalone module package         -> bootstrap a private .venv for this module.
+set "HERE=%~dp0"
+if exist "%HERE%..\install.ps1" call "%HERE%..\install.bat" %*
+if exist "%HERE%..\install.ps1" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" install --module app_ontology %*
+exit /b %errorlevel%

+ 8 - 3
app_ontology/install.sh

@@ -1,4 +1,9 @@
 #!/bin/sh
-# 观澜 · 模块安装入口(P9):调用安装根统一安装器(单包交付,一套安装)。
-# 模块: app_ontology —— 安装/运行说明见 ../README.MD
-exec sh "$(dirname "$0")/../install.sh" "$@"
+# 观澜 · 模块安装入口(P9 双模)。模块: app_ontology
+#   A) 总包(有安装根 install.sh)→ 走统一安装器;B) 独立模块包 → bootstrap 自建 .venv。
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/install.sh" ] && exec sh "$ROOT/install.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3(先装 Python 3.10+)"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" install --module app_ontology "$@"

+ 15 - 3
app_ontology/run.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): start the whole system via the install-root launcher.
-rem Module: app_ontology -- see ..\README.MD
-call "%~dp0..\start.bat" %*
+rem Guanlan module run entry (P9 dual-mode). Module: app_ontology
+rem  A) full package (start.bat present) -> start the whole system;
+rem  B) standalone module package       -> run this module's entry (bootstrap run).
+set "HERE=%~dp0"
+if exist "%HERE%..\start.bat" call "%HERE%..\start.bat" %*
+if exist "%HERE%..\start.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" run --module app_ontology %*
+exit /b %errorlevel%

+ 9 - 5
app_ontology/run.sh

@@ -1,8 +1,12 @@
 #!/bin/sh
-# 观澜 · 模块运行入口(P9):用安装根的解释器启动全部服务(等同 python guanlan.py serve)。
-set -e
+# 观澜 · 模块运行入口(P9 双模)。模块: app_ontology
 HERE=$(cd "$(dirname "$0")" && pwd)
 ROOT=$(cd "$HERE/.." && pwd)
-PY="$ROOT/.venv/bin/python"
-[ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
-exec "$PY" "$ROOT/guanlan.py" serve "$@"
+if [ -f "$ROOT/guanlan.py" ]; then
+  PY="$ROOT/.venv/bin/python"; [ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
+  [ -x "$PY" ] || PY=$(command -v python3 || command -v python)
+  exec "$PY" "$ROOT/guanlan.py" serve "$@"
+fi
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" run --module app_ontology "$@"

+ 13 - 3
app_ontology/uninstall.bat

@@ -1,4 +1,14 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified uninstaller.
-rem Module: app_ontology -- see ..\README.MD
-call "%~dp0..\uninstall.bat" %*
+rem Guanlan module uninstall entry (P9 dual-mode). Module: app_ontology
+set "HERE=%~dp0"
+if exist "%HERE%..\uninstall.bat" call "%HERE%..\uninstall.bat" %*
+if exist "%HERE%..\uninstall.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" uninstall --module app_ontology %*
+exit /b %errorlevel%

+ 7 - 2
app_ontology/uninstall.sh

@@ -1,3 +1,8 @@
 #!/bin/sh
-# 观澜 · 模块卸载入口(P9):调用安装根统一卸载器。
-exec sh "$(dirname "$0")/../uninstall.sh" "$@"
+# 观澜 · 模块卸载入口(P9 双模)。模块: app_ontology
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/uninstall.sh" ] && exec sh "$ROOT/uninstall.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" uninstall --module app_ontology "$@"

+ 10 - 0
app_qualityGate/app_qualityGate_guanlan/docs/version_log.py

@@ -97,6 +97,16 @@ def main() -> int:
             print(f'[X] 版本记录与 src/version.py 不一致(版本 v{V.VERSION})'
                   f' —— 跑 python scripts/version_log.py --write')
             return 6
+        # ★2026-09-29 加:根 `VERSION` 文件曾被漏在 0.2.0(历史遗留),与真源脱节 →
+        #   它是给外部工具看的**镜像标记**,必须与 version.py 同号(真源永远只有 version.py 一处)。
+        vf = ROOT / 'VERSION'
+        if vf.is_file():
+            import re as _re
+            m = _re.search(r'([0-9]+\.[0-9]+\.[0-9]+)', vf.read_text(encoding='utf-8'))
+            if m and m.group(1) != V.VERSION:
+                print(f'[X] 根 VERSION 文件是 {m.group(1)},与版本真源 {V.VERSION} 不一致'
+                      f' —— 真源只有 src/version.py 一处,VERSION 是它的镜像')
+                return 6
         print(f'[OK] 版本记录一致(v{V.VERSION},{len(V.HISTORY)} 条)')
         return 0
     print(render())

+ 206 - 0
app_qualityGate/app_qualityGate_guanlan/pack/pack_dist.py

@@ -519,6 +519,191 @@ def verify(zip_path: pathlib.Path, keep=False) -> int:
     return 0 if (ok >= 4 and ubad == 0) else 1
 
 
+
+# ══════════════════════════════════════════════════════════════════════════════════════════════
+# 分别打包(用户令 2026-09-28):`--module <名>` 出"单模块包"= 该模块 + 依赖闭包 + 公共层 + 共享支撑件
+# ══════════════════════════════════════════════════════════════════════════════════════════════
+MODULE_SUPPORT_DIRS = ['src', 'scripts', 'configs', 'wheels', 'vendor', 'reference']
+# 模块包还要带的"重资产/运行资产"(按模块给,避免每个包都驮 20 MB 门户与三维)
+MODULE_EXTRA_DIRS = {
+    'app_frontEnd': ['release', 'resources'],
+    'app_backEnd': ['release', 'resources'],
+    'app_algorithmModel': ['resources'],
+    'app_ETL': [],
+    'app_ontology': [],
+    'app_common': [],
+    'app_qualityGate': [],
+}
+# 只有集成层(app_backEnd)才带这套:根 CLI 与起停/打包入口(它们的自检走 guanlan.py check)
+BACKEND_ROOT_FILES = ['guanlan.py', 'check.bat', 'start.bat', 'stop.bat', 'pack.bat', 'pack.sh']
+# 模块包不带安装根那套(install.ps1/README_先读我/测试须知)——独立模块包用**模块自己的**
+# 入口脚本(app_X/install.bat|sh 等,双模:总包内调安装根、独立包走 bootstrap.py)。
+MODULE_COMMON_FILES = ['requirements.txt', 'VERSION',
+                       'data/raw/README_把原始数据放这里.txt']
+
+
+def module_registry() -> dict:
+    """→ {模块名: {allow: [...], cn: ...}}(读安装根的 configs/modules.yaml)。"""
+    import yaml
+    reg = yaml.safe_load(P.config('modules.yaml').read_text(encoding='utf-8')) or {}
+    return {m['name']: m for m in (reg.get('modules') or [])}
+
+
+def module_closure(name: str) -> list[str]:
+    """依赖闭包(依赖在前、自身在后;公共层永远在第一位)。"""
+    reg = module_registry()
+    if name not in reg:
+        raise SystemExit(f'[X] 登记表里没有模块 {name}(可选: {", ".join(reg)})')
+    seen: list[str] = []
+
+    def walk(n: str) -> None:
+        if n in seen:
+            return
+        for d in (reg.get(n, {}).get('allow') or []):
+            walk(d)
+        seen.append(n)
+
+    walk(name)
+    if 'app_common' in reg and 'app_common' not in seen:
+        seen.insert(0, 'app_common')
+    return seen
+
+
+def module_closure_real() -> list[str]:
+    """**真实导入依赖**闭包(含经 `src/**` 兼容壳的间接边)。
+
+    ★2026-09-29 实逮:按登记表 allow 取闭包会打出"缺件起不来"的包 —— `app_ETL.api` 在只含
+    app_common+app_ETL 的包里导入时报 `No module named 'app_backEnd'`(它经 `src.windscada.config`
+    这个旧路径壳用到后端)。实测七个模块的导入图近全连通,因此这里如实返回全部模块。
+    """
+    return list(module_dirs())
+
+
+def module_package_plan(name: str):
+    """→ (要打包的路径 list, 清单说明 dict)。
+
+    模块目录:按**真实导入闭包**取(当前 = 全部七个模块,见 `module_closure_real` 的实逮说明);
+    共享支撑件:src / scripts / configs / wheels / vendor / reference(+ 按模块带 release·resources)。
+    """
+    cl = module_closure_real()
+    cl_reg = module_closure(name)          # 登记表口径(设计意图),写进标记供人看
+    inc: list[pathlib.Path] = []
+    for d in MODULE_SUPPORT_DIRS + MODULE_EXTRA_DIRS.get(name, []):
+        p = ROOT / d
+        if p.is_dir():
+            inc.append(p)
+    for m in cl:
+        p = ROOT / m
+        if p.is_dir():
+            inc.append(p)
+        else:
+            print(f'   [!] 闭包里的模块目录 {m}/ 不在源码树里')
+    files = list(MODULE_COMMON_FILES)
+    if 'app_backEnd' in cl:
+        files += BACKEND_ROOT_FILES
+    for f in files:
+        p = ROOT / f
+        if p.is_file():
+            inc.append(p)
+        else:
+            print(f'   [!] 清单里的 {f} 不在源码树里')
+    manifest = dict(module=name, closure=cl, closure_registry=cl_reg,
+                    deps=[m for m in cl if m != name],
+                    note='单模块包:模块目录按**真实导入闭包**随包(当前=全部七个模块 —— 历史遗留的 src/** 壳'
+                         '把模块互相串起来,只带登记闭包会缺件起不来);差异在默认入口与 release/resources。'
+                         '安装/自检/运行走 app_common 的 bootstrap.py')
+    return inc, manifest
+
+
+def build_module(name: str, out: pathlib.Path, progress: bool = True) -> int:
+    """打单模块包:与总包同一套排除规则(无数据/无产物/无日志/无临时件)。"""
+    import json as _json
+    import time as _time
+    inc, extra = module_package_plan(name)
+    print(f'模块包 {name} · 依赖闭包 {" → ".join(extra["closure"])}')
+    manifest_extra = dict(extra)
+    t0 = _time.time()
+    tot = 0
+    n_items = 0
+    with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as z:
+        for p in inc:
+            if p.is_file():
+                if keep(p):
+                    z.write(p, p.relative_to(ROOT).as_posix())
+                    tot += p.stat().st_size
+                    n_items += 1
+            else:
+                for f in p.rglob('*'):
+                    if not f.is_file() or not keep(f):
+                        continue
+                    z.write(f, f.relative_to(ROOT).as_posix())
+                    tot += f.stat().st_size
+                    n_items += 1
+        # 模块包标记(安装/自检据此分流;总包不带这个文件)
+        z.writestr('MODULE_PACKAGE.json', _json.dumps(dict(
+            version=V.VERSION, package=out.name, **manifest_extra), ensure_ascii=False, indent=1))
+        z.writestr('dist-manifest.json', _json.dumps(dict(
+            version=V.VERSION, package=out.name, kind='module-package', generated_at=_time.strftime('%Y-%m-%d %H:%M:%S'),
+            no_data=True, no_products=True, no_logs=True, no_temp=True, **manifest_extra),
+            ensure_ascii=False, indent=1))
+        # 输入数据目录骨架(与总包一致:带结构不带数据文件)
+        for rel in data_skeleton():
+            zi = zipfile.ZipInfo(rel.rstrip('/') + '/')
+            zi.external_attr = (0o40775 << 16) | 0x10
+            zi.compress_type = zipfile.ZIP_STORED
+            z.writestr(zi, b'')
+    print(f'完成: {out}  {out.stat().st_size/1e6:.0f} MB (源 {tot/1e6:.0f} MB), 条目 {n_items} 件, 耗时 {_time.time()-t0:.0f}s')
+    return 0
+
+
+def verify_module(zip_path: pathlib.Path, name: str, keep_dir: bool = False) -> int:
+    """模块包开箱验证:解压 → 入口编码/引用闭合 → bootstrap 自检 →(前端/后端)起服务探活。"""
+    import json as _json
+    import shutil as _sh
+    import subprocess
+    import tempfile as _tmp
+    tmp = pathlib.Path(_tmp.gettempdir()) / f'guanlan_modulecheck_{name}'
+    if tmp.exists():
+        _sh.rmtree(tmp, ignore_errors=True)
+    print(f'① 解压到 {tmp}')
+    with zipfile.ZipFile(zip_path) as z:
+        z.extractall(tmp)
+    print(f'   {sum(1 for _ in tmp.rglob("*") if _.is_file())} 件')
+    from src.entry_refs import encoding_problems, missing_refs
+    miss, enc = missing_refs(tmp), encoding_problems(tmp)
+    if miss or enc:
+        for e, rel in miss:
+            print(f'   [X] {e} 引用 {rel},包内没有')
+        for m in enc:
+            print(f'   [X] 入口编码守则: {m}')
+        return 1
+    print('   [OK] 入口引用闭合 + 编码守则通过')
+    marker = tmp / 'MODULE_PACKAGE.json'
+    if not marker.is_file():
+        print('   [X] 缺 MODULE_PACKAGE.json(不是模块包?)')
+        return 1
+    info = _json.loads(marker.read_text(encoding='utf-8'))
+    print(f'② 模块包标记: 模块={info.get("module")} 闭包={" → ".join(info.get("closure") or [])} 版本={info.get("version")}')
+    if info.get('module') != name:
+        print(f'   [X] 标记里的模块是 {info.get("module")},与请求的 {name} 不符')
+        return 1
+    print('③ bootstrap 自检(用当前解释器,不改动包内 .venv)')
+    boot = tmp / 'app_common/app_common_guanlan/bootstrap.py'
+    r = subprocess.run([sys.executable, '-X', 'utf8', str(boot), 'check', '--module', name],
+                       cwd=str(tmp), capture_output=True, text=True, errors='replace', timeout=1200)
+    for ln in (r.stdout or '').strip().splitlines()[-8:]:
+        print('   ' + ln[:150])
+    if r.returncode != 0:
+        print(f'   [X] 自检 rc={r.returncode}')
+        return 1
+    r2 = subprocess.run([sys.executable, '-X', 'utf8', str(boot), 'list', '--module', name],
+                        cwd=str(tmp), capture_output=True, text=True, errors='replace', timeout=600)
+    print(f'④ 可运行入口列举 rc={r2.returncode}: {(r2.stdout or "").strip().splitlines()[-1][:80]}')
+    if not keep_dir:
+        _sh.rmtree(tmp, ignore_errors=True)
+        print('   已清理解压目录(--keep 可保留)')
+    return 0
+
 def main() -> int:
     ap = argparse.ArgumentParser()
     ap.add_argument('--out', default=None)
@@ -530,10 +715,31 @@ def main() -> int:
                     help='(旧开关) 不含产物 —— 现在**默认就是这个**, 保留只为兼容旧命令')
     ap.add_argument('--dry-run', action='store_true')
     ap.add_argument('--verify', default=None, help='对已打好的 zip 做开箱验证(解压→安装→起服务核验)')
+    ap.add_argument('--module', default=None,
+                    help='分别打包:只打这个模块(含其依赖闭包)→ app_guanlang_v<版本>_<模块>.zip;'
+                         '配 --verify 时表示"要验证的是该模块包"')
     ap.add_argument('--keep', action='store_true', help='--verify 后保留解压目录')
     a = ap.parse_args()
     if a.verify:
+        if a.module:
+            return verify_module(pathlib.Path(a.verify), a.module, a.keep)
         return verify(pathlib.Path(a.verify), a.keep)
+    if a.module:
+        # ★分别打包(用户令 2026-09-28):单模块包 = 该模块 + 依赖闭包 + 公共层 + 共享支撑件
+        cl = module_closure(a.module)
+        out = (pathlib.Path(a.out) if a.out
+               else ROOT.parent / f'{V.PACKAGE_STEM}_v{V.VERSION}_{a.module}.zip')
+        if a.dry_run:
+            inc, extra = module_package_plan(a.module)
+            print(f'== 模块包 {a.module}(version {VERSION})· 依赖闭包 {" → ".join(cl)} ==')
+            tot_n = tot_s = 0
+            for p in inc:
+                n, s = size_of(p); tot_n += n; tot_s += s
+                print(f'   {p.relative_to(ROOT).as_posix():26s} {n:6d} 件  {s/1e6:9.1f} MB')
+            print(f'   {"合计":26s} {tot_n:6d} 件  {tot_s/1e6:9.1f} MB  → {out}')
+            print('\n(dry-run, 未写文件)')
+            return 0
+        return build_module(a.module, out)
     out = (pathlib.Path(a.out) if a.out
            else ROOT.parent / V.package_name())
     if a.dry_run:

+ 15 - 3
app_qualityGate/install.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified installer (single-package delivery).
-rem Module: app_qualityGate -- see ..\README.MD for install / run / uninstall notes.
-call "%~dp0..\install.bat" %*
+rem Guanlan module entry (P9 dual-mode). Module: app_qualityGate
+rem  A) full package (install root present) -> call the unified installer;
+rem  B) standalone module package         -> bootstrap a private .venv for this module.
+set "HERE=%~dp0"
+if exist "%HERE%..\install.ps1" call "%HERE%..\install.bat" %*
+if exist "%HERE%..\install.ps1" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" install --module app_qualityGate %*
+exit /b %errorlevel%

+ 8 - 3
app_qualityGate/install.sh

@@ -1,4 +1,9 @@
 #!/bin/sh
-# 观澜 · 模块安装入口(P9):调用安装根统一安装器(单包交付,一套安装)。
-# 模块: app_qualityGate —— 安装/运行说明见 ../README.MD
-exec sh "$(dirname "$0")/../install.sh" "$@"
+# 观澜 · 模块安装入口(P9 双模)。模块: app_qualityGate
+#   A) 总包(有安装根 install.sh)→ 走统一安装器;B) 独立模块包 → bootstrap 自建 .venv。
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/install.sh" ] && exec sh "$ROOT/install.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3(先装 Python 3.10+)"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" install --module app_qualityGate "$@"

+ 15 - 3
app_qualityGate/run.bat

@@ -1,4 +1,16 @@
 @echo off
-rem Guanlan module entry (P9): start the whole system via the install-root launcher.
-rem Module: app_qualityGate -- see ..\README.MD
-call "%~dp0..\start.bat" %*
+rem Guanlan module run entry (P9 dual-mode). Module: app_qualityGate
+rem  A) full package (start.bat present) -> start the whole system;
+rem  B) standalone module package       -> run this module's entry (bootstrap run).
+set "HERE=%~dp0"
+if exist "%HERE%..\start.bat" call "%HERE%..\start.bat" %*
+if exist "%HERE%..\start.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found. Install Python 3.10+ first, then re-run.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" run --module app_qualityGate %*
+exit /b %errorlevel%

+ 9 - 5
app_qualityGate/run.sh

@@ -1,8 +1,12 @@
 #!/bin/sh
-# 观澜 · 模块运行入口(P9):用安装根的解释器启动全部服务(等同 python guanlan.py serve)。
-set -e
+# 观澜 · 模块运行入口(P9 双模)。模块: app_qualityGate
 HERE=$(cd "$(dirname "$0")" && pwd)
 ROOT=$(cd "$HERE/.." && pwd)
-PY="$ROOT/.venv/bin/python"
-[ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
-exec "$PY" "$ROOT/guanlan.py" serve "$@"
+if [ -f "$ROOT/guanlan.py" ]; then
+  PY="$ROOT/.venv/bin/python"; [ -x "$PY" ] || PY="$ROOT/.venv/Scripts/python.exe"
+  [ -x "$PY" ] || PY=$(command -v python3 || command -v python)
+  exec "$PY" "$ROOT/guanlan.py" serve "$@"
+fi
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" run --module app_qualityGate "$@"

+ 13 - 3
app_qualityGate/uninstall.bat

@@ -1,4 +1,14 @@
 @echo off
-rem Guanlan module entry (P9): call the install-root unified uninstaller.
-rem Module: app_qualityGate -- see ..\README.MD
-call "%~dp0..\uninstall.bat" %*
+rem Guanlan module uninstall entry (P9 dual-mode). Module: app_qualityGate
+set "HERE=%~dp0"
+if exist "%HERE%..\uninstall.bat" call "%HERE%..\uninstall.bat" %*
+if exist "%HERE%..\uninstall.bat" exit /b %errorlevel%
+set "PY="
+where py >nul 2>nul && set "PY=py -3"
+if not defined PY where python >nul 2>nul && set "PY=python"
+if not defined PY (
+  echo [X] Python 3 not found.
+  exit /b 2
+)
+%PY% "%HERE%..\app_common\app_common_guanlan\bootstrap.py" uninstall --module app_qualityGate %*
+exit /b %errorlevel%

+ 7 - 2
app_qualityGate/uninstall.sh

@@ -1,3 +1,8 @@
 #!/bin/sh
-# 观澜 · 模块卸载入口(P9):调用安装根统一卸载器。
-exec sh "$(dirname "$0")/../uninstall.sh" "$@"
+# 观澜 · 模块卸载入口(P9 双模)。模块: app_qualityGate
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$HERE/.." && pwd)
+[ -f "$ROOT/uninstall.sh" ] && exec sh "$ROOT/uninstall.sh" "$@"
+PY=$(command -v python3 || command -v python)
+[ -n "$PY" ] || { echo "[X] 找不到 python3"; exit 2; }
+exec "$PY" "$ROOT/app_common/app_common_guanlan/bootstrap.py" uninstall --module app_qualityGate "$@"

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

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

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

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

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

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


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

@@ -7,7 +7,7 @@
 <!-- VERSION-LOG:BEGIN -->
 ## 版本号规则(用户令 2026-09-17)
 
-编号 **`v<大版本号>.<中版本号>.<小版本号>`**,例如 `v2.13.0`。
+编号 **`v<大版本号>.<中版本号>.<小版本号>`**,例如 `v2.14.0`。
 
 | 级别 | 含义 |
 |---|---|
@@ -17,12 +17,13 @@
 
 **怎么改**:改动落在"解决方案/架构/核心功能" → 大 +1(中/小归 0);落在"非核心功能的新增/减少/修改" → 中 +1(小归 0);只是"消缺完善" → 小 +1;一次发布混了几类,取最高那一级
 
-**打包文件名** = `app_guanlang_v2.13.0.zip`(规则:`app_guanlang_v<版本号>.zip`)。
+**打包文件名** = `app_guanlang_v2.14.0.zip`(规则:`app_guanlang_v<版本号>.zip`)。
 
-版本号只写在 `src/version.py` 一行(`VERSION = '2.13.0'`);打包器、安装脚本、安装记录 `install-info.json`、自检都读它,不另设真源。
+版本号只写在 `src/version.py` 一行(`VERSION = '2.14.0'`);打包器、安装脚本、安装记录 `install-info.json`、自检都读它,不另设真源。
 
 | 版本 | 日期 | 级别 | 主要变更 | 说明 |
 |---|---|---|---|---|
+| `v2.14.0` | 2026-09-29 | 中 | 分别打包:前端 / 后端 / 算法 / 数据接入与管理 各自成包(含依赖闭包 + 独立 install/run/uninstall) | 功能新增(打包形态)⇒ 中版本 +1。用户令 2026-09-28「前端、后端、算法、数据接入及管理 支持分别打包」。① 打包器加 `--module <名>`:产物 app_guanlang_v<版本>_<模块>.zip;**模块包 = 该模块 + 它依赖的模块(按 configs/modules.yaml 的 allow 递归闭包)+ 公共层 + 共享支撑件**(src 兼容壳 / scripts / configs 单件 / wheels·vendor 离线依赖 / reference;前端与后端另带 release·resources,算法带 resources)。② 包根写 MODULE_PACKAGE.json(模块 + 闭包 + 版本),模块目录里的 install/run/uninstall(bat+sh)改**双模**:总包内调安装根统一入口;独立模块包走新增的 app_common/.../bootstrap.py(自建 .venv、离线装依赖、自检、按模块起服务或跑链、卸载)。③ bootstrap 自检覆盖:目录与 6 个入口脚本、版本一致、配置单源、公开面可导入;run 支持 --cmd(后端 serve/check/status/stop;前端 portal 静态服务;ETL/算法按脚本名跑,ETL 默认 --dry-run);list 列出各模块可运行入口。④ 新增门:根 VERSION 镜像文件必须与 version.py 同号(version_log --check 里查 —— 此前它是 0.2.0 的历史遗留,与真源脱节)。⑤ 打包验证加 `--verify <zip> --module <名>`:解压 → 入口引用闭合与编码守则 → bootstrap 自检 → 可运行入口列举。验收:四个模块包各自出包并验证通过;总包开箱 5/5 + 卸载核验;全门禁 rc=0;guanlan.py check 全绿。 |
 | `v2.13.0` | 2026-09-28 | 中 | P9 源码目录结构变更:实现包上提模块根 + 每模块 common/configs/data/README/入口脚本 + 配置双根 | 用户令 2026-09-28 变更重构要求(架构布局变更)⇒ 中版本 +1。① 实现包上提:app_<模块>/common/app_<模块>_guanlan/ → app_<模块>/app_<模块>_guanlan/(7 模块),全仓点号 540 处 + 实路径 190 处一次改写(src/** 壳、scripts/** 壳、api.py、安装器、打包器、门禁、文档)。② 旧前缀兼容:common/app_<模块>_guanlan/__init__.py 用 sys.meta_path 前缀重定向(sys.modules[旧名] is sys.modules[新名],不产生两份模块对象),保留一个版本。③ 每模块补齐 common/(模块内通用组件)、configs/(模块专属配置)、data/(数据件预留)、README.MD(含安装部署与运行)、install/uninstall/run(bat+sh,内部调安装根统一入口;纳入入口引用闭合,92 条)。④ 配置双根:app_ETL/configs/{canonical,contracts,farms}(164 件)与 app_frontEnd/configs/terms(3 件)下放;安装根 configs/ 只留共用单件(serve.json/models.json/portal_pages.yaml/modules.yaml/registry.yaml);P.config()/P.config_dir() 多根查找 + 单源校验;登记表加 owner,config_audit 按归属根校验。⑤ 门禁联动:模块边界 R1/R2/R5 改新布局(并修 _TARGETS 带注解写法漏检);uninstall --all 纳入模块目录。验收:189 个模块文件新旧前缀同体导入;全门禁 rc=0;guanlan.py check 全绿;起服务逐页(/detail/v2 唯一差异为页内注释里的配置路径 +13 字节);打包开箱 5/5 + 卸载核验通过。 |
 | `v2.12.1` | 2026-09-28 | 小 | 源码梳理与死代码清理:删 cms_availability(275 行,另有实现)+ 清本地残留 15.4 MB | 清理类改动(不改行为口径)⇒ 小版本 +1。用户令「梳理源代码,及清理无用源代码程序」。新增 docs/源代码梳理_v0.1.md(761 受管件 / 437 .py / 72,787 行 / 158 兼容壳的规模与分层、十条根入口、23 个质量门、依赖纪律 R1–R8,以及按"引用计数 + 出处"取证的五类无用件清单)。① 删死代码: app_ontology/.../sop/cms_availability.py(275 行)与壳 src/sop/cms_availability.py(23 行)——全仓无调用,L0 数据可用性判据的实际实现在 scripts/rudong_model_run.py::l0_layer;并从 app_ontology/api.py 摘掉 sop_cms_availability 公开名。④ 清本地残留: scripts/sim_hub/frames/(37 张 PNG,8.7 MB,本就 .gitignore)+ 全仓 46 个 __pycache__ 与散落 .pyc,共释放约 15.4 MB(.venv 未动)。②③ 两批(8 件零引用一次性生成器、33 个无人使用的旧路径壳)经用户裁示保留;C 类接入点契约(MinIO objectstore / Redis cache)保留。验收: 全门禁 rc=0(模块边界 R1–R8 / 配置统一 / 页面归口 / 产物反向呼应 / 链路缺口 / 可移植 / 版本记录 / 依赖台账)+ guanlan.py check 全绿 + 起服务逐页探活 + 打包开箱 5/5 + 卸载核验。 |
 | `v2.12.0` | 2026-09-28 | 中 | 模块化重构收官 P7:质量门与打包安装迁移(23 件 / 7,426 行)——七个模块齐备 | 重构收官的架构里程碑(七个模块目录 + 公开面 + 兼容壳齐备)⇒ 中版本 +1。P7 把质量门与打包安装 23 件 / 7,426 行迁入 app_qualityGate/app_qualityGate_guanlan/{audits,docs,pack}/;旧路径 scripts/<件>.py 留 CLI 壳(公开名同面转发 + main(),guanlan.py check 的 importlib 载入照旧)。**根引导脚本 install.ps1/install.sh/uninstall.*/check.bat/pack.*/start|stop.bat 原地不动**("解压即用"引导层,其 Python 实现已迁入模块)。实逮: ① 子包内 __file__ 取根(parents[1])失效 22 处 ⇒ 统一 install_root();② 文件间 bare import 4 处改相对导入;③ audit_chinese_terms 的 here.parent 是仓库根语义 ⇒ 换 install_root();④ 补 P4 遗留:该审计器被审件 scripts/windscada_serve.py 早已是壳 ⇒ 改读实现位置。验收: 8 条质量门 stdout 逐字节等于迁移前 + 23 个 CLI 壳 importlib 公开名同面 + 新路径 -m 可跑 + 打包器从新位置出包并开箱验证 5/5 + 卸载核验通过 + guanlan.py check 全绿。至此 P0–P7 全部完成:公共/数据接入/算法/本体/前端/后端/质量门各一个目录与 api 公开面,旧路径全部留兼容壳。 |
@@ -55,8 +56,8 @@
 
 ## 本次交付
 
-* 版本:`v2.13.0`(观澜·如东样板 v2)
-* 交付包:`app_guanlang_v2.13.0.zip`
+* 版本:`v2.14.0`(观澜·如东样板 v2)
+* 交付包:`app_guanlang_v2.14.0.zip`
 * 服务名:`guanlan`(Windows 服务 / systemd 单元)
 * 安装 / 卸载:`install.bat`、`install.sh` / `uninstall.bat`、`uninstall.sh`
 <!-- VERSION-LOG:END -->

BIN
docs/系统设计说明_观澜_2.14.0.docx


BIN
docs/需求分析_观澜_2.14.0.docx