aps-agent/server/agent_core/feature_flags.py

110 lines
4.2 KiB
Python
Raw Normal View History

# ============================================================
# 功能开关(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()
},
}