"""Mock 宿主的效果类型注册表与渲染语义(参数带 min/max/choices)。 ``get_effect_params`` 从这张表读出参数元数据,供工具适配层回填 tool schema (计划文档 §3.2:参数 min/max/choices 由 get_params 类信息回填)。 """ from __future__ import annotations from dataclasses import dataclass from typing import Any @dataclass(frozen=True) class ParamSpec: """单个效果参数的定义。""" name: str type: str # number | string | boolean min: float | None = None max: float | None = None choices: tuple[str, ...] | None = None default: Any = None description: str = "" def to_json(self) -> dict: out: dict = {"name": self.name, "type": self.type, "default": self.default} if self.min is not None: out["min"] = self.min if self.max is not None: out["max"] = self.max if self.choices is not None: out["choices"] = list(self.choices) if self.description: out["description"] = self.description return out MOCK_EFFECT_TYPES: dict[str, dict] = { "brightness": { "description": "亮度调整(level 为 -1..1 的亮度增量)", "params": [ ParamSpec("level", "number", min=-1.0, max=1.0, default=0.0, description="亮度增量"), ParamSpec("enabled", "boolean", default=True, description="是否启用"), ], }, "color_shift": { "description": "色相偏移(0..360 度)", "params": [ ParamSpec("hue", "number", min=0.0, max=360.0, default=0.0, description="色相偏移角度"), ParamSpec("enabled", "boolean", default=True), ], }, "crop": { "description": "裁切(各边 0..0.95 比例)", "params": [ ParamSpec("left", "number", min=0.0, max=0.95, default=0.0), ParamSpec("top", "number", min=0.0, max=0.95, default=0.0), ParamSpec("right", "number", min=0.0, max=0.95, default=0.0), ParamSpec("bottom", "number", min=0.0, max=0.95, default=0.0), ], }, "blur": { "description": "模糊(像素半径,近似盒式平均)", "params": [ ParamSpec("radius", "number", min=0.0, max=50.0, default=0.0), ParamSpec("enabled", "boolean", default=True), ], }, "overlay": { "description": "叠加混合(blend_mode 带 choices,供参数回填测试)", "params": [ ParamSpec( "blend_mode", "string", choices=("normal", "screen", "multiply"), default="normal", ), ParamSpec("opacity", "number", min=0.0, max=1.0, default=0.5), ], }, } def effect_spec(effect_type: str) -> dict: spec = MOCK_EFFECT_TYPES.get(effect_type) if spec is None: raise KeyError(effect_type) return spec def validate_param(effect_type: str, name: str, value: Any) -> None: """校验参数值是否满足注册表的类型/范围/choices,非法抛 ValueError。""" spec = effect_spec(effect_type) pspec = next((p for p in spec["params"] if p.name == name), None) if pspec is None: raise ValueError( f"效果 {effect_type} 无参数 {name!r}(可选: {[p.name for p in spec['params']]})" ) ok = _valid_value(pspec, value) if not ok: raise ValueError( f"参数 {name}={value!r} 非法:类型 {pspec.type}" + (f",范围 {pspec.min}..{pspec.max}" if pspec.min is not None or pspec.max is not None else "") + (f",可选 {list(pspec.choices)}" if pspec.choices is not None else "") ) def _valid_value(pspec: ParamSpec, value: Any) -> bool: if pspec.type == "boolean": return isinstance(value, bool) if pspec.type == "string": if not isinstance(value, str): return False if pspec.choices is not None and value not in pspec.choices: return False return True if pspec.type == "number": if not isinstance(value, (int, float)) or isinstance(value, bool): return False if pspec.min is not None and value < pspec.min: return False if pspec.max is not None and value > pspec.max: return False return True return False