From 4ad7aba5a3dc6ee93da2d7ce1875f09fa653f877 Mon Sep 17 00:00:00 2001 From: bot_dev1 Date: Wed, 5 Aug 2026 05:34:33 +0800 Subject: [PATCH] =?UTF-8?q?docs(#62-#67):=20=E6=A8=A1=E6=9D=BF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=B0=20README=EF=BC=886=20=E5=AD=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=98=A0=E5=B0=84=EF=BC=89+=20=E7=A6=BB=E7=BA=BF=20?= =?UTF-8?q?=5Fsanity=5Fcheck=EF=BC=88105=20=E6=B5=8B=E8=AF=95+=E7=AB=AF?= =?UTF-8?q?=E5=88=B0=E7=AB=AF=E5=86=92=E7=83=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/template-console/README.md | 82 +++++++++++++++ core/template-console/_sanity_check.py | 139 +++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 core/template-console/README.md create mode 100644 core/template-console/_sanity_check.py diff --git a/core/template-console/README.md b/core/template-console/README.md new file mode 100644 index 0000000..8b3b62e --- /dev/null +++ b/core/template-console/README.md @@ -0,0 +1,82 @@ +# ⑤.7 模板配置台(Template Console)内核引擎 + +> 父 EPIC:#9「⑤.7 模板配置台 Template Console」 +> 子 issue:#62 / #63 / #64 / #65 / #66 / #67(同一 feature 分支 `feature/issue-62`,单 PR 关联全部 6 个 issue) + +配置台是一个**无代码、配置驱动**的内核能力,让实施工程师(而非开发者)按现场调 +模板:点位字典、模型超参、RAG、驾驶舱布局全部在配置台编排,预览确认后发布版本, +再把版本推送给内核(edge-gateway / rag-kb / model-framework)生效。本目录是配置台 +的**纯标准库核心引擎**(不是 Web 前端——前端由 cockpit 渲染本引擎产出的结构化输出)。 + +## 为什么放在 `core/`? + +配置台是**跨模板通用的内核能力**(RBAC / 配置存储 / 版本 / 推送契约服务于所有行业 +模板:氯化 ti-cl4、树脂 resin、…),与 `core/edge-gateway`、`core/rag-kb`、 +`core/model-framework` 同级,而非属于某个具体模板,故置于 `core/template-console/`。 + +## 6 个子任务映射 + +| issue | 模块 | 职责 | +|-------|------|------| +| #62 | `rbac.py` | 三级 RBAC(管理员 admin / 行业工程师 engineer / 只读 readonly),角色继承、`has_permission(resource, action)` 带理由判定、细粒度收窄 | +| #63 | `point_importer.py` | 点位字典 CSV 导入 + 自动校验页面。**复用** `core/edge-gateway/point_dict` 校验器(量纲/数据类型/采样率/重复点号/协议),增加 OPC 节点格式校验、表头列序校验、模板级量纲收窄(resin/ti)、行级结果聚合 | +| #64 | `config_store.py` | 配置项 CRUD(模型超参 / RAG / 布局三类),文件系统版本化 JSON 存储,list/get/upsert/delete + 按类别校验,原子写,快照 snapshot/restore | +| #65 | `preview.py` | 预览渲染引擎:布局(widget 卡片 + 网格占用率/越界/重叠检测)/ 告警(规则渲染 + 模拟触发评估)/ NL 查询(模板 → 示例问句)。对齐 `iAOP-cockpit-layout-v1` widget 类型 | +| #66 | `release.py` | 版本发布 + 回滚点。基于 `config_store` 快照的 Release,semver 单调递增校验,publish 固化快照、rollback 恢复快照(不删历史、回滚事件可追溯) | +| #67 | `push_channel.py` | 配置台↔内核配置推送契约。PushManifest(版本/快照/SHA256 校验和),PushChannel 模拟推送(写 manifest 到内核 inbox)、幂等(同版本不重复推送)、retract 撤回、verify 完整性校验 | + +## 设计原则(对齐 PRD「可解释可溯源」与既有内核范式) + +- **纯标准库零运行时依赖**:不 import pyyaml/numpy/pandas。需要哈希用 `hashlib`, + JSON 用 `json`,CSV 用 `csv`。 +- **dataclass + Enum + 类型注解 + 中文 docstring**,与 `core/data-bus`、 + `core/edge-gateway` 风格一致。 +- **可解释性**:关键决策都带 `meaning` / `reason` 字段(RBAC 判定理据、配置项修改 + 原因、发布 changelog、回滚事件、推送日志),便于审计与配置台展示。 +- **复用而非重造**:#63 直接复用 `core/edge-gateway/point_dict`(schema/loader/validator), + 只增加配置台专属校验维度,避免与内核点位字典机制漂移。 + +## 目录结构 + +``` +core/template-console/ +├── __init__.py # 包入口(导出 RBAC 公共 API) +├── rbac.py # #62 三级 RBAC +├── point_importer.py # #63 点位字典 CSV 导入+校验 +├── config_store.py # #64 配置项 CRUD 存储 +├── preview.py # #65 预览渲染引擎 +├── release.py # #66 版本发布+回滚 +├── push_channel.py # #67 配置推送契约 +├── _sanity_check.py # 离线基本校验(跑全部测试 + 冒烟) +├── README.md # 本文件 +└── tests/ + ├── _bootstrap.py # 挂载 template_console 包 + 暴露 edge-gateway/point_dict + ├── test_rbac.py + ├── test_point_importer.py + ├── test_config_store.py + ├── test_preview.py + ├── test_release.py + └── test_push_channel.py +``` + +## 运行测试 + +```bash +# 嵌入式 Python(无 pip/pyyaml) +/c/gitea/python312/python.exe -m unittest discover \ + -s core/template-console/tests -p "test_*.py" -v + +# 离线基本校验(跑全部测试 + 冒烟) +/c/gitea/python312/python.exe core/template-console/_sanity_check.py +``` + +## 数据流(配置台典型用例) + +``` +实施工程师导入点位字典(#63) ─┐ +行业工程师调模型超参/RAG/布局(#64) ─┼─▶ 预览确认(#65) ─▶ 管理员发布版本(#66) + │ │ + │ ▼ + └──────────────────── 配置推送内核(#67) ─▶ edge-gateway/rag-kb/... +全程受三级 RBAC(#62) 权限管控;每次变更可解释、可溯源、可回滚。 +``` diff --git a/core/template-console/_sanity_check.py b/core/template-console/_sanity_check.py new file mode 100644 index 0000000..33d1539 --- /dev/null +++ b/core/template-console/_sanity_check.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +"""⑤.7 模板配置台离线基本校验(无构建环境下的离线验证)。 + +检查项: +1. 全部单元测试通过(unittest discover); +2. 冒烟:6 个子模块可导入; +3. 冒烟:端到端数据流跑通—— + RBAC 判定(#62) → 点位导入(#63) → 配置 CRUD(#64) → 预览(#65) + → 发布版本(#66) → 推送内核(#67) → 完整性校验通过。 + +用法:python _sanity_check.py +退出码:0 成功 / 1 失败。 +""" +from __future__ import annotations + +import os +import sys +import tempfile +import unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _run_unit_tests() -> tuple[int, int]: + """跑 tests/ 下全部测试,返回 (run, failures+errors)。""" + # 挂载 template_console 包 + edge-gateway/point_dict(同 tests/_bootstrap.py) + sys.path.insert(0, HERE) + import types + if "template_console" not in sys.modules: + pkg = types.ModuleType("template_console") + pkg.__path__ = [HERE] + sys.modules["template_console"] = pkg + edge_gw = os.path.join(os.path.dirname(HERE), "edge-gateway") + if os.path.isdir(edge_gw) and edge_gw not in sys.path: + sys.path.insert(0, edge_gw) + + loader = unittest.TestLoader() + suite = loader.discover(os.path.join(HERE, "tests"), pattern="test_*.py") + runner = unittest.TextTestRunner(verbosity=1, stream=sys.stdout) + result = runner.run(suite) + return result.testsRun, len(result.failures) + len(result.errors) + + +def _smoke_flow() -> list[str]: + """端到端冒烟:返回问题列表(空=通过)。""" + problems: list[str] = [] + try: + from template_console.rbac import ( # type: ignore + Action, Resource, RoleKind, User, has_permission, + ) + from template_console.point_importer import ( # type: ignore + TemplateKind, import_csv_string, + ) + from template_console.config_store import ConfigKind, ConfigStore # type: ignore + from template_console.preview import preview_from_store, PreviewKind # type: ignore + from template_console.release import ReleaseManager # type: ignore + from template_console.push_channel import PushChannel # type: ignore + except Exception as exc: # noqa: BLE001 + problems.append(f"模块导入失败:{exc}") + return problems + + # 1) RBAC:admin 可发布,readonly 不可 + admin = User("a", RoleKind.ADMIN) + viewer = User("v", RoleKind.READONLY) + if not has_permission(admin, Resource.RELEASE, Action.PUBLISH).allow: + problems.append("RBAC:admin 应能发布") + if has_permission(viewer, Resource.RELEASE, Action.PUBLISH).allow: + problems.append("RBAC:readonly 不应能发布") + + # 2) 点位字典导入 + csv_text = ( + "device_id,point_id,name,unit,dataType,sampleRate,qualityCode,opcNode,protocol\n" + "CLF-01,CLF-01.TEMP,炉温,℃,float,1000,true,ns=2;s=CLF.Temp,opcua\n" + ) + _, rep = import_csv_string(csv_text, template=TemplateKind.TI) + if not rep.ok: + problems.append(f"点位导入应通过:{rep.summary()}") + + # 3) 配置 CRUD + 4) 预览 + 5) 发布 + 6) 推送 + tmp = tempfile.mkdtemp() + try: + store = ConfigStore(tmp) + store.upsert(ConfigKind.LAYOUT, "dashboard", + [{"type": "trend", "bind": "CLF-01.TEMP", + "x": 0, "y": 0, "w": 6, "h": 2}], + updated_by="li", reason="冒烟") + # 预览 + prev = preview_from_store(store, PreviewKind.LAYOUT) + if not prev.items: + problems.append("预览:布局应渲染出 widget") + + # 发布 + rm = ReleaseManager(store) + rel = rm.publish("1.0.0", released_by="admin", changelog="冒烟发布") + if rm.latest().version != "1.0.0": + problems.append("发布:最新版本应为 1.0.0") + + # 推送 + 完整性校验 + inbox = os.path.join(tmp, "inbox") + ch = PushChannel(inbox=inbox) + manifest = ch.push(rel, pushed_by="admin") + if not PushChannel.verify(manifest): + problems.append("推送:manifest 完整性校验失败") + if not os.path.isfile(os.path.join(inbox, "manifest-1.0.0.json")): + problems.append("推送:manifest 文件未写入 inbox") + finally: + import shutil + shutil.rmtree(tmp, ignore_errors=True) + + return problems + + +def main() -> int: + print("=" * 60) + print("⑤.7 模板配置台 离线基本校验") + print("=" * 60) + + # 1) 单元测试 + print("\n[1/2] 单元测试") + run_count, fail_count = _run_unit_tests() + if fail_count: + print(f"\nFAIL: 单元测试 {fail_count} 项失败(共 {run_count} 项)") + return 1 + + # 2) 冒烟 + print("\n[2/2] 端到端冒烟") + problems = _smoke_flow() + if problems: + print("FAIL") + for p in problems: + print(" -", p) + return 1 + + print(f"\nOK: 单元测试 {run_count} 项全过;端到端冒烟通过(#62→#67 数据流正常)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())