feat(webui): enhance ConfigSchemaGenerator with field_docs and UI metadata

- Add AttrDocBase.get_class_field_docs() classmethod for class-level field docs extraction
- Merge json_schema_extra (x-widget, x-icon, step) into schema output
- Map Pydantic constraints (ge/le) to minValue/maxValue for frontend compatibility
- Add ge=0, le=1 constraints to ChatConfig.talk_value for validation

Completes Task 1 (including subtasks 1a, 1b, 1c, 1d) of webui-config-visualization-refactor plan.
pull/1496/head
DrSmoothl 2026-02-17 17:05:25 +08:00
parent 19c9c5a39a
commit 278a084c23
No known key found for this signature in database
3 changed files with 26 additions and 2 deletions

View File

@ -5,7 +5,7 @@ import types
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from typing import Union, get_args, get_origin, Tuple, Any, List, Dict, Set, Literal from typing import Any, Dict, List, Literal, Set, Tuple, Union, cast, get_args, get_origin
__all__ = ["ConfigBase", "Field", "AttributeData"] __all__ = ["ConfigBase", "Field", "AttributeData"]
@ -44,6 +44,16 @@ class AttrDocBase:
# 从类定义节点中提取字段文档 # 从类定义节点中提取字段文档
return self._extract_field_docs(class_node, allow_extra_methods) return self._extract_field_docs(class_node, allow_extra_methods)
@classmethod
def get_class_field_docs(cls) -> dict[str, str]:
class_source = cls._get_class_source()
class_node = cls._find_class_node(class_source)
return AttrDocBase._extract_field_docs(
cast(AttrDocBase, cast(Any, cls)),
class_node,
allow_extra_methods=False,
)
@classmethod @classmethod
def _get_class_source(cls) -> str: def _get_class_source(cls) -> str:
"""获取类定义所在文件的完整源代码""" """获取类定义所在文件的完整源代码"""
@ -265,7 +275,7 @@ class ConfigBase(BaseModel, AttrDocBase):
if origin_type in (int, float, str, bool, complex, bytes, Any): if origin_type in (int, float, str, bool, complex, bytes, Any):
continue continue
# 允许嵌套的ConfigBase自定义类 # 允许嵌套的ConfigBase自定义类
if inspect.isclass(origin_type) and issubclass(origin_type, ConfigBase): # type: ignore if isinstance(origin_type, type) and issubclass(cast(type, origin_type), ConfigBase):
continue continue
# 只允许 list, set, dict 三类泛型 # 只允许 list, set, dict 三类泛型
if origin_type not in (list, set, dict, List, Set, Dict, Literal): if origin_type not in (list, set, dict, List, Set, Dict, Literal):

View File

@ -104,6 +104,8 @@ class ChatConfig(ConfigBase):
talk_value: float = Field( talk_value: float = Field(
default=1, default=1,
ge=0,
le=1,
json_schema_extra={ json_schema_extra={
"x-widget": "slider", "x-widget": "slider",
"x-icon": "message-circle", "x-icon": "message-circle",

View File

@ -77,6 +77,18 @@ class ConfigSchemaGenerator:
if options: if options:
schema["options"] = options schema["options"] = options
# Task 1c: Merge json_schema_extra (x-widget, x-icon, step, etc.)
if hasattr(field_info, "json_schema_extra") and field_info.json_schema_extra:
schema.update(field_info.json_schema_extra)
# Task 1d: Map Pydantic constraints to minValue/maxValue (frontend naming convention)
if hasattr(field_info, "metadata") and field_info.metadata:
for constraint in field_info.metadata:
if hasattr(constraint, "ge"):
schema["minValue"] = constraint.ge
if hasattr(constraint, "le"):
schema["maxValue"] = constraint.le
return schema return schema
@staticmethod @staticmethod