fix(web): solve p0 mapping, nested validation, and add health checks

这个提交包含在:
2026-01-21 16:01:40 +08:00
父节点 e30cd6d93f
当前提交 85787fb572
共修改 7 个文件,包含 232 行新增和 23 行删除
+20 -1
查看文件
@@ -178,10 +178,11 @@ def show_algo_library():
# 环境状态概览 # 环境状态概览
with st.expander("🖥️ 当前环境能力", expanded=False): with st.expander("🖥️ 当前环境能力", expanded=False):
col1, col2, col3 = st.columns(3) col1, col2, col3, col4 = st.columns(4)
col1.metric("Python", "✅ 可用") col1.metric("Python", "✅ 可用")
col2.metric("MATLAB/Octave", "✅ 可用" if "matlab" in available_types else "❌ 不可用") col2.metric("MATLAB/Octave", "✅ 可用" if "matlab" in available_types else "❌ 不可用")
col3.metric("C++ 运行时", "✅ 可用" if "cpp" in available_types else "❌ 不可用") col3.metric("C++ 运行时", "✅ 可用" if "cpp" in available_types else "❌ 不可用")
col4.metric("Web 桥接", "✅ 可用" if "web" in available_types else "❌ 不可用")
st.json(env) st.json(env)
st.subheader("已注册算法") st.subheader("已注册算法")
@@ -208,6 +209,16 @@ def show_algo_library():
st.write(f"**入口**: `{algo.get('lib_path', 'N/A')}::{algo.get('function', 'N/A')}`") st.write(f"**入口**: `{algo.get('lib_path', 'N/A')}::{algo.get('function', 'N/A')}`")
elif algo_type == "matlab": elif algo_type == "matlab":
st.write(f"**入口**: `{algo.get('m_path', 'N/A')}::{algo.get('function', 'N/A')}`") st.write(f"**入口**: `{algo.get('m_path', 'N/A')}::{algo.get('function', 'N/A')}`")
elif algo_type == "web":
endpoint = algo.get('endpoint', 'N/A')
st.write(f"**端点**: `{endpoint}`")
if st.button(f"🌐 测试连接: {algo['name']}", key=f"ping_{algo['name']}"):
from src.env_detect import check_endpoint_active
active, msg = check_endpoint_active(endpoint)
if active:
st.success(f"✅ 联通正常: {msg}")
else:
st.error(f"❌ 无法访问: {msg}")
else: else:
st.write(f"**入口**: `{algo.get('function', 'N/A')}`") st.write(f"**入口**: `{algo.get('function', 'N/A')}`")
@@ -395,6 +406,14 @@ def show_scheduling():
param: (spec or {}).get("field") or param param: (spec or {}).get("field") or param
for param, spec in (algo.get("inputs") or {}).items() for param, spec in (algo.get("inputs") or {}).items()
} }
if algo_type == "web":
# Web 类型使用 Key 映射 (如 transmission_loss -> vis_data.tl_result_path)
out_map = {
field_name: (spec or {}).get("field") or field_name
for field_name, spec in (algo.get("outputs") or {}).items()
}
else:
# 传统类型使用 Index 映射 (如 0 -> clean_data)
out_map = { out_map = {
str((spec or {}).get("index", 0)): field str((spec or {}).get("index", 0)): field
for field, spec in (algo.get("outputs") or {}).items() for field, spec in (algo.get("outputs") or {}).items()
+4 -1
查看文件
@@ -468,7 +468,7 @@ class WebBridge(AlgorithmBridge):
from src.resource_slots import slot_context from src.resource_slots import slot_context
from src.security import ( from src.security import (
validate_endpoint_host, sanitize_output_params, validate_endpoint_host, sanitize_output_params,
generate_run_id, version_tracker generate_run_id, version_tracker, validate_critical_enums
) )
try: try:
@@ -484,6 +484,9 @@ class WebBridge(AlgorithmBridge):
if value is not None: if value is not None:
request_params[http_field] = value request_params[http_field] = value
# 3. 业务逻辑检查:关键参数枚举校验
validate_critical_enums(request_params)
# 3. 清洗输出参数(生成唯一 run_id) # 3. 清洗输出参数(生成唯一 run_id)
run_id = generate_run_id() run_id = generate_run_id()
if self.sanitize_output: if self.sanitize_output:
+46 -4
查看文件
@@ -36,6 +36,15 @@ def _check_ld_paths() -> List[str]:
return paths[:10] # 限制输出长度 return paths[:10] # 限制输出长度
def _cpp_runtime_available() -> bool:
"""检测 C++ 运行时能力(不依赖编译器)"""
try:
import ctypes # noqa: F401
except Exception:
return False
return platform.system().lower() in {"linux", "darwin", "windows"}
def collect_env_info() -> Dict[str, Any]: def collect_env_info() -> Dict[str, Any]:
"""收集完整的环境信息""" """收集完整的环境信息"""
compilers = { compilers = {
@@ -45,6 +54,8 @@ def collect_env_info() -> Dict[str, Any]:
"clang++": shutil.which("clang++"), "clang++": shutil.which("clang++"),
"cl": shutil.which("cl"), # MSVC "cl": shutil.which("cl"), # MSVC
} }
compile_available = any(compilers.values())
runtime_available = _cpp_runtime_available()
return { return {
"python": { "python": {
@@ -64,11 +75,15 @@ def collect_env_info() -> Dict[str, Any]:
}, },
"cpp": { "cpp": {
"compilers": {k: v for k, v in compilers.items() if v}, "compilers": {k: v for k, v in compilers.items() if v},
"any_available": any(compilers.values()), "compile_available": compile_available,
"runtime_available": runtime_available,
# Backward-compatible alias (historically used as "is cpp usable")
"any_available": runtime_available,
}, },
"ld_paths": _check_ld_paths(), "ld_paths": _check_ld_paths(),
"scipy_available": _has_module("scipy"), "scipy_available": _has_module("scipy"),
"numpy_available": _has_module("numpy"), "numpy_available": _has_module("numpy"),
"web_available": _has_module("requests"),
} }
@@ -80,12 +95,33 @@ def get_available_algo_types() -> List[str]:
if env["matlab"]["engine_available"] or env["matlab"]["oct2py_available"] or env["matlab"]["octave_cli"]: if env["matlab"]["engine_available"] or env["matlab"]["oct2py_available"] or env["matlab"]["octave_cli"]:
types.append("matlab") types.append("matlab")
if env["cpp"]["any_available"]: # C++ 运行时可用即视为可执行(编译器仅影响“从源码编译”能力)
if env["cpp"]["runtime_available"]:
types.append("cpp") types.append("cpp")
if env["web_available"]:
types.append("web")
return types return types
def check_endpoint_active(url: str, timeout: int = 5) -> Tuple[bool, str]:
"""检查远程端点是否存活"""
try:
import requests
# 对根路径发起 GET 请求进行探活
from urllib.parse import urljoin, urlparse
parsed = urlparse(url)
base_url = f"{parsed.scheme}://{parsed.netloc}"
resp = requests.get(base_url, timeout=timeout)
if resp.status_code < 500:
return True, f"Active (HTTP {resp.status_code})"
return False, f"Server Error (HTTP {resp.status_code})"
except Exception as e:
return False, f"Disconnected: {str(e)}"
def get_actionable_suggestions() -> List[Dict[str, str]]: def get_actionable_suggestions() -> List[Dict[str, str]]:
"""针对故障环境提供具体的修复指令建议""" """针对故障环境提供具体的修复指令建议"""
env = collect_env_info() env = collect_env_info()
@@ -106,10 +142,16 @@ def get_actionable_suggestions() -> List[Dict[str, str]]:
}) })
# C++ 建议 # C++ 建议
if not env["cpp"]["any_available"]: if not env["cpp"]["runtime_available"]:
suggestions.append({
"target": "C++ Runtime",
"reason": "当前 Python 环境缺少 ctypes/动态链接支持",
"fix": "请使用包含 ctypes 的标准 Python 发行版,或联系交付方提供兼容运行环境。",
})
elif not env["cpp"]["compile_available"]:
suggestions.append({ suggestions.append({
"target": "C++ Compiler", "target": "C++ Compiler",
"reason": "未检测到 gcc/g++/clang 等编译器", "reason": "未检测到 gcc/g++/clang 等编译器(仅影响“从源码编译”能力)",
"fix": "建议安装基础构建工具: `sudo apt install build-essential`" "fix": "建议安装基础构建工具: `sudo apt install build-essential`"
}) })
+1 -1
查看文件
@@ -86,6 +86,6 @@ class CoreScheduler:
# 按声明的输出类型填充哑元数据,让后续步骤能做类型/维度校验 # 按声明的输出类型填充哑元数据,让后续步骤能做类型/维度校验
spec = output_specs.get(attr) or {} spec = output_specs.get(attr) or {}
expected_type = spec.get("type") or "ndarray" expected_type = spec.get("type") or "ndarray"
setattr(temp_data, attr, _dummy_value(expected_type)) FlowValidator._set_nested_field(temp_data, attr, _dummy_value(expected_type))
return issues return issues
+20
查看文件
@@ -87,6 +87,26 @@ def sanitize_filename(filename: str) -> str:
return re.sub(r'[^\w\-.]', '_', filename) return re.sub(r'[^\w\-.]', '_', filename)
def validate_critical_enums(params: Dict) -> None:
"""
对关键参数进行枚举校验,防止远程服务静默回落 (Silent Fallback)
"""
# 建模时间段白名单
ALLOWED_TIME_TYPES = {
"Annual", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
"Winter", "Spring", "Summer", "Autumn"
}
# 建模方法白名单
ALLOWED_MODEL_TYPES = {"bellhop", "kraken", "ram"}
if "timeType" in params and params["timeType"] not in ALLOWED_TIME_TYPES:
raise ValueError(f"不合法的时间段类型: {params['timeType']}")
if "modelType" in params and params["modelType"] not in ALLOWED_MODEL_TYPES:
raise ValueError(f"不支持的声学模型类型: {params['modelType']}")
class ServiceVersionTracker: class ServiceVersionTracker:
"""服务版本追踪器,用于审计与回滚""" """服务版本追踪器,用于审计与回滚"""
+52 -11
查看文件
@@ -58,17 +58,57 @@ class FlowValidator:
return errors return errors
@staticmethod
def _get_nested_field(obj: Any, path: str) -> Any:
"""支持 'sea_env.modeling.freq_hz' 这种嵌套取值"""
for key in path.split('.'):
if obj is None:
return None
if isinstance(obj, dict):
obj = obj.get(key)
else:
obj = getattr(obj, key, None)
return obj
@staticmethod
def _set_nested_field(obj: Any, path: str, value: Any) -> None:
"""支持 'vis_data.tl_result_path' 这种嵌套写入"""
keys = path.split('.')
for key in keys[:-1]:
if isinstance(obj, dict):
if key not in obj:
obj[key] = {}
obj = obj[key]
else:
if not hasattr(obj, key):
setattr(obj, key, {})
next_obj = getattr(obj, key)
if not isinstance(next_obj, dict):
setattr(obj, key, {})
next_obj = getattr(obj, key)
obj = next_obj
final_key = keys[-1]
if isinstance(obj, dict):
obj[final_key] = value
else:
setattr(obj, final_key, value)
@staticmethod @staticmethod
def check_field_existence(data: WaterSoundData, field_name: str) -> Tuple[bool, str, str]: def check_field_existence(data: WaterSoundData, field_name: str) -> Tuple[bool, str, str]:
"""1. 字段存在性校验""" """1. 字段存在性校验 (支持嵌套路径)"""
if not hasattr(data, field_name) or getattr(data, field_name) is None: val = FlowValidator._get_nested_field(data, field_name)
return False, f"缺少必要字段: {field_name}", f"建议:请在流程前置步骤中增加输出 '{field_name}' 的算法,或手动配置输入。" if val is None:
return False, f"缺少必要字段/属性: {field_name}", f"建议:请在流程前置步骤中增加输出 '{field_name}' 的算法,或手动配置输入。"
return True, "", "" return True, "", ""
@staticmethod @staticmethod
def check_data_type(data: WaterSoundData, field_name: str, expected_type: str) -> Tuple[bool, str, str]: def check_data_type(data: WaterSoundData, field_name: str, expected_type: str) -> Tuple[bool, str, str]:
"""2. 类型校验""" """2. 类型校验 (支持嵌套路径)"""
val = getattr(data, field_name) val = FlowValidator._get_nested_field(data, field_name)
if val is None:
return True, "", "" # 存在性由上一个 check 负责
actual_type = type(val).__name__ actual_type = type(val).__name__
type_map = { type_map = {
@@ -85,9 +125,9 @@ class FlowValidator:
@staticmethod @staticmethod
def check_dimensions(data: WaterSoundData, field_name: str, expected_dim: List[Optional[int]]) -> Tuple[bool, str, str]: def check_dimensions(data: WaterSoundData, field_name: str, expected_dim: List[Optional[int]]) -> Tuple[bool, str, str]:
"""3. 维度校验""" """3. 维度校验 (支持嵌套路径)"""
val = getattr(data, field_name) val = FlowValidator._get_nested_field(data, field_name)
if not isinstance(val, np.ndarray): if val is None or not isinstance(val, np.ndarray):
return True, "", "" # 非数组不校验维度 return True, "", "" # 非数组不校验维度
actual_shape = val.shape actual_shape = val.shape
@@ -115,12 +155,13 @@ class FlowValidator:
return True, "", "" return True, "", ""
if algo_type == "cpp": if algo_type == "cpp":
if env_info.get("cpp", {}).get("any_available"): cpp_info = env_info.get("cpp", {})
if cpp_info.get("runtime_available") or cpp_info.get("any_available"):
return True, "", "" return True, "", ""
return ( return (
False, False,
"当前环境未检测到可用的 C/C++ 工具链", "当前环境未检测到可用的 C++ 动态库运行时能力",
"建议:安装 MinGW/GCC/MSVC(或在交付包中提供已编译的动态库并更新检测策略)。", "建议:使用包含 ctypes 的标准 Python,并确保系统支持动态链接(Linux: glibc/dlopen)。若需从源码编译,再安装 build-essential/clang。",
) )
if algo_type == "matlab": if algo_type == "matlab":
+85 -1
查看文件
@@ -84,6 +84,30 @@ def test_validation():
assert issues[0]['algo'] == "Clean" assert issues[0]['algo'] == "Clean"
print("Scheduler validation flow passed.") print("Scheduler validation flow passed.")
def test_nested_validation():
from src.validator import FlowValidator
from src.models import WaterSoundData
wsd = WaterSoundData()
wsd.sea_env = {"modeling": {"freq_hz": 100}}
# 1. 存在性校验
ok, msg, _ = FlowValidator.check_field_existence(wsd, "sea_env.modeling.freq_hz")
assert ok, f"Nested existence failed: {msg}"
ok, _, _ = FlowValidator.check_field_existence(wsd, "sea_env.modeling.non_existent")
assert not ok, "Nested non-existence should fail"
# 2. 类型校验
ok, msg, _ = FlowValidator.check_data_type(wsd, "sea_env.modeling.freq_hz", "int")
assert ok, f"Nested type check failed: {msg}"
# 3. 嵌套写入仿真
FlowValidator._set_nested_field(wsd, "vis_data.tl_matrix.values", np.array([1, 2, 3]))
assert wsd.vis_data["tl_matrix"]["values"][1] == 2
print("Nested validation test passed.")
def test_sea_trial_pipeline(): def test_sea_trial_pipeline():
print("Starting sea-trial pipeline test...") print("Starting sea-trial pipeline test...")
from src.sample_algorithms import dc_removal_and_bandpass, quality_check, snr_estimation from src.sample_algorithms import dc_removal_and_bandpass, quality_check, snr_estimation
@@ -113,6 +137,7 @@ def test_sea_trial_pipeline():
def test_cpp_bridge(): def test_cpp_bridge():
print("Starting C++ bridge test...") print("Starting C++ bridge test...")
from src.bridges import CppBridge from src.bridges import CppBridge
from src.integrity import sha256_file
lib_path = "src/sample_algos_cpp/simple_ops.so" lib_path = "src/sample_algos_cpp/simple_ops.so"
if not os.path.exists(lib_path): if not os.path.exists(lib_path):
@@ -127,7 +152,8 @@ def test_cpp_bridge():
lib_path, lib_path,
"multiply_by_factor_cpp", "multiply_by_factor_cpp",
{"in_data": "clean_data"}, {"in_data": "clean_data"},
{"0": "final_result"} {"0": "final_result"},
expected_sha256=sha256_file(lib_path),
) )
scheduler = CoreScheduler() scheduler = CoreScheduler()
@@ -143,6 +169,60 @@ def test_cpp_bridge():
assert np.allclose(result.final_result, raw * 1.5) assert np.allclose(result.final_result, raw * 1.5)
print("C++ bridge test passed.") print("C++ bridge test passed.")
def test_cpp_bridge_isolated():
print("Starting C++ bridge isolated test...")
from src.bridges import CppBridge
from src.integrity import sha256_file
lib_path = "src/sample_algos_cpp/simple_ops.so"
if not os.path.exists(lib_path):
print("C++ library not found, skipping isolated CPP test.")
return
raw = np.array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=np.float64)
wsd = WaterSoundData(clean_data=raw)
cpp_bridge = CppBridge(
"GainCPP(Isolated)",
lib_path,
"multiply_by_factor_cpp",
{"in_data": "clean_data"},
{"0": "final_result"},
expected_sha256=sha256_file(lib_path),
isolate=True,
)
scheduler = CoreScheduler()
scheduler.set_flow([cpp_bridge])
result = scheduler.execute(wsd)
if result.status != "finished":
print(f"C++ Bridge Isolated Execution Failed: {result.error_info}")
for l in result.log:
print(f" Log: {l}")
assert result.status == "finished"
assert np.allclose(result.final_result, raw * 1.5)
print("C++ bridge isolated test passed.")
def test_python_loader_sha256():
print("Starting Python loader SHA256 test...")
from src.algo_loader import load_algo_func
from src.integrity import sha256_file
module_file = "src/sample_algorithms.py"
good_hash = sha256_file(module_file)
func = load_algo_func("src.sample_algorithms", "snr_estimation", expected_sha256=good_hash)
assert callable(func)
bad_hash = "0" * 64
try:
load_algo_func("src.sample_algorithms", "snr_estimation", expected_sha256=bad_hash)
assert False, "expected SHA256 mismatch to raise"
except Exception as e:
assert "SHA256" in str(e)
print("Python loader SHA256 test passed.")
def test_matlab_bridge_fallback(): def test_matlab_bridge_fallback():
print("Starting MATLAB bridge fallback test...") print("Starting MATLAB bridge fallback test...")
from src.bridges import MatlabBridge from src.bridges import MatlabBridge
@@ -171,4 +251,8 @@ if __name__ == "__main__":
test_validation() test_validation()
test_sea_trial_pipeline() test_sea_trial_pipeline()
test_cpp_bridge() test_cpp_bridge()
test_cpp_bridge_isolated()
test_python_loader_sha256()
test_matlab_bridge_fallback() test_matlab_bridge_fallback()
test_nested_validation()
print("\nAll Tests Passed Successfully! 🚀")