diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 4d62becb55..429a1ac3d1 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -18,6 +18,61 @@ logger = logging.getLogger("astrbot") +def _is_config_number(value) -> bool: + return isinstance(value, (int, float)) and not isinstance(value, bool) + + +_SCHEMA_TYPE_VALIDATORS = { + "int": lambda v: isinstance(v, int) and not isinstance(v, bool), + "float": _is_config_number, + "bool": lambda v: isinstance(v, bool), + "string": lambda v: isinstance(v, str), + "text": lambda v: isinstance(v, str), + "list": lambda v: isinstance(v, list), + "file": lambda v: isinstance(v, list), + "object": lambda v: isinstance(v, dict), + "dict": lambda v: isinstance(v, dict), + "template_list": lambda v: isinstance(v, list), +} + + +def _validate_schema_default(field: str, typ: str, default) -> None: + if not _SCHEMA_TYPE_VALIDATORS[typ](default): + raise TypeError(f"配置项 {field} 的 default 与类型 {typ} 不匹配") + + +def _validate_schema_slider(field: str, typ: str, slider: dict) -> None: + if typ not in ("int", "float"): + raise TypeError(f"配置项 {field} 只有 int/float 类型支持 slider") + if not isinstance(slider, dict) or not all( + _is_config_number(slider.get(key)) for key in ("min", "max", "step") + ): + raise TypeError( + f"配置项 {field} 的 slider 必须包含数字 min/max/step", + ) + + +def _validate_config_schema_item(field: str, item: dict) -> None: + typ = item["type"] + if typ not in DEFAULT_VALUE_MAP: + raise TypeError( + f"不受支持的配置类型 {typ}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}", + ) + if "options" in item and not isinstance(item["options"], list): + raise TypeError(f"配置项 {field} 的 options 必须是列表") + if "obvious_hint" in item and not isinstance(item["obvious_hint"], bool): + raise TypeError(f"配置项 {field} 的 obvious_hint 必须是布尔值") + if "slider" in item: + _validate_schema_slider(field, typ, item["slider"]) + if typ == "object" and not isinstance(item.get("items"), dict): + raise TypeError(f"配置项 {field} 的 items 必须是对象") + default = item["default"] if "default" in item else DEFAULT_VALUE_MAP[typ] + _validate_schema_default(field, typ, default) + if typ == "object": + for child_key, child_item in item["items"].items(): + _validate_config_schema_item(f"{field}.{child_key}", child_item) + + class RateLimitStrategy(enum.Enum): STALL = "stall" DISCARD = "discard" @@ -133,10 +188,7 @@ def _config_schema_to_default_config(self, schema: dict) -> dict: def _parse_schema(schema: dict, conf: dict) -> None: for k, v in schema.items(): - if v["type"] not in DEFAULT_VALUE_MAP: - raise TypeError( - f"不受支持的配置类型 {v['type']}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}", - ) + _validate_config_schema_item(k, v) if "default" in v: default = v["default"] else: diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index ce79559bd6..8aac080eed 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -4301,5 +4301,6 @@ "list": [], "file": [], "object": {}, + "dict": {}, "template_list": [], } diff --git a/docs/zh/dev/star/guides/plugin-config.md b/docs/zh/dev/star/guides/plugin-config.md index 8374cdcd1f..711478a95a 100644 --- a/docs/zh/dev/star/guides/plugin-config.md +++ b/docs/zh/dev/star/guides/plugin-config.md @@ -43,14 +43,15 @@ AstrBot 提供了“强大”的配置解析和可视化功能。能够让用户 } ``` -- `type`: **此项必填**。配置的类型。支持 `string`, `text`, `int`, `float`, `bool`, `object`, `list`, `dict`, `template_list`。当类型为 `text` 时,将会可视化为一个更大的可拖拽宽高的 textarea 组件,以适应大文本。 +- `type`: **此项必填**。配置的类型。支持 `string`, `text`, `int`, `float`, `bool`, `object`, `list`, `dict`, `file`, `template_list`。当类型为 `text` 时,将会可视化为一个更大的可拖拽宽高的 textarea 组件,以适应大文本。 - `description`: 可选。配置的描述。建议一句话描述配置的行为。 - `hint`: 可选。配置的提示信息,表现在上图中右边的问号按钮,当鼠标悬浮在问号按钮上时显示。 -- `obvious_hint`: 可选。配置的 hint 是否醒目显示。如上图的 `token`。 -- `default`: 可选。配置的默认值。如果用户没有配置,将使用默认值。int 是 0,float 是 0.0,bool 是 False,string 是 "",object 是 {},list 是 []。 +- `obvious_hint`: 可选,布尔值。配置的 hint 是否醒目显示;只有同时配置了 `hint` 时才会显示。如上图的 `token`。 +- `default`: 可选。配置的默认值。如果用户没有配置,将使用默认值。int 是 0,float 是 0.0,bool 是 False,string/text 是 "",object/dict 是 {},list/file/template_list 是 []。 - `items`: 可选。如果配置的类型是 `object`,需要添加 `items` 字段。`items` 的内容是这个配置项的子 Schema。理论上可以无限嵌套,但是不建议过多嵌套。 - `invisible`: 可选。配置是否隐藏。默认是 `false`。如果设置为 `true`,则不会在管理面板上显示。 -- `options`: 可选。一个列表,如 `"options": ["chat", "agent", "workflow"]`。提供下拉列表可选项。 +- `options`: 可选,必须是列表,如 `"options": ["chat", "agent", "workflow"]`。提供下拉列表可选项。 +- `slider`: 可选,仅支持 `int` / `float` 类型。必须包含数字类型的 `min`、`max`、`step`。 - `editor_mode`: 可选。是否启用代码编辑器模式。需要 AstrBot >= `v3.5.10`, 低于这个版本不会报错,但不会生效。默认是 false。 - `editor_language`: 可选。代码编辑器的代码语言,默认为 `json`。 - `editor_theme`: 可选。代码编辑器的主题,可选值有 `vs-light`(默认), `vs-dark`。 diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 7afe82ebed..a1cb4c67f1 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -596,6 +596,46 @@ def test_template_list_type(self, temp_config_path): assert config.templates == [] + def test_dict_schema_type(self, temp_config_path): + """Test dict schema type.""" + schema = { + "headers": {"type": "dict"}, + } + + config = AstrBotConfig(config_path=temp_config_path, schema=schema) + + assert config.headers == {} + + @pytest.mark.parametrize( + ("schema", "error"), + [ + ( + {"field": {"type": "string", "default": 1}}, + "default 与类型 string 不匹配", + ), + ( + {"field": {"type": "list", "options": "bad"}}, + "options 必须是列表", + ), + ( + {"field": {"type": "string", "obvious_hint": "yes"}}, + "obvious_hint 必须是布尔值", + ), + ( + {"field": {"type": "float", "slider": {"min": "0", "max": 1, "step": 1}}}, + "slider 必须包含数字 min/max/step", + ), + ( + {"field": {"type": "string", "slider": {"min": 0, "max": 1, "step": 1}}}, + "只有 int/float 类型支持 slider", + ), + ], + ) + def test_schema_metadata_validation(self, temp_config_path, schema, error): + """Test schema metadata validation.""" + with pytest.raises(TypeError, match=error): + AstrBotConfig(config_path=temp_config_path, schema=schema) + def test_nested_object_schema(self, temp_config_path): """Test nested object schema conversion.""" schema = {