Files
Mike-Solar 1b66a70859 P2: tool adapter layer, mock host, frame pipeline, and local web UI
- MockHostAdapter (tests/mock_host): in-memory editing model with opaque
  entity ids, id invalidation, token-bucket rate limiting, snapshot/restore
  with cap, event subscription, and a stdlib-zlib PNG encoder producing
  time-varying non-black frames
- HostToolExecutor: all 26 tool handlers mapped to HostAdapter calls,
  snapshot orchestration for mutating batches, concurrent scan_timeline
  frame pipeline with token bucket and RateLimited backoff, undo_session()
  restoring per-batch snapshots in reverse, get_params min/max/choices
  reflection
- Web UI (core[webui] extra): FastAPI + single-page vanilla JS, chat log,
  pending-confirmation list with approve/reject endpoints (timeout defaults
  to reject), progress, undo-session, snapshot management; SSE push + POST,
  bound to 127.0.0.1
- Host error types (HostError/EntityNotFound/RateLimited) and shared
  TokenBucket
- P2 acceptance chain: import -> place -> split -> ripple delete ->
  effect + param -> frame verification (non-black, pixel changes at cut
  and after effect) -> undo_session restores original state

Tests: 129 passed via uv run pytest (no network)
2026-08-25 02:40:43 +08:00

125 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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