zhenxun_bot/zhenxun/services/group_settings_service.py
Rumio e5b2a872d3
Some checks failed
检查bot是否运行正常 / bot check (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Sequential Lint and Type Check / ruff-call (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Force Sync to Aliyun / sync (push) Has been cancelled
Update Version / update-version (push) Has been cancelled
Sequential Lint and Type Check / pyright-call (push) Has been cancelled
feat(group-settings): 实现群插件配置管理系统 (#2072)
*  feat(group-settings): 实现群插件配置管理系统

- 引入 GroupSettingsService 服务,提供统一的群插件配置管理接口
- 新增 GroupPluginSetting 模型,用于持久化存储插件在不同群组的配置
- 插件扩展数据 PluginExtraData 增加 group_config_model 字段,用于注册分群配置模型
- 新增 GetGroupConfig 依赖注入,允许插件轻松获取和解析当前群组的配置

【核心服务 GroupSettingsService】
- 支持按群组、插件名和键设置、获取和删除配置项
- 实现配置聚合缓存机制,提升配置读取效率,减少数据库查询
- 支持配置继承与覆盖逻辑(群配置覆盖全局默认值)
- 提供批量设置功能 set_bulk,方便为多个群组同时更新配置

【管理与缓存】
- 新增超级用户命令 pconf (plugin_config_manager),用于命令行管理插件的分群和全局配置
- 新增 CacheType.GROUP_PLUGIN_SETTINGS 缓存类型并注册
- 增加 Pydantic model_construct 兼容函数

* 🐛 fix(codeql): 移除对 JavaScript 和 TypeScript 的分析支持

---------

Co-authored-by: webjoin111 <455457521@qq.com>
2025-12-01 14:52:36 +08:00

224 lines
8.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Any, TypeVar, overload
from pydantic import BaseModel, ValidationError
import ujson as json
from zhenxun.configs.config import Config
from zhenxun.models.group_plugin_setting import GroupPluginSetting
from zhenxun.services.cache import Cache
from zhenxun.services.data_access import DataAccess
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import model_dump, model_validate, parse_as
T = TypeVar("T", bound=BaseModel)
class GroupSettingsService:
"""
一个用于管理插件分群配置的服务。
集成了聚合缓存、批量操作和版本迁移功能。
"""
def __init__(self):
self.dao = DataAccess(GroupPluginSetting)
self._cache = Cache[dict]("group_plugin_settings")
async def set(
self, group_id: str, plugin_name: str, settings_model: BaseModel
) -> None:
"""
为一个插件在指定群组中设置完整的配置模型。
参数:
group_id: 目标群组ID。
plugin_name: 插件的模块名。
settings_model: 包含完整配置的Pydantic模型实例。
"""
settings_dict = model_dump(settings_model)
json_value = json.dumps(settings_dict, ensure_ascii=False)
await self.dao.update_or_create(
defaults={"settings": json_value}, # type: ignore
group_id=group_id,
plugin_name=plugin_name,
)
await self.dao.clear_cache(group_id=group_id, plugin_name=plugin_name)
async def set_key_value(
self, group_id: str, plugin_name: str, key: str, value: Any
) -> None:
"""为一个插件在指定群组中设置单个配置项的值。"""
setting_entry, _ = await GroupPluginSetting.get_or_create(
defaults={"settings": {}},
group_id=group_id,
plugin_name=plugin_name,
)
if not isinstance(setting_entry.settings, dict):
setting_entry.settings = {}
setting_entry.settings[key] = value
await setting_entry.save(update_fields=["settings"])
await self.dao.clear_cache(group_id=group_id, plugin_name=plugin_name)
async def reset_key(self, group_id: str, plugin_name: str, key: str) -> bool:
"""重置单个配置项"""
setting = await self.dao.get_or_none(group_id=group_id, plugin_name=plugin_name)
if setting and isinstance(setting.settings, dict) and key in setting.settings:
del setting.settings[key]
if not setting.settings:
await setting.delete()
else:
await setting.save(update_fields=["settings"])
await self.dao.clear_cache(group_id=group_id, plugin_name=plugin_name)
return True
return False
async def get(
self, group_id: str, plugin_name: str, key: str, default: Any = None
) -> Any:
"""
获取一个分群配置项的值,如果群组未单独设置,则回退到全局默认值。
参数:
group_id: 目标群组ID。
plugin_name: 插件的模块名。
key: 配置项的键。
default: 如果找不到配置项,返回的默认值。
返回:
配置项的值。
"""
full_settings = await self.get_all_for_plugin(group_id, plugin_name)
return full_settings.get(key, default)
async def reset_all_for_plugin(self, group_id: str, plugin_name: str) -> bool:
"""
重置一个插件在指定群组的配置,使其回退到全局默认值。
这通过删除数据库中的对应记录来实现。
参数:
group_id: 目标群组ID。
plugin_name: 插件的模块名。
返回:
bool: 如果成功删除了一个条目,则返回 True否则返回 False。
"""
deleted_count = await self.dao.delete(
group_id=group_id, plugin_name=plugin_name
)
if deleted_count > 0:
await self.dao.clear_cache(group_id=group_id, plugin_name=plugin_name)
logger.debug(f"已重置插件 '{plugin_name}' 在群组 '{group_id}' 的配置。")
return True
return False
@overload
async def get_all_for_plugin(
self, group_id: str, plugin_name: str, *, parse_model: type[T]
) -> T: ...
@overload
async def get_all_for_plugin(
self, group_id: str, plugin_name: str, *, parse_model: None = None
) -> dict[str, Any]: ...
async def get_all_for_plugin(
self, group_id: str, plugin_name: str, *, parse_model: type[T] | None = None
) -> T | dict[str, Any]:
"""
获取一个插件在指定群组中的完整配置,应用了“继承与覆盖”逻辑。
它首先获取全局默认配置,然后用数据库中存储的群组特定配置覆盖它。
参数:
group_id: 目标群组ID。
plugin_name: 插件的模块名。
parse_model: (可选) Pydantic模型用于解析和验证配置。
"""
cache_key = f"{group_id}:{plugin_name}"
cached_settings = await self._cache.get(cache_key)
if cached_settings is not None:
logger.debug(f"缓存命中: {cache_key}")
if parse_model:
try:
return parse_as(parse_model, cached_settings)
except (ValidationError, TypeError) as e:
logger.warning(
f"缓存数据 '{cache_key}' 与模型 '{parse_model.__name__}' "
f"不匹配: {e}。将从数据库重新加载。"
)
else:
return cached_settings
logger.debug(f"缓存未命中: {cache_key},从数据库加载。")
global_config_group = Config.get(plugin_name)
final_settings_dict = {
key: global_config_group.get(key, build_model=False)
for key in global_config_group.configs.keys()
}
group_setting_entry = await self.dao.get_or_none(
group_id=group_id, plugin_name=plugin_name
)
if group_setting_entry:
try:
group_specific_settings = group_setting_entry.settings
if isinstance(group_specific_settings, dict):
final_settings_dict.update(group_specific_settings)
else:
logger.warning(
f"群组 {group_id} 插件 '{plugin_name}' 的配置格式不正确"
f"(不是字典),已忽略。"
)
except Exception as e:
logger.warning(
f"加载群组 {group_id} 插件 '{plugin_name}' 的特定配置时出错: {e}"
)
await self._cache.set(cache_key, final_settings_dict)
if parse_model:
try:
return parse_as(parse_model, final_settings_dict)
except (ValidationError, TypeError) as e:
logger.warning(
f"插件 '{plugin_name}' 的配置无法解析为 '{parse_model.__name__}'"
f"值: {final_settings_dict}, 错误: {e}。将返回一个默认模型实例。"
)
return parse_as(parse_model, {})
return final_settings_dict
async def set_bulk(
self, group_ids: list[str], plugin_name: str, key: str, value: Any
) -> tuple[int, int]:
"""
为多个群组批量设置同一个配置项。
参数:
group_ids: 目标群组ID列表。
plugin_name: 插件模块名。
key: 配置项的键。
value: 要设置的值。
返回:
一个元组 (updated_count, created_count)。
"""
if not group_ids:
return 0, 0
for group_id in group_ids:
current_settings = await self.get_all_for_plugin(group_id, plugin_name)
current_settings[key] = value
await self.set(
group_id, plugin_name, model_validate(BaseModel, current_settings)
)
return len(group_ids), 0
group_settings_service = GroupSettingsService()