Repository: AstrBotDevs/AstrBot
Pull request #1512: feat: CLI支持部分配置文件项的设定
Author: login: Raven95676
Created: 2025-05-10T07:46:30Z | Merged: 2025-05-10T08:42:53Z

### Motivation

提供通过CLI修改部分配置文件项的方法

### Modifications

新增配置文件修改功能

### Check

<!--如果分支被合并，您的代码将服务于数万名用户！在提交前，请核查一下几点内容-->

- [x] 😊 我的 Commit Message 符合良好的[规范](https://www.conventionalcommits.org/en/v1.0.0/#summary)
- [x] 👀 我的更改经过良好的测试
- [x] 🤓 我确保没有引入新依赖库，或者引入了新依赖库的同时将其添加到了 `requirements.txt` 和 `pyproject.toml` 文件相应位置。
- [x] 😮 我的更改没有引入恶意代码

好的，这是对pull request摘要的中文翻译：

## Sourcery 总结

添加 CLI 配置管理功能，允许用户修改特定的配置文件设置

新功能：
- 引入一个新的 'conf' CLI 命令来管理配置设置
- 支持通过 CLI 设置和检索特定的配置参数

增强功能：
- 实现配置值的验证
- 提供对敏感配置（如密码）的安全处理

<details>
<summary>Original summary in English</summary>

好的，这是翻译成中文的 pull request 总结：

## Sourcery 总结

添加 CLI 配置管理功能，允许用户修改应用程序的特定配置设置

**新功能：**

- 引入一个新的 'conf' CLI 命令来管理配置设置
- 支持通过 CLI 设置和检索特定的配置参数

**增强功能：**

- 实现配置值的验证
- 提供敏感配置项的安全处理

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Add CLI configuration management functionality to allow users to modify specific configuration settings for the application

New Features:
- Introduce a new 'conf' CLI command for managing configuration settings
- Support setting and retrieving specific configuration parameters via CLI

Enhancements:
- Implement configuration value validation
- Provide secure handling of sensitive configuration items

</details>

</details>


Diff:
diff --git a/astrbot/cli/__main__.py b/astrbot/cli/__main__.py
index c8181247e7..f2b6651f59 100644
--- a/astrbot/cli/__main__.py
+++ b/astrbot/cli/__main__.py
@@ -5,7 +5,7 @@
 import click
 import sys
 from . import __version__
-from .commands import init, run, plug
+from .commands import init, run, plug, conf
 
 logo_tmpl = r"""
      ___           _______.___________..______      .______     ______   .___________.
@@ -53,6 +53,7 @@ def help(command_name: str | None) -> None:
 cli.add_command(run)
 cli.add_command(help)
 cli.add_command(plug)
+cli.add_command(conf)
 
 if __name__ == "__main__":
     cli()
diff --git a/astrbot/cli/commands/__init__.py b/astrbot/cli/commands/__init__.py
index d250c8c0f5..9fa9149e22 100644
--- a/astrbot/cli/commands/__init__.py
+++ b/astrbot/cli/commands/__init__.py
@@ -1,5 +1,6 @@
 from .cmd_init import init
 from .cmd_run import run
 from .cmd_plug import plug
+from .cmd_conf import conf
 
-__all__ = ["init", "run", "plug"]
+__all__ = ["init", "run", "plug", "conf"]
diff --git a/astrbot/cli/commands/cmd_conf.py b/astrbot/cli/commands/cmd_conf.py
new file mode 100644
index 0000000000..fea654f209
--- /dev/null
+++ b/astrbot/cli/commands/cmd_conf.py
@@ -0,0 +1,206 @@
+import json
+import click
+import hashlib
+import zoneinfo
+from typing import Any, Callable
+from ..utils import get_astrbot_root, check_astrbot_root
+
+
+def _validate_log_level(value: str) -> str:
+    """验证日志级别"""
+    value = value.upper()
+    if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
+        raise click.ClickException(
+            "日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一"
+        )
+    return value
+
+
+def _validate_dashboard_port(value: str) -> int:
+    """验证 Dashboard 端口"""
+    try:
+        port = int(value)
+        if port < 1 or port > 65535:
+            raise click.ClickException("端口必须在 1-65535 范围内")
+        return port
+    except ValueError:
+        raise click.ClickException("端口必须是数字")
+
+
+def _validate_dashboard_username(value: str) -> str:
+    """验证 Dashboard 用户名"""
+    if not value:
+        raise click.ClickException("用户名不能为空")
+    return value
+
+
+def _validate_dashboard_password(value: str) -> str:
+    """验证 Dashboard 密码"""
+    if not value:
+        raise click.ClickException("密码不能为空")
+    return hashlib.md5(value.encode()).hexdigest()
+
+
+def _validate_timezone(value: str) -> str:
+    """验证时区"""
+    try:
+        zoneinfo.ZoneInfo(value)
+    except Exception:
+        raise click.ClickException(f"无效的时区: {value}，请使用有效的IANA时区名称")
+    return value
+
+
+def _validate_callback_api_base(value: str) -> str:
+    """验证回调接口基址"""
+    if not value.startswith("http://") and not value.startswith("https://"):
+        raise click.ClickException("回调接口基址必须以 http:// 或 https:// 开头")
+    return value
+
+
+# 可通过CLI设置的配置项，配置键到验证器函数的映射
+CONFIG_VALIDATORS: dict[str, Callable[[str], Any]] = {
+    "timezone": _validate_timezone,
+    "log_level": _validate_log_level,
+    "dashboard.port": _validate_dashboard_port,
+    "dashboard.username": _validate_dashboard_username,
+    "dashboard.password": _validate_dashboard_password,
+    "callback_api_base": _validate_callback_api_base,
+}
+
+
+def _load_config() -> dict[str, Any]:
+    """加载或初始化配置文件"""
+    root = get_astrbot_root()
+    if not check_astrbot_root(root):
+        raise click.ClickException(
+            f"{root}不是有效的 AstrBot 根目录，如需初始化请使用 astrbot init"
+        )
+
+    config_path = root / "data" / "cmd_config.json"
+    if not config_path.exists():
+        from astrbot.core.config.default import DEFAULT_CONFIG
+
+        config_path.write_text(
+            json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
+            encoding="utf-8-sig",
+        )
+
+    try:
+        return json.loads(config_path.read_text(encoding="utf-8-sig"))
+    except json.JSONDecodeError as e:
+        raise click.ClickException(f"配置文件解析失败: {str(e)}")
+
+
+def _save_config(config: dict[str, Any]) -> None:
+    """保存配置文件"""
+    config_path = get_astrbot_root() / "data" / "cmd_config.json"
+
+    config_path.write_text(
+        json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig"
+    )
+
+
+def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
+    """设置嵌套字典中的值"""
+    parts = path.split(".")
+    for part in parts[:-1]:
+        if part not in obj:
+            obj[part] = {}
+        elif not isinstance(obj[part], dict):
+            raise click.ClickException(
+                f"配置路径冲突: {'.'.join(parts[: parts.index(part) + 1])} 不是字典"
+            )
+        obj = obj[part]
+    obj[parts[-1]] = value
+
+
+def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
+    """获取嵌套字典中的值"""
+    parts = path.split(".")
+    for part in parts:
+        obj = obj[part]
+    return obj
+
+
+@click.group(name="conf")
+def conf():
+    """配置管理命令
+
+    支持的配置项:
+
+    - timezone: 时区设置 (例如: Asia/Shanghai)
+
+    - log_level: 日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL)
+
+    - dashboard.port: Dashboard 端口
+
+    - dashboard.username: Dashboard 用户名
+
+    - dashboard.password: Dashboard 密码
+
+    - callback_api_base: 回调接口基址
+    """
+    pass
+
+
+@conf.command(name="set")
+@click.argument("key")
+@click.argument("value")
+def set_config(key: str, value: str):
+    """设置配置项的值"""
+    if key not in CONFIG_VALIDATORS.keys():
+        raise click.ClickException(f"不支持的配置项: {key}")
+
+    config = _load_config()
+
+    try:
+        old_value = _get_nested_item(config, key)
+        validated_value = CONFIG_VALIDATORS[key](value)
+        _set_nested_item(config, key, validated_value)
+        _save_config(config)
+
+        click.echo(f"配置已更新: {key}")
+        if key == "dashboard.password":
+            click.echo("  原值: ********")
+            click.echo("  新值: ********")
+        else:
+            click.echo(f"  原值: {old_value}")
+            click.echo(f"  新值: {validated_value}")
+
+    except KeyError:
+        raise click.ClickException(f"未知的配置项: {key}")
+    except Exception as e:
+        raise click.UsageError(f"设置配置失败: {str(e)}")
+
+
+@conf.command(name="get")
+@click.argument("key", required=False)
+def get_config(key: str = None):
+    """获取配置项的值，不提供key则显示所有可配置项"""
+    config = _load_config()
+
+    if key:
+        if key not in CONFIG_VALIDATORS.keys():
+            raise click.ClickException(f"不支持的配置项: {key}")
+
+        try:
+            value = _get_nested_item(config, key)
+            if key == "dashboard.password":
+                value = "********"
+            click.echo(f"{key}: {value}")
+        except KeyError:
+            raise click.ClickException(f"未知的配置项: {key}")
+        except Exception as e:
+            raise click.UsageError(f"获取配置失败: {str(e)}")
+    else:
+        click.echo("当前配置:")
+        for key in CONFIG_VALIDATORS.keys():
+            try:
+                value = (
+                    "********"
+                    if key == "dashboard.password"
+                    else _get_nested_item(config, key)
+                )
+                click.echo(f"  {key}: {value}")
+            except (KeyError, TypeError):
+                pass


Reviews:
user: login: sourcery-ai[bot]
body: 嘿 @Raven95676 - 我已经查看了你的更改 - 这里有一些反馈：

- 考虑使用比 MD5 更强的哈希算法来存储密码，例如 SHA-256 或 bcrypt。
- 修改 `set_config` 以确保新的、有效的键（来自 `CLI_CONFIGURABLE_KEYS`）可以添加到配置文件中，即使它们尚不存在。
- 初始配置文件以 'utf-8-sig' 编码写入，而后续保存使用 'utf-8'；考虑将所有配置文件写入标准化为一种编码。

<details>
<summary>这是我在审查期间查看的内容</summary>

- 🟡 **一般问题**：发现 1 个问题
- 🟢 **安全性**：一切看起来都不错
- 🟢 **测试**：一切看起来都不错
- 🟡 **复杂性**：发现 1 个问题
- 🟢 **文档**：一切看起来都不错
</details>

***

<details>
<summary>Sourcery 对开源是免费的 - 如果你喜欢我们的评论，请考虑分享它们 ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
帮助我更有用！请点击每个评论上的 👍 或 👎，我将使用反馈来改进你的评论。
</sub>

<details>
<summary>Original comment in English</summary>

Hey @Raven95676 - I've reviewed your changes - here's some feedback:

- Consider using a stronger hashing algorithm than MD5 for storing passwords, such as SHA-256 or bcrypt.
- Revise `set_config` to ensure new, valid keys (from `CLI_CONFIGURABLE_KEYS`) can be added to the configuration file even if they don't already exist.
- The initial configuration file is written with 'utf-8-sig' encoding, while subsequent saves use 'utf-8'; consider standardizing to one encoding for all configuration file writes.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

</details>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T07:48:38Z
user: login: sourcery-ai[bot]
body: 嘿 @Raven95676 - 我已经审查了你的更改，发现了一些需要解决的问题。

**阻塞问题**：
- 审查 MD5 在密码哈希中的使用。([link](https://github.com/AstrBotDevs/AstrBot/pull/1512/files#diff-486cf330d18d8d68d846469f1ef8991c0ca2cf687e4511d405c180eed0f709b3R37))

- 考虑让 `get_config`（没有键）列出 `CONFIG_VALIDATORS` 中的所有项目，指示未设置的值，而不是静默地省略它们。
- 确保应用程序从 `data/cmd_config.json` 读取设置，以便这些 CLI 驱动的配置更改生效。

<details>
<summary>以下是我在审查期间查看的内容</summary>

- 🟡 **一般问题**：发现 2 个问题
- 🔴 **安全**：1 个阻塞问题
- 🟢 **测试**：一切看起来都很好
- 🟢 **复杂性**：一切看起来都很好
- 🟢 **文档**：一切看起来都很好
</details>

***

<details>
<summary>Sourcery 对开源是免费的 - 如果你喜欢我们的评论，请考虑分享它们 ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
帮助我更有用！请点击每个评论上的 👍 或 👎，我将使用反馈来改进你的评论。
</sub>

<details>
<summary>Original comment in English</summary>

Hey @Raven95676 - I've reviewed your changes and found some issues that need to be addressed.

**Blocking issues**:
- Review the use of MD5 for password hashing. ([link](https://github.com/AstrBotDevs/AstrBot/pull/1512/files#diff-486cf330d18d8d68d846469f1ef8991c0ca2cf687e4511d405c180eed0f709b3R37))

- Consider having `get_config` (with no key) list all items from `CONFIG_VALIDATORS`, indicating unset values, rather than silently omitting them.
- Ensure the application reads settings from `data/cmd_config.json` for these CLI-driven configuration changes to be effective.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🔴 **Security**: 1 blocking issue
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

</details>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T08:09:28Z

Review comments:
diff_hunk: @@ -0,0 +1,180 @@
+import json
+import click
+import hashlib
+import zoneinfo
+from typing import Any
+from ..utils import get_astrbot_root, check_astrbot_root
+
+# 可通过CLI设置的配置项
+CLI_CONFIGURABLE_KEYS = [
+    "timezone",
+    "log_level",
+    "dashboard.port",
+    "dashboard.username",
+    "dashboard.password",
+    "callback_api_base",
+]
+
+
+def _load_config() -> dict[str, Any]:
+    """加载或初始化配置文件"""
+    root = get_astrbot_root()
+    if not check_astrbot_root(root):
+        raise click.ClickException(
+            f"{root}不是有效的 AstrBot 根目录，如需初始化请使用 astrbot init"
+        )
+
+    config_path = root / "data" / "cmd_config.json"
+    if not config_path.exists():
+        from astrbot.core.config.default import DEFAULT_CONFIG
+
+        config_path.parent.mkdir(parents=True, exist_ok=True)
+        config_path.write_text(
+            json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
+            encoding="utf-8-sig",
+        )
+
+    return json.loads(config_path.read_text(encoding="utf-8-sig"))
+
+
+def _save_config(config: dict[str, Any]) -> None:
+    """保存配置文件"""
+    config_path = get_astrbot_root() / "data" / "cmd_config.json"
+
+    config_path.write_text(
+        json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8"
+    )
path: astrbot/cli/commands/cmd_conf.py
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** 对齐保存和加载配置文件之间的编码。

使用相同的编码进行读取和写入（例如，都使用 'utf-8-sig' 或都使用 'utf-8'），以确保处理 BOM 的一致性。

```suggestion
def _save_config(config: dict[str, Any]) -> None:
    """保存配置文件"""
    config_path = get_astrbot_root() / "data" / "cmd_config.json"

    config_path.write_text(
        json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig"
    )
```

<details>
<summary>Original comment in English</summary>

**suggestion (bug_risk):** Align encoding between saving and loading configuration files.

Use the same encoding for reading and writing (e.g., both 'utf-8-sig' or both 'utf-8') to ensure consistent BOM handling.

```suggestion
def _save_config(config: dict[str, Any]) -> None:
    """保存配置文件"""
    config_path = get_astrbot_root() / "data" / "cmd_config.json"

    config_path.write_text(
        json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig"
    )
```

</details>
created_at: 2025-05-10T07:48:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,180 @@
+import json
+import click
+import hashlib
+import zoneinfo
+from typing import Any
+from ..utils import get_astrbot_root, check_astrbot_root
+
+# 可通过CLI设置的配置项
+CLI_CONFIGURABLE_KEYS = [
+    "timezone",
+    "log_level",
+    "dashboard.port",
+    "dashboard.username",
+    "dashboard.password",
+    "callback_api_base",
+]
+
+
+def _load_config() -> dict[str, Any]:
+    """加载或初始化配置文件"""
+    root = get_astrbot_root()
+    if not check_astrbot_root(root):
+        raise click.ClickException(
+            f"{root}不是有效的 AstrBot 根目录，如需初始化请使用 astrbot init"
+        )
+
+    config_path = root / "data" / "cmd_config.json"
+    if not config_path.exists():
+        from astrbot.core.config.default import DEFAULT_CONFIG
+
+        config_path.parent.mkdir(parents=True, exist_ok=True)
+        config_path.write_text(
+            json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
+            encoding="utf-8-sig",
+        )
+
+    return json.loads(config_path.read_text(encoding="utf-8-sig"))
+
+
+def _save_config(config: dict[str, Any]) -> None:
+    """保存配置文件"""
+    config_path = get_astrbot_root() / "data" / "cmd_config.json"
+
+    config_path.write_text(
+        json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8"
+    )
+
+
+def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
+    """设置嵌套字典中的值"""
+    parts = path.split(".")
+    for part in parts[:-1]:
+        if part not in obj:
+            obj[part] = {}
+        obj = obj[part]
+    obj[parts[-1]] = value
+
+
+def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
+    """获取嵌套字典中的值"""
+    parts = path.split(".")
+    for part in parts:
+        obj = obj[part]
+    return obj
+
+
+@click.group(name="conf")
+def conf():
+    """配置管理命令
+
+    支持的配置项:
+
+    - timezone: 时区设置 (例如: Asia/Shanghai)
+
+    - log_level: 日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL)
+
+    - dashboard.port: Dashboard 端口
+
+    - dashboard.username: Dashboard 用户名
+
+    - dashboard.password: Dashboard 密码
+
+    - callback_api_base: 回调接口基址
+    """
+    pass
+
+
+@conf.command(name="set")
+@click.argument("key")
+@click.argument("value")
+def set_config(key: str, value: str):
path: astrbot/cli/commands/cmd_conf.py
user: login: sourcery-ai[bot]
body: **issue (complexity):** 考虑将 if-elif 链重构为配置键到专用验证器函数的映射。

将 if-elif 链重构为配置键到专用验证器函数的映射。这通过隔离每个键的逻辑来降低认知负荷，并使代码更易于扩展。

例如，您可以将每个验证提取到辅助函数中：

```python
def validate_log_level(value: str) -> str:
    value = value.upper()
    if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
        raise click.ClickException("日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一")
    return value

def validate_dashboard_port(value: str) -> int:
    try:
        port = int(value)
    except ValueError:
        raise click.ClickException("端口必须是数字")
    if not (1 <= port <= 65535):
        raise click.ClickException("端口必须在 1-65535 范围内")
    return port

def validate_dashboard_username(value: str) -> str:
    if not value:
        raise click.ClickException("用户名不能为空")
    return value

def validate_dashboard_password(value: str) -> str:
    if not value:
        raise click.ClickException("密码不能为空")
    import hashlib
    return hashlib.md5(value.encode()).hexdigest()

def validate_timezone(value: str) -> str:
    if not value:
        raise click.ClickException("时区不能为空")
    try:
        import zoneinfo
        zoneinfo.ZoneInfo(value)
    except Exception:
        raise click.ClickException(f"无效的时区: {value}，请使用有效的IANA时区名称")
    return value
```

然后将键映射到它们的验证器：

```python
VALIDATORS = {
    "log_level": validate_log_level,
    "dashboard.port": validate_dashboard_port,
    "dashboard.username": validate_dashboard_username,
    "dashboard.password": validate_dashboard_password,
    "timezone": validate_timezone,
    # callback_api_base may not need validation
}
```

并相应地更新命令函数：

```python
@conf.command(name="set")
@click.argument("key")
@click.argument("value")
def set_config(key: str, value: str):
    if key not in CLI_CONFIGURABLE_KEYS:
        raise click.ClickException(f"不支持的配置项: {key}")

    config = _load_config()

    # Validate and convert the value
    if key in VALIDATORS:
        value = VALIDATORS[key](value)

    try:
        old_value = _get_nested_item(config, key)
        _set_nested_item(config, key, value)
        _save_config(config)
        click.echo(f"配置已更新: {key}")
        if key == "dashboard.password":
            click.echo("  原值: ********")
            click.echo("  新值: ********")
        else:
            click.echo(f"  原值: {old_value}")
            click.echo(f"  新值: {value}")
    except KeyError:
        raise click.ClickException(f"未知的配置项: {key}")
    except Exception as e:
        raise click.UsageError(f"设置配置失败: {str(e)}")
```

这种重构保持了所有功能，同时显著降低了验证逻辑的复杂性。

<details>
<summary>Original comment in English</summary>

**issue (complexity):** Consider refactoring the if-elif chain into a mapping of configuration keys to dedicated validator functions.

Refactor the if-elif chain into a mapping of configuration keys to dedicated validator functions. This reduces the cognitive load by isolating each key’s logic and makes the code easier to extend.

For example, you could extract each validation into a helper function:

```python
def validate_log_level(value: str) -> str:
    value = value.upper()
    if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
        raise click.ClickException("日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一")
    return value

def validate_dashboard_port(value: str) -> int:
    try:
        port = int(value)
    except ValueError:
        raise click.ClickException("端口必须是数字")
    if not (1 <= port <= 65535):
        raise click.ClickException("端口必须在 1-65535 范围内")
    return port

def validate_dashboard_username(value: str) -> str:
    if not value:
        raise click.ClickException("用户名不能为空")
    return value

def validate_dashboard_password(value: str) -> str:
    if not value:
        raise click.ClickException("密码不能为空")
    import hashlib
    return hashlib.md5(value.encode()).hexdigest()

def validate_timezone(value: str) -> str:
    if not value:
        raise click.ClickException("时区不能为空")
    try:
        import zoneinfo
        zoneinfo.ZoneInfo(value)
    except Exception:
        raise click.ClickException(f"无效的时区: {value}，请使用有效的IANA时区名称")
    return value
```

Then map keys to their validators:

```python
VALIDATORS = {
    "log_level": validate_log_level,
    "dashboard.port": validate_dashboard_port,
    "dashboard.username": validate_dashboard_username,
    "dashboard.password": validate_dashboard_password,
    "timezone": validate_timezone,
    # callback_api_base may not need validation
}
```

And update the command function accordingly:

```python
@conf.command(name="set")
@click.argument("key")
@click.argument("value")
def set_config(key: str, value: str):
    if key not in CLI_CONFIGURABLE_KEYS:
        raise click.ClickException(f"不支持的配置项: {key}")

    config = _load_config()

    # Validate and convert the value
    if key in VALIDATORS:
        value = VALIDATORS[key](value)

    try:
        old_value = _get_nested_item(config, key)
        _set_nested_item(config, key, value)
        _save_config(config)
        click.echo(f"配置已更新: {key}")
        if key == "dashboard.password":
            click.echo("  原值: ********")
            click.echo("  新值: ********")
        else:
            click.echo(f"  原值: {old_value}")
            click.echo(f"  新值: {value}")
    except KeyError:
        raise click.ClickException(f"未知的配置项: {key}")
    except Exception as e:
        raise click.UsageError(f"设置配置失败: {str(e)}")
```

This refactoring maintains all functionality while significantly reducing the complexity of your validation logic.

</details>
created_at: 2025-05-10T07:48:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,200 @@
+import json
+import click
+import hashlib
+import zoneinfo
+from typing import Any, Callable
+from ..utils import get_astrbot_root, check_astrbot_root
+
+
+def _validate_log_level(value: str) -> str:
+    """验证日志级别"""
+    value = value.upper()
+    if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
+        raise click.ClickException(
+            "日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一"
+        )
+    return value
+
+
+def _validate_dashboard_port(value: str) -> int:
+    """验证 Dashboard 端口"""
+    try:
+        port = int(value)
+        if port < 1 or port > 65535:
+            raise click.ClickException("端口必须在 1-65535 范围内")
+        return port
+    except ValueError:
+        raise click.ClickException("端口必须是数字")
+
+
+def _validate_dashboard_username(value: str) -> str:
+    """验证 Dashboard 用户名"""
+    if not value:
+        raise click.ClickException("用户名不能为空")
+    return value
+
+
+def _validate_dashboard_password(value: str) -> str:
+    """验证 Dashboard 密码"""
+    if not value:
+        raise click.ClickException("密码不能为空")
+    return hashlib.md5(value.encode()).hexdigest()
+
+
+def _validate_timezone(value: str) -> str:
+    """验证时区"""
+    try:
+        zoneinfo.ZoneInfo(value)
+    except Exception:
+        raise click.ClickException(f"无效的时区: {value}，请使用有效的IANA时区名称")
+    return value
+
+
+def _validate_callback_api_base(value: str) -> str:
+    """验证回调接口基址"""
+    if not value.startswith("http://") and not value.startswith("https://"):
+        raise click.ClickException("回调接口基址必须以 http:// 或 https:// 开头")
+    return value
+
+
+# 可通过CLI设置的配置项，配置键到验证器函数的映射
+CONFIG_VALIDATORS: dict[str, Callable[[str], Any]] = {
+    "timezone": _validate_timezone,
+    "log_level": _validate_log_level,
+    "dashboard.port": _validate_dashboard_port,
+    "dashboard.username": _validate_dashboard_username,
+    "dashboard.password": _validate_dashboard_password,
+    "callback_api_base": _validate_callback_api_base,
+}
+
+
+def _load_config() -> dict[str, Any]:
+    """加载或初始化配置文件"""
+    root = get_astrbot_root()
+    if not check_astrbot_root(root):
+        raise click.ClickException(
+            f"{root}不是有效的 AstrBot 根目录，如需初始化请使用 astrbot init"
+        )
+
+    config_path = root / "data" / "cmd_config.json"
+    if not config_path.exists():
+        from astrbot.core.config.default import DEFAULT_CONFIG
+
+        config_path.parent.mkdir(parents=True, exist_ok=True)
+        config_path.write_text(
+            json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
+            encoding="utf-8-sig",
+        )
+
+    return json.loads(config_path.read_text(encoding="utf-8-sig"))
path: astrbot/cli/commands/cmd_conf.py
user: login: sourcery-ai[bot]
body: **建议：** 考虑在加载配置时处理 JSON 解析错误。

捕获加载配置时的 json.JSONDecodeError，以便为用户提供清晰的错误消息。

```suggestion
    try:
        config_data = json.loads(config_path.read_text(encoding="utf-8-sig"))
    except json.JSONDecodeError as e:
        raise click.ClickException(f"Failed to parse configuration file '{config_path}': {e}") from e
    return config_data
```

<details>
<summary>Original comment in English</summary>

**suggestion:** Consider handling JSON parsing errors when loading the config.

Catch json.JSONDecodeError when loading the config to give users a clear error message.

```suggestion
    try:
        config_data = json.loads(config_path.read_text(encoding="utf-8-sig"))
    except json.JSONDecodeError as e:
        raise click.ClickException(f"Failed to parse configuration file '{config_path}': {e}") from e
    return config_data
```

</details>
created_at: 2025-05-10T08:09:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,200 @@
+import json
+import click
+import hashlib
+import zoneinfo
+from typing import Any, Callable
+from ..utils import get_astrbot_root, check_astrbot_root
+
+
+def _validate_log_level(value: str) -> str:
+    """验证日志级别"""
+    value = value.upper()
+    if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
+        raise click.ClickException(
+            "日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一"
+        )
+    return value
+
+
+def _validate_dashboard_port(value: str) -> int:
+    """验证 Dashboard 端口"""
+    try:
+        port = int(value)
+        if port < 1 or port > 65535:
+            raise click.ClickException("端口必须在 1-65535 范围内")
+        return port
+    except ValueError:
+        raise click.ClickException("端口必须是数字")
+
+
+def _validate_dashboard_username(value: str) -> str:
+    """验证 Dashboard 用户名"""
+    if not value:
+        raise click.ClickException("用户名不能为空")
+    return value
+
+
+def _validate_dashboard_password(value: str) -> str:
+    """验证 Dashboard 密码"""
+    if not value:
+        raise click.ClickException("密码不能为空")
+    return hashlib.md5(value.encode()).hexdigest()
+
+
+def _validate_timezone(value: str) -> str:
+    """验证时区"""
+    try:
+        zoneinfo.ZoneInfo(value)
+    except Exception:
+        raise click.ClickException(f"无效的时区: {value}，请使用有效的IANA时区名称")
+    return value
+
+
+def _validate_callback_api_base(value: str) -> str:
+    """验证回调接口基址"""
+    if not value.startswith("http://") and not value.startswith("https://"):
+        raise click.ClickException("回调接口基址必须以 http:// 或 https:// 开头")
+    return value
+
+
+# 可通过CLI设置的配置项，配置键到验证器函数的映射
+CONFIG_VALIDATORS: dict[str, Callable[[str], Any]] = {
+    "timezone": _validate_timezone,
+    "log_level": _validate_log_level,
+    "dashboard.port": _validate_dashboard_port,
+    "dashboard.username": _validate_dashboard_username,
+    "dashboard.password": _validate_dashboard_password,
+    "callback_api_base": _validate_callback_api_base,
+}
+
+
+def _load_config() -> dict[str, Any]:
+    """加载或初始化配置文件"""
+    root = get_astrbot_root()
+    if not check_astrbot_root(root):
+        raise click.ClickException(
+            f"{root}不是有效的 AstrBot 根目录，如需初始化请使用 astrbot init"
+        )
+
+    config_path = root / "data" / "cmd_config.json"
+    if not config_path.exists():
+        from astrbot.core.config.default import DEFAULT_CONFIG
+
+        config_path.parent.mkdir(parents=True, exist_ok=True)
+        config_path.write_text(
+            json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
+            encoding="utf-8-sig",
+        )
+
+    return json.loads(config_path.read_text(encoding="utf-8-sig"))
+
+
+def _save_config(config: dict[str, Any]) -> None:
+    """保存配置文件"""
+    config_path = get_astrbot_root() / "data" / "cmd_config.json"
+
+    config_path.write_text(
+        json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig"
+    )
+
+
+def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
+    """设置嵌套字典中的值"""
+    parts = path.split(".")
+    for part in parts[:-1]:
+        if part not in obj:
+            obj[part] = {}
+        obj = obj[part]
path: astrbot/cli/commands/cmd_conf.py
user: login: sourcery-ai[bot]
body: **建议（bug 风险）：** 验证 _set_nested_item 中的中间类型。

确保每个中间路径元素都是一个字典，然后再设置值，以防止配置结构更改时出现运行时错误。

```suggestion
def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
    """设置嵌套字典中的值"""
    parts = path.split(".")
    for part in parts[:-1]:
        if part in obj:
            if not isinstance(obj[part], dict):
                raise ValueError(f"Intermediate path element '{part}' is not a dict")
        else:
            obj[part] = {}
        obj = obj[part]
```

<details>
<summary>Original comment in English</summary>

**suggestion (bug_risk):** Validate intermediate types in _set_nested_item.

Ensure each intermediate path element is a dict before setting the value to prevent runtime errors if the configuration structure changes.

```suggestion
def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
    """设置嵌套字典中的值"""
    parts = path.split(".")
    for part in parts[:-1]:
        if part in obj:
            if not isinstance(obj[part], dict):
                raise ValueError(f"Intermediate path element '{part}' is not a dict")
        else:
            obj[part] = {}
        obj = obj[part]
```

</details>
created_at: 2025-05-10T08:09:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,200 @@
+import json
+import click
+import hashlib
+import zoneinfo
+from typing import Any, Callable
+from ..utils import get_astrbot_root, check_astrbot_root
+
+
+def _validate_log_level(value: str) -> str:
+    """验证日志级别"""
+    value = value.upper()
+    if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
+        raise click.ClickException(
+            "日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一"
+        )
+    return value
+
+
+def _validate_dashboard_port(value: str) -> int:
+    """验证 Dashboard 端口"""
+    try:
+        port = int(value)
+        if port < 1 or port > 65535:
+            raise click.ClickException("端口必须在 1-65535 范围内")
+        return port
+    except ValueError:
+        raise click.ClickException("端口必须是数字")
+
+
+def _validate_dashboard_username(value: str) -> str:
+    """验证 Dashboard 用户名"""
+    if not value:
+        raise click.ClickException("用户名不能为空")
+    return value
+
+
+def _validate_dashboard_password(value: str) -> str:
path: astrbot/cli/commands/cmd_conf.py
user: login: sourcery-ai[bot]
body: **🚨 建议（安全）：** 审查 MD5 在密码哈希中的使用。

MD5 不再安全用于密码哈希。请切换到更强的算法（例如，bcrypt、Argon2 或 PBKDF2）。

建议的实现：

```python
import hashlib
import bcrypt

```

```python
def _validate_dashboard_password(value: str) -> str:
    """验证 Dashboard 密码"""
    if not value:
        raise click.ClickException("密码不能为空")
    # 使用 bcrypt 进行密码哈希
    hashed = bcrypt.hashpw(value.encode("utf-8"), bcrypt.gensalt())
    return hashed.decode("utf-8")

```

确保 bcrypt 包在您的环境中可用（例如，添加到 requirements.txt 或 setup.py）。

<details>
<summary>Original comment in English</summary>

**🚨 suggestion (security):** Review the use of MD5 for password hashing.

MD5 is no longer secure for password hashing. Please switch to a stronger algorithm (e.g., bcrypt, Argon2, or PBKDF2).

Suggested implementation:

```python
import hashlib
import bcrypt

```

```python
def _validate_dashboard_password(value: str) -> str:
    """验证 Dashboard 密码"""
    if not value:
        raise click.ClickException("密码不能为空")
    # 使用 bcrypt 进行密码哈希
    hashed = bcrypt.hashpw(value.encode("utf-8"), bcrypt.gensalt())
    return hashed.decode("utf-8")

```

Ensure that the bcrypt package is available in your environment (e.g., added to requirements.txt or setup.py).

</details>
created_at: 2025-05-10T08:09:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-10T07:46:42Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

```
## 审查者指南

这个 pull request 引入了一个新的 CLI 命令 `conf`，用于管理应用程序配置。`conf` 命令在新的文件 `astrbot/cli/commands/cmd_conf.py` 中实现，允许用户设置和获取特定的配置值。配置存储在 `data/cmd_config.json` 中，并且新的功能包括对各种配置键的验证以及对敏感数据（如密码）的安全处理，密码在保存之前会被哈希，并在显示时被屏蔽。

### 文件级别变更

| 变更 | 详情 | 文件 |
| ------ | ------- | ----- |
| 添加了新的 CLI 命令 `conf` 用于配置管理。 | <ul><li>引入了一个新的名为 `conf` 的 `click` 命令组。</li><li>将 `conf` 命令添加到主 CLI 应用程序和命令导出。</li></ul> | `astrbot/cli/__main__.py`<br/>`astrbot/cli/commands/__init__.py`<br/>`astrbot/cli/commands/cmd_conf.py` |
| 实现了配置加载、保存、设置和获取逻辑。 | <ul><li>添加了从 `data/cmd_config.json` 加载配置的函数，如果文件不存在，则使用默认值初始化。</li><li>添加了将配置保存到 `data/cmd_config.json` 的函数。</li><li>实现了 `set` 子命令，用于使用新值更新指定的配置键，包括验证和显示旧/新值（屏蔽密码）。</li><li>实现了 `get` 子命令，用于检索和显示指定配置键的值或所有可配置的键值对（屏蔽密码）。</li><li>添加了用于嵌套字典项访问的辅助函数（`_set_nested_item`，`_get_nested_item`）。</li></ul> | `astrbot/cli/commands/cmd_conf.py` |
| 添加了可配置设置的验证和安全密码处理。 | <ul><li>创建了一个字典 `CONFIG_VALIDATORS`，将配置键映射到它们各自的验证函数。</li><li>实现了 `log_level`、`dashboard.port`、`dashboard.username`、`dashboard.password`（使用 MD5 哈希密码）、`timezone` 和 `callback_api_base` 的验证函数。</li><li>确保在获取或设置密码时不会直接显示密码。</li></ul> | `astrbot/cli/commands/cmd_conf.py` |

---

<details>
<summary>提示和命令</summary>

#### 与 Sourcery 互动

- **触发新的审查：** 在 pull request 上评论 `@sourcery-ai review`。
- **继续讨论：** 直接回复 Sourcery 的审查评论。
- **从审查评论生成 GitHub issue：** 通过回复审查评论，要求 Sourcery 从审查评论创建一个 issue。您也可以回复审查评论并使用 `@sourcery-ai issue` 从中创建一个 issue。
- **生成 pull request 标题：** 在 pull request 标题中的任何位置写入 `@sourcery-ai` 以随时生成标题。您也可以在 pull request 上评论 `@sourcery-ai title` 以随时（重新）生成标题。
- **生成 pull request 摘要：** 在 pull request 正文中的任何位置写入 `@sourcery-ai summary` 以随时在您想要的位置生成 PR 摘要。您也可以在 pull request 上评论 `@sourcery-ai summary` 以随时（重新）生成摘要。
- **生成审查者指南：** 在 pull request 上评论 `@sourcery-ai guide` 以随时（重新）生成审查者指南。
- **解决所有 Sourcery 评论：** 在 pull request 上评论 `@sourcery-ai resolve` 以解决所有 Sourcery 评论。如果您已经处理了所有评论并且不想再看到它们，这将非常有用。
- **驳回所有 Sourcery 审查：** 在 pull request 上评论 `@sourcery-ai dismiss` 以驳回所有现有的 Sourcery 审查。如果您想从新的审查开始，这将特别有用 - 不要忘记评论 `@sourcery-ai review` 以触发新的审查！

#### 自定义您的体验

访问您的 [dashboard](https://app.sourcery.ai) 以：
- 启用或禁用审查功能，例如 Sourcery 生成的 pull request 摘要、审查者指南等。
- 更改审查语言。
- 添加、删除或编辑自定义审查说明。
- 调整其他审查设置。

#### 获取帮助

- [联系我们的支持团队](mailto:support@sourcery.ai) 提出问题或反馈。
- 访问我们的 [documentation](https://docs.sourcery.ai) 获取详细指南和信息。
- 通过在 [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) 或 [GitHub](https://github.com/sourcery-ai) 上关注我们，与 Sourcery 团队保持联系。

</details>
```

<details>
<summary>Original review guide in English</summary>

## Reviewer's Guide

This pull request introduces a new CLI command `conf` for managing application configuration. The `conf` command, implemented in the new file `astrbot/cli/commands/cmd_conf.py`, allows users to set and get specific configuration values. Configuration is stored in `data/cmd_config.json`, and the new functionality includes validation for various configuration keys and secure handling for sensitive data like passwords, which are hashed before saving and masked when displayed.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Added new CLI command `conf` for configuration management. | <ul><li>Introduced a new `click` command group named `conf`.</li><li>Added `conf` command to the main CLI application and command exports.</li></ul> | `astrbot/cli/__main__.py`<br/>`astrbot/cli/commands/__init__.py`<br/>`astrbot/cli/commands/cmd_conf.py` |
| Implemented configuration loading, saving, setting, and getting logic. | <ul><li>Added functions to load configuration from `data/cmd_config.json`, initializing with defaults if the file doesn't exist.</li><li>Added a function to save configuration to `data/cmd_config.json`.</li><li>Implemented `set` subcommand to update a specified configuration key with a new value, including validation and displaying old/new values (masking passwords).</li><li>Implemented `get` subcommand to retrieve and display the value of a specified configuration key or all configurable key-value pairs (masking passwords).</li><li>Added helper functions for nested dictionary item access (`_set_nested_item`, `_get_nested_item`).</li></ul> | `astrbot/cli/commands/cmd_conf.py` |
| Added validation for configurable settings and secure password handling. | <ul><li>Created a dictionary `CONFIG_VALIDATORS` mapping configuration keys to their respective validation functions.</li><li>Implemented validation functions for `log_level`, `dashboard.port`, `dashboard.username`, `dashboard.password` (hashes the password using MD5), `timezone`, and `callback_api_base`.</li><li>Ensured passwords are not displayed directly when getting or setting them.</li></ul> | `astrbot/cli/commands/cmd_conf.py` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: Raven95676
created_at: 2025-05-10T08:05:38Z
body: @sourcery-ai dismiss
author_association: MEMBER
user: login: Raven95676
created_at: 2025-05-10T08:06:11Z
body: @sourcery-ai review
author_association: MEMBER