feat(annotation): 替换模板配置表单为树形编辑器组件

- 移除 TemplateConfigurationForm 组件并引入 TemplateConfigurationTreeEditor
- 使用 useTagConfig Hook 获取标签配置
- 将自定义XML状态 customXml 替换为 labelConfig
- 删除模板编辑标签页和选择模板状态管理
- 更新XML解析逻辑支持更多对象和标注控件类型
- 添加配置验证功能确保至少包含数据对象和标注控件
- 在模板详情页面使用树形编辑器显示配置详情
- 更新任务创建页面集成新的树形配置编辑器
- 调整预览数据生成功能适配新的XML解析方式
This commit is contained in:
2026-01-23 16:11:59 +08:00
parent 76d06b9809
commit 3f566a0b08
14 changed files with 1383 additions and 900 deletions

View File

@@ -1,7 +1,7 @@
"""
Label Studio Configuration Validation Utilities
"""
from typing import Dict, List, Tuple, Optional
from typing import Dict, List, Tuple, Optional, Set
import xml.etree.ElementTree as ET
from app.module.annotation.config import LabelStudioTagConfig
@@ -13,6 +13,23 @@ class LabelStudioConfigValidator:
def _get_config() -> LabelStudioTagConfig:
"""获取标签配置实例"""
return LabelStudioTagConfig()
@staticmethod
def _get_required_attrs(tag_type: str, is_control: bool) -> List[str]:
config = LabelStudioConfigValidator._get_config()
tag_config = (
config.get_control_config(tag_type)
if is_control
else config.get_object_config(tag_type)
)
required_attrs = tag_config.get("required_attrs", []) if tag_config else []
return required_attrs if isinstance(required_attrs, list) else []
@staticmethod
def _get_control_category(tag_type: str) -> Optional[str]:
config = LabelStudioConfigValidator._get_config()
tag_config = config.get_control_config(tag_type) or {}
return tag_config.get("category")
@staticmethod
def validate_xml(xml_string: str) -> Tuple[bool, Optional[str]]:
@@ -33,24 +50,49 @@ class LabelStudioConfigValidator:
if root.tag != 'View':
return False, "Root element must be <View>"
# 检查是否有对象定义
object_types = config.get_object_types()
objects = [elem for elem in root.iter() if elem is not root and elem.tag in object_types]
control_types = config.get_control_types()
objects = [elem for elem in root.iter() if elem.tag in object_types]
controls = [elem for elem in root.iter() if elem.tag in control_types]
labeling_controls = [
control
for control in controls
if LabelStudioConfigValidator._get_control_category(control.tag) == "labeling"
]
if not objects:
return False, "No data objects (Image, Text, etc.) found"
# 检查是否有控件定义
control_types = config.get_control_types()
controls = [elem for elem in root.iter() if elem is not root and elem.tag in control_types]
if not controls:
return False, "No annotation controls found"
# 验证每个控件
if not labeling_controls:
return False, "No labeling controls found"
object_names = {
obj.get("name") for obj in objects if obj.get("name")
}
# 校验对象必填属性
for obj in objects:
required_attrs = LabelStudioConfigValidator._get_required_attrs(
obj.tag, is_control=False
)
for attr in required_attrs:
if not obj.attrib.get(attr):
return False, f"Object {obj.tag} missing '{attr}' attribute"
if obj.attrib.get("value") and not obj.attrib.get("value", "").startswith("$"):
return False, "Object value must start with '$' (e.g., '$image')"
# 校验控件(布局类仅提示,不作为失败条件)
for control in controls:
valid, error = LabelStudioConfigValidator._validate_control(control)
if not valid:
category = LabelStudioConfigValidator._get_control_category(control.tag)
strict = category == "labeling"
valid, error = LabelStudioConfigValidator._validate_control(
control, object_names, strict
)
if not valid and strict:
return False, f"Control {control.tag}: {error}"
return True, None
except ET.ParseError as e:
@@ -59,7 +101,11 @@ class LabelStudioConfigValidator:
return False, f"Validation error: {str(e)}"
@staticmethod
def _validate_control(control: ET.Element) -> Tuple[bool, Optional[str]]:
def _validate_control(
control: ET.Element,
object_names: Set[str],
strict: bool
) -> Tuple[bool, Optional[str]]:
"""
验证单个控件元素
@@ -72,26 +118,37 @@ class LabelStudioConfigValidator:
config = LabelStudioConfigValidator._get_config()
# 检查必需属性
if 'name' not in control.attrib:
return False, "Missing 'name' attribute"
if 'toName' not in control.attrib:
return False, "Missing 'toName' attribute"
required_attrs = LabelStudioConfigValidator._get_required_attrs(
control.tag, is_control=True
)
for attr in required_attrs:
if not control.attrib.get(attr):
return (False, f"Missing '{attr}' attribute") if strict else (True, None)
# 校验 toName 指向对象
if strict and control.attrib.get("toName"):
to_names = [
name.strip()
for name in control.attrib.get("toName", "").split(",")
if name.strip()
]
invalid = [name for name in to_names if name not in object_names]
if invalid:
return False, f"toName references unknown object(s): {', '.join(invalid)}"
# 检查控件是否需要子元素
if config.requires_children(control.tag):
child_tag = config.get_child_tag(control.tag)
if not child_tag:
return False, f"Configuration error: no child_tag defined for {control.tag}"
return (False, f"Configuration error: no child_tag defined for {control.tag}") if strict else (True, None)
children = control.findall(child_tag)
if not children:
return False, f"{control.tag} must have at least one <{child_tag}> child"
# 检查每个子元素是否有value
return (False, f"{control.tag} must have at least one <{child_tag}> child") if strict else (True, None)
for child in children:
if 'value' not in child.attrib:
return False, f"{child_tag} missing 'value' attribute"
if "value" not in child.attrib or not child.attrib.get("value"):
return (False, f"{child_tag} missing 'value' attribute") if strict else (True, None)
return True, None