Files
my-v0-server/app/schemas/component_resource.py
T

180 lines
7.6 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.
"""前端组件目录及查询结果"""
from typing import Literal, Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
from app.schemas.base import ExtensibleModel, JsonObject, NonBlankString
from app.schemas.material_field import MISSING, FieldGroup, lookup
from app.schemas.screen import Layout, PositiveNumber
class ComponentSummary(BaseModel):
key: NonBlankString # 目录编号
type: NonBlankString # 前端实际组件类型
name: NonBlankString # 组件名称
description: NonBlankString # 组件用途
default_width: PositiveNumber # 默认宽度,单位 px
default_height: PositiveNumber # 默认高度,单位 px
class MaterialTemplate(ExtensibleModel):
type: NonBlankString
name: NonBlankString
layout: Layout
props: JsonObject = Field(default_factory=dict)
style: JsonObject | None = None
events: list[JsonObject] = Field(default_factory=list)
class ComponentDefinition(ExtensibleModel):
model_config = ConfigDict(serialize_by_alias=True)
key: NonBlankString = Field(description="目录中的唯一编号,不是 Screen 节点 ID")
type: NonBlankString = Field(
description="前端实际注册的节点 type,不根据图表名称猜测"
)
name: NonBlankString = Field(description="组件名称")
description: NonBlankString = Field(description="组件用途和支持的展示方式")
tags: list[NonBlankString] = Field(
default_factory=list, description="用于检索的分类和关键词"
)
renderer: NonBlankString = Field(description="渲染能力标识,当前从前端实现映射而来")
group: NonBlankString
icon: NonBlankString | None = None
schema_: MaterialTemplate = Field(
alias="schema", description="完整默认物料配置,不包含实例 ID"
)
field_groups: list[NonBlankString | FieldGroup] = Field(
default_factory=list,
description="字段分组:字符串引用目录的共享分组,对象为本物料独有的分组;"
"前端属性面板的 setters 由它派生,不单独保存",
)
eventOptions: list[JsonObject] = Field(default_factory=list)
sample_data: bool = Field(
default=False, description="默认配置是否含示例数据,不能视为真实业务数据"
)
usage_notes: list[NonBlankString] = Field(
default_factory=list, description="配置、数据绑定及使用限制说明"
)
def template(self) -> JsonObject:
"""默认配置的 JSON;style 为 null 表示未设置,字段默认值按路径不存在处理"""
data = self.schema_.model_dump(mode="json")
if data.get("style") is None:
data.pop("style", None)
return data
@model_validator(mode="after")
def consistent_type(self) -> Self:
if self.type != self.schema_.type:
raise ValueError("目录 type 必须与 schema.type 一致")
if "id" in self.schema_.model_extra:
raise ValueError("默认物料配置不能包含实例 ID")
return self
def _default_problem(field, template: JsonObject) -> str | None:
"""默认值只有一个来源:默认配置有该路径时取它,没有时字段必须声明 default"""
try:
current = lookup(template, field.segments)
except ValueError:
return "的路径与默认配置结构冲突"
if current is MISSING:
if field.default is None:
return "在默认配置中不存在,需要声明 default"
error = field.value_error(field.default)
return f"的 default {error}" if error else None
if field.default is not None:
return "在默认配置中已有值,不能再声明 default"
error = field.value_error(current)
return f"的默认配置值 {error}" if error else None
class ComponentCatalog(ExtensibleModel):
version: NonBlankString = Field(
description="前端组件目录版本,后续用于检查是否与前端匹配"
)
components: list[ComponentDefinition]
field_groups: list[FieldGroup] = Field(
default_factory=list, description="多个物料共用的字段分组,物料按 key 引用"
)
@model_validator(mode="after")
def unique_keys(self) -> Self:
keys = [component.key for component in self.components]
if len(keys) != len(set(keys)):
raise ValueError("组件目录 key 不能重复")
shared = [group.key for group in self.field_groups]
if len(shared) != len(set(shared)):
raise ValueError("共享字段分组 key 不能重复")
for component in self.components:
groups = [
group.key if isinstance(group, FieldGroup) else group
for group in component.field_groups
]
if len(groups) != len(set(groups)):
raise ValueError(f"物料 {component.key} 的字段分组 key 重复")
if missing := [
group
for group in component.field_groups
if isinstance(group, str) and group not in shared
]:
raise ValueError(f"物料 {component.key} 引用了不存在的共享分组: {missing}")
if shadowed := [
group.key
for group in component.field_groups
if isinstance(group, FieldGroup) and group.key in shared
]:
raise ValueError(f"物料 {component.key} 的独有分组与共享分组重名: {shadowed}")
by_key = {group.key: group for group in self.field_groups}
fields = [
field
for group in component.field_groups
for field in (by_key[group] if isinstance(group, str) else group).fields
]
paths = [field.path for field in fields]
if duplicated := sorted({path for path in paths if paths.count(path) > 1}):
raise ValueError(f"物料 {component.key} 的字段路径重复: {duplicated}")
template = component.template()
for field in fields:
problem = _default_problem(field, template)
if problem:
raise ValueError(f"物料 {component.key} 字段 {field.path} {problem}")
return self
class ComponentQuery(ExtensibleModel):
key: NonBlankString | None = Field(default=None, description="精确匹配目录编号")
type: NonBlankString | None = Field(
default=None, description="精确匹配前端节点类型,可能对应多个预设"
)
keyword: NonBlankString | None = Field(
default=None, description="按名称、说明和标签查询,多个条件同时满足"
)
class ComponentResult(ExtensibleModel):
status: Literal["resolved", "not_found", "unsupported"]
components: list[ComponentDefinition] = Field(default_factory=list)
catalog_version: NonBlankString | None = None
message: NonBlankString
def to_planner_context(self) -> JsonObject:
# 显式提取摘要,避免默认数据、事件和扩展字段进入规划提示词
return {
"status": self.status,
"catalog_version": self.catalog_version,
"message": self.message,
"components": [
ComponentSummary(
key=component.key,
type=component.type,
name=component.name,
description=component.description,
default_width=component.schema_.layout.width,
default_height=component.schema_.layout.height,
).model_dump(mode="json")
for component in self.components
],
}