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()
|
||
},
|
||
}
|