Skip to content

fix: validate plugin config schema metadata#8257

Open
he-yufeng wants to merge 1 commit into
AstrBotDevs:masterfrom
he-yufeng:fix/config-schema-validation
Open

fix: validate plugin config schema metadata#8257
he-yufeng wants to merge 1 commit into
AstrBotDevs:masterfrom
he-yufeng:fix/config-schema-validation

Conversation

@he-yufeng
Copy link
Copy Markdown
Contributor

@he-yufeng he-yufeng commented May 20, 2026

Summary

Fixes #8252.

This tightens plugin _conf_schema.json handling so invalid metadata is rejected before it reaches the WebUI:

  • add dict to the default value map so the documented schema type works
  • validate default values against their declared type
  • require options to be a list
  • require obvious_hint to be boolean
  • require slider to be used only on int/float fields with numeric min/max/step
  • update the Chinese plugin config docs for these constraints

To verify

  • python -m pytest tests/unit/test_config.py -q -p no:cacheprovider
  • python -m ruff check astrbot/core/config/astrbot_config.py astrbot/core/config/default.py tests/unit/test_config.py
  • python -m py_compile astrbot/core/config/astrbot_config.py astrbot/core/config/default.py tests/unit/test_config.py
  • git diff --check

Summary by Sourcery

Validate plugin configuration schema metadata and document the supported constraints.

New Features:

  • Support the documented dict type in plugin configuration schemas.

Bug Fixes:

  • Reject plugin configuration schema entries with invalid metadata or mismatched default values before they reach the WebUI.

Enhancements:

  • Enforce type-appropriate validation for schema metadata such as default values, options lists, obvious_hint flags, slider definitions, and nested object items.

Documentation:

  • Clarify Chinese plugin configuration documentation to describe the required types and constraints for schema fields like default, obvious_hint, options, and slider.

Tests:

  • Add unit tests covering dict schema defaults and schema metadata validation failures for invalid defaults, options, obvious_hint, and slider usage.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. feature:plugin The bug / feature is about AstrBot plugin system. labels May 20, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The validators mapping inside _validate_default is rebuilt on every call and must be kept manually in sync with DEFAULT_VALUE_MAP; consider centralizing type metadata (e.g., a single shared registry of type → default/validator) to avoid divergence and reduce overhead.
  • The _is_number helper uses isinstance(value, int | float), which relies on the 3.10+ union syntax in isinstance; if this module is intended to run on earlier Python versions, switch to isinstance(value, (int, float)) to keep compatibility.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `validators` mapping inside `_validate_default` is rebuilt on every call and must be kept manually in sync with `DEFAULT_VALUE_MAP`; consider centralizing type metadata (e.g., a single shared registry of type → default/validator) to avoid divergence and reduce overhead.
- The `_is_number` helper uses `isinstance(value, int | float)`, which relies on the 3.10+ union syntax in `isinstance`; if this module is intended to run on earlier Python versions, switch to `isinstance(value, (int, float))` to keep compatibility.

## Individual Comments

### Comment 1
<location path="docs/zh/dev/star/guides/plugin-config.md" line_range="50" />
<code_context>
-- `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`,则不会在管理面板上显示。
</code_context>
<issue_to_address>
**question:** The mention of `file` in `list/file/template_list` seems inconsistent with the supported types listed above.

In the `type` description, the supported values are `string`, `text`, `int`, `float`, `bool`, `object`, `list`, `dict`, `template_list`, but the `default` description additionally mentions `file` (in `list/file/template_list`). Please either add `file` to the supported `type` list (if valid) or remove it from the `default` description to keep the docs consistent.
</issue_to_address>

### Comment 2
<location path="astrbot/core/config/astrbot_config.py" line_range="134" />
<code_context>
-        def _parse_schema(schema: dict, conf: dict) -> None:
-            for k, v in schema.items():
-                if v["type"] not in DEFAULT_VALUE_MAP:
+        def _is_number(value) -> bool:
+            return isinstance(value, int | float) and not isinstance(value, bool)
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the new validation helpers and splitting them into smaller top-level functions so `_config_schema_to_default_config` stays focused on building the config structure.

You can keep all the new validation but reduce complexity by:

1. **Lifting helpers out of `_config_schema_to_default_config`**
2. **Centralizing type → validator mapping instead of rebuilding `validators` inline**
3. **Splitting the object‑specific validation into a separate helper to flatten `_validate_schema_item`**

### 1. Move helpers to private methods / module helpers

Instead of defining `_is_number`, `_validate_default`, and `_validate_schema_item` as inner functions, lift them out. That makes `_config_schema_to_default_config` easier to scan and keeps helpers testable.

```python
# module or class level

def _is_number(value) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool)


TYPE_VALIDATORS: dict[str, Callable[[Any], bool]] = {
    "int": lambda v: isinstance(v, int) and not isinstance(v, bool),
    "float": _is_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),
    "template_list": lambda v: isinstance(v, list),
    "object": lambda v: isinstance(v, dict),
    "dict": lambda v: isinstance(v, dict),
}


def _validate_default(field: str, typ: str, default) -> None:
    validator = TYPE_VALIDATORS.get(typ)
    if not validator:
        raise TypeError(
            f"不受支持的配置类型 {typ}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}",
        )
    if not validator(default):
        raise TypeError(f"配置项 {field} 的 default 与类型 {typ} 不匹配")
```

Then your method reduces to wiring these helpers:

```python
def _config_schema_to_default_config(self, schema: dict) -> dict:
    conf: dict = {}

    def _parse_schema(schema: dict, conf: dict) -> None:
        for k, v in schema.items():
            self._validate_schema_item(k, v)  # or module-level function

            default = v["default"] if "default" in v else DEFAULT_VALUE_MAP[v["type"]]

            if v["type"] == "object":
                conf[k] = {}
                _parse_schema(v["items"], conf[k])
            elif v["type"] == "template_list":
                conf[k] = default
            else:
                conf[k] = default

    _parse_schema(schema, conf)
    return conf
```

(You can keep `_parse_schema` inner if you want, but it’s now focused only on building `conf`.)

### 2. Split `_validate_schema_item` into small helpers

This reduces nesting and keeps each concern local.

```python
def _validate_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_number(slider.get(key)) for key in ("min", "max", "step")
    ):
        raise TypeError(f"配置项 {field} 的 slider 必须包含数字 min/max/step")


def _validate_object_items(field: str, item: dict) -> None:
    if not isinstance(item.get("items"), dict):
        raise TypeError(f"配置项 {field} 的 items 必须是对象")
    for child_key, child_item in item["items"].items():
        _validate_schema_item(f"{field}.{child_key}", child_item)
```

Then `_validate_schema_item` becomes flatter:

```python
def _validate_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_slider(field, typ, item["slider"])

    default = item["default"] if "default" in item else DEFAULT_VALUE_MAP[typ]
    _validate_default(field, typ, default)

    if typ == "object":
        _validate_object_items(field, item)
```

This retains your validation behavior but removes nested inner definitions, centralizes the type/validator logic, and keeps `_config_schema_to_default_config` focused on its primary job.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docs/zh/dev/star/guides/plugin-config.md
Comment thread astrbot/core/config/astrbot_config.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive schema validation for configuration items in AstrBotConfig. It adds internal validation logic to ensure that default values, options, and slider configurations match their declared types (e.g., int, float, bool, list). Additionally, it adds support for a dict type, updates the documentation to reflect these metadata requirements, and includes unit tests to verify the new validation rules. I have no feedback to provide.

@he-yufeng he-yufeng force-pushed the fix/config-schema-validation branch from 987fd81 to 270d48e Compare May 20, 2026 14:27
Copy link
Copy Markdown
Contributor Author

Updated to address the review feedback:

  • moved the schema default/slider/item validation helpers out of _config_schema_to_default_config
  • centralized the type validator mapping so it is no longer rebuilt on each schema item
  • changed number validation to isinstance(value, (int, float)) while still excluding bool
  • added file to the documented supported type list so it matches the existing default map and the file schema section below

Validation:

  • ./.venv/Scripts/python.exe -m pytest tests/unit/test_config.py -q -p no:cacheprovider (45 passed)
  • ./.venv/Scripts/python.exe -m py_compile astrbot/core/config/astrbot_config.py tests/unit/test_config.py
  • ./.venv/Scripts/python.exe -m ruff check astrbot/core/config/astrbot_config.py tests/unit/test_config.py
  • git diff --check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:plugin The bug / feature is about AstrBot plugin system. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] _conf_schema.json 缺少类型校验和文档测试

1 participant