Files
my-v0-server/app/actions/batch.py
T
2026-10-08 00:28:29 -07:00

97 lines
4.2 KiB
Python
Raw 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.
"""整批模拟执行:新增组件补全物料默认配置,再按顺序在大屏副本上执行,遇到第一个错误即停止
纯函数,不依赖 agent;agent 的 apply_actions 工具和测试共用
"""
from copy import deepcopy
from app.actions.layout import EXPANDERS
from app.actions.registry import apply_action
from app.actions.update import fit_existing_path
from app.resources.material_fields import check_update_field, find_material
from app.schemas.actions import Action, AddAction, AgentAction, UpdateAction
from app.schemas.capabilities import DATA_BINDING_ERROR, DATA_BINDING_SUPPORTED
from app.schemas.component_resource import ComponentCatalog
from app.schemas.screen import Screen, ScreenNode
class BatchError(ValueError):
"""整批中第 index 个动作(从 0 开始)失败,消息里带上序号和目标,模型据此修正"""
def __init__(self, index: int, action: AgentAction, reason: str):
self.index = index
self.reason = reason
if isinstance(action, AddAction):
target = action.node.id
elif ids := getattr(action, "target_ids", None):
target = "、".join(ids)
else:
target = getattr(action, "target_id", "画布")
super().__init__(f"第 {index + 1} 个动作({action.type} {target}):{reason}")
def merge_config(defaults: dict, overrides: dict) -> dict:
"""对象逐层覆盖,数组和标量整体替换,不修改传入的数据"""
result = deepcopy(defaults)
for key, value in overrides.items():
result[key] = (
merge_config(result[key], value)
if isinstance(result.get(key), dict) and isinstance(value, dict)
else deepcopy(value)
)
return result
def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
"""按 node.type 找物料补全新增组件,模型显式写出的字段优先
sample_data 模型写了就以模型为准(只有它知道是否换成了自己的数据),没写时按物料设置
"""
material = find_material(catalog, action.node.type)
if not DATA_BINDING_SUPPORTED and action.node.model_extra.get("dataId"):
raise ValueError(DATA_BINDING_ERROR)
overrides = action.node.model_dump(mode="json", exclude_unset=True)
node = ScreenNode.model_validate(merge_config(material.template(), overrides))
sample_data = (
action.sample_data
if "sample_data" in action.model_fields_set
else material.sample_data
)
return action.model_copy(update={"node": node, "sample_data": sample_data})
def run_batch(
screen: Screen, catalog: ComponentCatalog, actions: list[AgentAction]
) -> tuple[list[Action], Screen]:
"""从原始大屏开始按顺序模拟执行,返回前端要执行的动作和执行后的大屏;第一个错误抛 BatchError
布局意图(arrange、align、distribute、set_layout)在这里按当时的大屏展开成 move 和 resize
"""
current = screen.model_copy(deep=True)
completed: list[Action] = []
for index, action in enumerate(actions):
try:
if expander := EXPANDERS.get(action.type):
expanded = expander(current, action)
for primitive in expanded:
current = apply_action(current, primitive)
completed.extend(expanded)
continue
if isinstance(action, AddAction):
action = complete_add(action, catalog)
elif isinstance(action, UpdateAction):
# 目标不存在时交给执行器报错
node = next(
(item for item in current.nodes if item.id == action.target_id),
None,
)
if node is not None:
check_update_field(catalog, node.type, action)
# 字段定义允许、但组件中还没有的中间对象,折叠进值里再执行
action = fit_existing_path(node, action)
current = apply_action(current, action)
except ValueError as error: # 含 pydantic ValidationError
raise BatchError(index, action, str(error)) from error
completed.append(action)
return completed, current