110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 功能开关(Feature Flags)文件化配置(moduleId: feature-flags, 可重生 ✅)
|
|||
|
|
# 用一个 JSON 文件控制哪些功能上线可见、哪些功能隐藏不用。
|
|||
|
|
#
|
|||
|
|
# 语义(务必牢记):
|
|||
|
|
# - 这是「可用性/可见性」开关,不是权限边界;隐藏不等于鉴权,
|
|||
|
|
# P2/P3 权力路径仍由 Harness 门禁独立保证。
|
|||
|
|
# - fail-open 诚实回退:文件缺失/损坏/版本不符 → 全部功能默认开启,
|
|||
|
|
# 并在响应里显式携带 source/error,绝不静默吞掉配置错误,
|
|||
|
|
# 也绝不因配置损坏把系统锁死。
|
|||
|
|
# - 未知键忽略但如实列入 unknown;非 bool 值该键回退默认开启并记录错误。
|
|||
|
|
#
|
|||
|
|
# 配置文件(默认 server/data/features.json,APS_FEATURES_PATH 可覆盖):
|
|||
|
|
# {"version": 1, "features": {"orders": true, "drawing": false, ...}}
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
_SCHEMA_VERSION = 1
|
|||
|
|
_PATH_ENV = "APS_FEATURES_PATH"
|
|||
|
|
|
|||
|
|
# 功能目录:key → 中文标签(顺序即展示顺序;新增功能必须在此登记)
|
|||
|
|
FEATURE_CATALOG: dict[str, str] = {
|
|||
|
|
"visual": "会话结果视口(甘特/负荷/交期)",
|
|||
|
|
"orders": "订单管理",
|
|||
|
|
"master": "主数据",
|
|||
|
|
"flex": "柔性工作台",
|
|||
|
|
"timeline": "排产结构(时间线)",
|
|||
|
|
"drawing": "图纸识别(DXF)",
|
|||
|
|
"mesh": "多智能体编排",
|
|||
|
|
"knowledge": "知识平台",
|
|||
|
|
"skills": "算法 Skill 管理",
|
|||
|
|
"gov": "审批治理",
|
|||
|
|
"regen": "重生中心",
|
|||
|
|
"settings": "设置",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def default_features_path() -> str:
|
|||
|
|
configured = os.environ.get(_PATH_ENV)
|
|||
|
|
if configured:
|
|||
|
|
return configured
|
|||
|
|
from server.aps_home import path_under_data
|
|||
|
|
|
|||
|
|
return str(path_under_data("features.json"))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _all_enabled() -> dict[str, bool]:
|
|||
|
|
return {key: True for key in FEATURE_CATALOG}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_feature_flags(file_path: str | None = None) -> dict[str, Any]:
|
|||
|
|
"""读取并解析功能开关配置。
|
|||
|
|
|
|||
|
|
Returns: {"version": 1, "source": "default"|"file", "path": str,
|
|||
|
|
"error": str | None, "unknown": [str],
|
|||
|
|
"features": {key: {"enabled": bool, "label": str}}}
|
|||
|
|
"""
|
|||
|
|
path = file_path or default_features_path()
|
|||
|
|
enabled = _all_enabled()
|
|||
|
|
source = "default"
|
|||
|
|
error: str | None = None
|
|||
|
|
unknown: list[str] = []
|
|||
|
|
|
|||
|
|
if not os.path.exists(path):
|
|||
|
|
pass # 文件缺失:默认全开
|
|||
|
|
else:
|
|||
|
|
try:
|
|||
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|||
|
|
raw = json.load(handle)
|
|||
|
|
except (json.JSONDecodeError, OSError, UnicodeDecodeError) as exc:
|
|||
|
|
error = f"功能开关配置无法解析({type(exc).__name__}),已回退全部默认开启"
|
|||
|
|
else:
|
|||
|
|
if not isinstance(raw, dict) or raw.get("version") != _SCHEMA_VERSION:
|
|||
|
|
error = "功能开关配置顶层结构或 version 非法,已回退全部默认开启"
|
|||
|
|
elif not isinstance(raw.get("features"), dict):
|
|||
|
|
error = "功能开关配置 features 段不是对象,已回退全部默认开启"
|
|||
|
|
else:
|
|||
|
|
source = "file"
|
|||
|
|
value_errors: list[str] = []
|
|||
|
|
for key, value in raw["features"].items():
|
|||
|
|
if key not in FEATURE_CATALOG:
|
|||
|
|
unknown.append(str(key))
|
|||
|
|
continue
|
|||
|
|
if not isinstance(value, bool):
|
|||
|
|
value_errors.append(key) # 非 bool:该键回退默认开启
|
|||
|
|
continue
|
|||
|
|
enabled[key] = value
|
|||
|
|
if value_errors:
|
|||
|
|
error = (
|
|||
|
|
"功能开关配置存在非 bool 取值("
|
|||
|
|
+ ",".join(sorted(value_errors))
|
|||
|
|
+ "),对应功能已回退默认开启"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"version": _SCHEMA_VERSION,
|
|||
|
|
"source": source,
|
|||
|
|
"path": str(path),
|
|||
|
|
"error": error,
|
|||
|
|
"unknown": unknown,
|
|||
|
|
"features": {
|
|||
|
|
key: {"enabled": enabled[key], "label": label}
|
|||
|
|
for key, label in FEATURE_CATALOG.items()
|
|||
|
|
},
|
|||
|
|
}
|