fix:处理模型错误添加大量文本的bug

This commit is contained in:
2026-10-14 01:08:50 -07:00
parent a80727b7a0
commit 41b769adde
35 changed files with 388 additions and 149 deletions
+174 -2
View File
@@ -1,7 +1,10 @@
"""按物料展开字段定义:解析共享分组引用、取字段默认值、派生前端属性面板 setters"""
import re
from pydantic import JsonValue
from app.schemas.base import JsonObject
from app.schemas.component_resource import (
ComponentCatalog,
ComponentDefinition,
@@ -61,13 +64,35 @@ def panel_setters(groups: list[FieldGroup]) -> list[dict]:
def frontend_catalog(catalog: ComponentCatalog) -> dict:
"""前端物料接口的数据:在保存的定义上附加由字段定义派生的 setters"""
"""前端物料接口的数据:在保存的定义上附加由字段定义派生的 setters 和 defaults"""
data = catalog.model_dump(mode="json", by_alias=True)
for item, component in zip(data["components"], catalog.components, strict=True):
item["setters"] = panel_setters(resolve_field_groups(catalog, component))
item["defaults"] = structural_defaults(catalog, component)
return data
def structural_defaults(
catalog: ComponentCatalog, component: ComponentDefinition
) -> JsonObject:
"""默认配置去掉示例内容(字段定义里标了 example 的标题、数据……)后剩下的结构默认值
前端打开大屏时用它给旧节点补上物料后来新增的字段;不用完整的默认配置,否则用户删掉的标题
会被补回成示例标题,真实大屏上又出现演示内容
"""
defaults = component.template()
for field in component_fields(catalog, component):
if field.example:
*parents, last = field.segments
try:
container = lookup(defaults, parents)
except ValueError:
continue
if isinstance(container, dict) and isinstance(last, str):
container.pop(last, None)
return defaults
# ---- 字段查询:MCP 工具、动作提示词和动作校验共用同一套逻辑 ----
@@ -128,7 +153,15 @@ def describe_field(
"""给模型看的字段说明:省略空值和属性面板专用的信息"""
data = field.model_dump(
mode="json",
exclude={"default", "panel", "span", "editable", "themed"},
exclude={
"default",
"panel",
"span",
"editable",
"themed",
"example",
"identifies",
},
exclude_none=True,
exclude_defaults=True,
)
@@ -136,6 +169,9 @@ def describe_field(
if field.themed:
# 默认为 null:没单独设置时画面上按画布主题取色
data["follows_theme"] = True
if field.example:
# 默认值是演示内容,要按实际内容写,不能当成合理的默认
data["default_is_example"] = True
if not field.editable:
data["editable"] = False
data["group"] = group.key
@@ -257,3 +293,139 @@ def check_update_field(
raise ValueError(f"字段 {path}({field.label})不允许修改")
if error := field.value_error(action.value):
raise ValueError(f"字段 {path}({field.label}){error}")
def component_fields(
catalog: ComponentCatalog, component: ComponentDefinition
) -> list[MaterialField]:
return [
field
for group in resolve_field_groups(catalog, component)
for field in group.fields
]
def check_add_fields(
catalog: ComponentCatalog, node_type: str, written: JsonValue
) -> None:
"""新增组件时模型显式写的字段,和 update 用同一套字段定义检查;不合法时一次列出全部问题
没写的字段来自默认配置,载入目录时已经检查过;物料没有定义的路径不检查,新增时可以整段写 option
"""
problems = []
for field in component_fields(catalog, find_material(catalog, node_type)):
try:
value = lookup(written, field.segments)
except ValueError:
problems.append(
f"字段 {field.path}({field.label})所在的上一层结构和物料不同,"
"按 get_component_materials 返回的默认配置写"
)
continue
if value is not MISSING and (error := field.value_error(value)):
problems.append(f"字段 {field.path}({field.label}){error}")
if problems:
raise ValueError(";".join(problems))
def retained_examples(
catalog: ComponentCatalog | None, node: dict
) -> list[tuple[MaterialField, JsonValue]]:
"""组件上还和默认配置一样的示例内容(标题、系列名、数据……),按字段定义的 example 判断
只比较现在的值,不管是新增时没写、写了一样的值,还是之后又改回来;物料不存在时为空
"""
if catalog is None:
return []
try:
component = find_material(catalog, node["type"])
except ValueError:
return []
template = component.template()
retained = []
for field in component_fields(catalog, component):
if not field.example:
continue
try:
current = lookup(node, field.segments)
except ValueError:
continue
default = lookup(template, field.segments)
if current is not MISSING and current == default:
retained.append((field, default))
return retained
# 摘要里每段文字的长度上限:文本组件的内容可能很长,认出组件只需要开头
IDENTITY_MAX_CHARS = 40
def identifying_texts(catalog: ComponentCatalog | None, node: dict) -> list[str]:
"""组件在画面上的名字或内容:字段定义里标了 identifies 的字段的文字,过长时截断
路径途经数组而字段按对象写时(例如 ECharts 的多个 title),每一项都取
"""
if catalog is None:
return []
try:
component = find_material(catalog, node["type"])
except ValueError:
return []
texts = []
for field in component_fields(catalog, component):
if field.identifies:
texts.extend(
text
if len(text) <= IDENTITY_MAX_CHARS
else text[:IDENTITY_MAX_CHARS] + "…"
for value in _values_at(node, field.segments)
if isinstance(value, str) and (text := value.strip())
)
return texts
# 识别字段(标题、名称、文本内容)在画面上是一行短文字。换行、超过这个长度或带句号分号时是一段话:
# 线上问答之后"把结果放到大屏上",模型把"关键发现 1. …"、"注:……;……。"写进了文本组件
LABEL_MAX_CHARS = 60
SENTENCE_MARKS = re.compile(r"[。;;]")
def is_label(text: str) -> bool:
text = text.strip()
return (
"\n" not in text
and len(text) <= LABEL_MAX_CHARS
and not SENTENCE_MARKS.search(text)
)
def paragraph_texts(
catalog: ComponentCatalog | None, node: dict, paths: set[str] | None = None
) -> list[tuple[MaterialField, str]]:
"""组件的识别字段里写成一段话的文字;paths 给出时只看这些字段"""
if catalog is None:
return []
try:
component = find_material(catalog, node["type"])
except ValueError:
return []
return [
(field, value)
for field in component_fields(catalog, component)
if field.identifies and (paths is None or field.path in paths)
for value in _values_at(node, field.segments)
if isinstance(value, str) and not is_label(value)
]
def _values_at(value: JsonValue, segments: list[str | int]) -> list[JsonValue]:
if not segments:
return [value]
head, *rest = segments
if isinstance(value, dict) and isinstance(head, str) and head in value:
return _values_at(value[head], rest)
if isinstance(value, list) and isinstance(head, int) and 0 <= head < len(value):
return _values_at(value[head], rest)
if isinstance(value, list) and isinstance(head, str):
return [found for item in value for found in _values_at(item, segments)]
return []
-1
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:graph-new-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
+2 -2
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:chart-2-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
@@ -49,7 +48,8 @@
"系列名",
"数据名称"
],
"panel": false
"panel": false,
"example": "label"
},
{
"path": "props.option.series.0.barWidth",
+8 -5
View File
@@ -15,7 +15,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:chart-2-linear",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作",
@@ -72,7 +71,8 @@
"aliases": [
"柱子名称"
],
"panel": false
"panel": false,
"example": "label"
},
{
"path": "props.option.series.1.name",
@@ -82,7 +82,8 @@
"aliases": [
"折线名称"
],
"panel": false
"panel": false,
"example": "label"
},
{
"path": "props.option.series.0.barWidth",
@@ -109,7 +110,8 @@
"span": 12,
"aliases": [
"左轴标题"
]
],
"example": "label"
},
{
"path": "props.option.yAxis.1.name",
@@ -119,7 +121,8 @@
"span": 12,
"aliases": [
"右轴标题"
]
],
"example": "label"
},
{
"path": "props.option.yAxis.1.axisLabel.formatter",
+9 -4
View File
@@ -86,7 +86,9 @@
"图表名称",
"图表标题",
"标题文字"
]
],
"example": "label",
"identifies": true
},
{
"path": "props.option.title.textStyle.color",
@@ -362,7 +364,8 @@
"示例数据",
"数据集"
],
"panel": false
"panel": false,
"example": "data"
}
]
},
@@ -443,7 +446,8 @@
"系列名",
"数据名称"
],
"panel": false
"panel": false,
"example": "label"
},
{
"path": "props.option.series.0.smooth",
@@ -534,7 +538,8 @@
"系列名",
"数据名称"
],
"panel": false
"panel": false,
"example": "label"
},
{
"path": "props.option.series.0.label.show",
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:filter-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
-1
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:speedometer-middle-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
+2 -2
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:align-left-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
@@ -133,7 +132,8 @@
"系列名",
"数据名称"
],
"panel": false
"panel": false,
"example": "label"
},
{
"path": "props.option.series.0.barWidth",
@@ -15,7 +15,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:widget-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作",
-1
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:graph-up-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
@@ -15,7 +15,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:battery-charge-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
@@ -120,7 +119,8 @@
"系列名",
"数据名称"
],
"panel": false
"panel": false,
"example": "label"
}
]
},
-1
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:pie-chart-2-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
@@ -15,7 +15,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:pie-chart-3-linear",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作",
-1
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:radar-2-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
-1
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:pie-chart-3-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
-1
View File
@@ -14,7 +14,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:pie-chart-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
+2 -2
View File
@@ -12,7 +12,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:chart-square-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
@@ -63,7 +62,8 @@
"系列名",
"数据名称"
],
"panel": false
"panel": false,
"example": "label"
}
]
},
@@ -14,7 +14,6 @@
"renderer": "echarts",
"group": "charts",
"icon": "solar:chart-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作",
@@ -65,7 +64,8 @@
"aliases": [
"图例名称"
],
"panel": false
"panel": false,
"example": "label"
},
{
"path": "props.option.series.1.name",
@@ -75,7 +75,8 @@
"aliases": [
"图例名称"
],
"panel": false
"panel": false,
"example": "label"
},
{
"path": "props.option.series.2.name",
@@ -85,7 +86,8 @@
"aliases": [
"图例名称"
],
"panel": false
"panel": false,
"example": "label"
}
]
},
+5 -3
View File
@@ -15,7 +15,6 @@
"renderer": "stat",
"group": "info",
"icon": "solar:graph-up-bold",
"sample_data": true,
"usage_notes": [
"默认数值仅用于演示,不是真实业务数据",
"一个指标卡只显示一个指标,多个指标用多个指标卡并排",
@@ -43,7 +42,9 @@
"名称",
"指标名称",
"卡片标题"
]
],
"example": "label",
"identifies": true
},
{
"path": "props.value",
@@ -54,7 +55,8 @@
"数字",
"指标值",
"数据"
]
],
"example": "data"
},
{
"path": "props.prefix",
+4 -3
View File
@@ -3,7 +3,7 @@
"key": "text",
"type": "text",
"name": "文本",
"description": "显示一段文字,常用作大屏标题、说明或标签",
"description": "显示文字,用作大屏标题、小标题或标签",
"tags": [
"info",
"文本",
@@ -12,7 +12,6 @@
"renderer": "text",
"group": "info",
"icon": "solar:text-bold",
"sample_data": false,
"usage_notes": [
"events 中代码只作为资料保存,不由后端执行"
],
@@ -33,7 +32,9 @@
"文本内容",
"标题文字",
"名字"
]
],
"example": "label",
"identifies": true
}
]
},