import os import uuid from astrbot.core import AstrBotConfig, logger from astrbot.core.utils.shared_preferences import SharedPreferences from astrbot.core.config.astrbot_config import ASTRBOT_CONFIG_PATH from astrbot.core.config.default import DEFAULT_CONFIG from astrbot.core.platform.message_session import MessageSession from astrbot.core.utils.astrbot_path import get_astrbot_config_path from typing import TypeVar, TypedDict _VT = TypeVar("_VT") class ConfInfo(TypedDict): """Configuration information for a specific session or platform.""" id: str # UUID of the configuration or "default" umop: list[str] # Unified Message Origin Pattern name: str path: str # File name to the configuration file DEFAULT_CONFIG_CONF_INFO = ConfInfo( id="default", umop=["::"], name="default", path=ASTRBOT_CONFIG_PATH, ) class AstrBotConfigManager: """A class to manage the system configuration of AstrBot, aka ACM""" def __init__(self, default_config: AstrBotConfig, sp: SharedPreferences): self.sp = sp self.confs: dict[str, AstrBotConfig] = {} """uuid / "default" -> AstrBotConfig""" self.confs["default"] = default_config self._load_all_configs() def _load_all_configs(self): """Load all configurations from the shared preferences.""" abconf_data = self.sp.get( "abconf_mapping", {}, scope="global", scope_id="global" ) for uuid_, meta in abconf_data.items(): filename = meta["path"] conf_path = os.path.join(get_astrbot_config_path(), filename) if os.path.exists(conf_path): conf = AstrBotConfig(config_path=conf_path) self.confs[uuid_] = conf else: logger.warning( f"Config file {conf_path} for UUID {uuid_} does not exist, skipping." ) continue def _is_umo_match(self, p1: str, p2: str) -> bool: """判断 p2 umo 是否逻辑包含于 p1 umo""" p1_ls = p1.split(":") p2_ls = p2.split(":") if len(p1_ls) != 3 or len(p2_ls) != 3: return False # 非法格式 return all(p == "" or p == "*" or p == t for p, t in zip(p1_ls, p2_ls)) def _load_conf_mapping(self, umo: str | MessageSession) -> ConfInfo: """获取指定 umo 的配置文件 uuid, 如果不存在则返回默认配置(返回 "default") Returns: ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型 """ # uuid -> { "umop": list, "path": str, "name": str } abconf_data = self.sp.get( "abconf_mapping", {}, scope="global", scope_id="global" ) if isinstance(umo, MessageSession): umo = str(umo) else: try: umo = str(MessageSession.from_str(umo)) # validate except Exception: return DEFAULT_CONFIG_CONF_INFO for uuid_, meta in abconf_data.items(): for pattern in meta["umop"]: if self._is_umo_match(pattern, umo): return ConfInfo(**meta, id=uuid_) return DEFAULT_CONFIG_CONF_INFO def _save_conf_mapping( self, abconf_path: str, abconf_id: str, umo_parts: list[str] | list[MessageSession], abconf_name: str | None = None, ) -> None: """保存配置文件的映射关系""" for part in umo_parts: if isinstance(part, MessageSession): part = str(part) elif not isinstance(part, str): raise ValueError( "umo_parts must be a list of strings or MessageSession instances" ) abconf_data = self.sp.get( "abconf_mapping", {}, scope="global", scope_id="global" ) random_word = abconf_name or uuid.uuid4().hex[:8] abconf_data[abconf_id] = { "umop": umo_parts, "path": abconf_path, "name": random_word, } self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig: """获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。""" if not umo: return self.confs["default"] if isinstance(umo, MessageSession): umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}" uuid_ = self._load_conf_mapping(umo)["id"] conf = self.confs.get(uuid_) if not conf: conf = self.confs["default"] # default MUST exists return conf @property def default_conf(self) -> AstrBotConfig: """获取默认配置文件""" return self.confs["default"] def get_conf_info(self, umo: str | MessageSession) -> ConfInfo: """获取指定 umo 的配置文件元数据""" if isinstance(umo, MessageSession): umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}" return self._load_conf_mapping(umo) def get_conf_list(self) -> list[ConfInfo]: """获取所有配置文件的元数据列表""" conf_list = [] conf_list.append(DEFAULT_CONFIG_CONF_INFO) abconf_mapping = self.sp.get( "abconf_mapping", {}, scope="global", scope_id="global" ) for uuid_, meta in abconf_mapping.items(): conf_list.append(ConfInfo(**meta, id=uuid_)) return conf_list def create_conf( self, umo_parts: list[str] | list[MessageSession], config: dict = DEFAULT_CONFIG, name: str | None = None, ) -> str: """ umo 由三个部分组成 [platform_id]:[message_type]:[session_id]。 umo_parts 可以是 "::" (代表所有), 可以是 "[platform_id]::" (代表指定平台下的所有类型消息和会话)。 """ conf_uuid = str(uuid.uuid4()) conf_file_name = f"abconf_{conf_uuid}.json" conf_path = os.path.join(get_astrbot_config_path(), conf_file_name) conf = AstrBotConfig(config_path=conf_path, default_config=config) conf.save_config() self._save_conf_mapping(conf_file_name, conf_uuid, umo_parts, abconf_name=name) self.confs[conf_uuid] = conf return conf_uuid def delete_conf(self, conf_id: str) -> bool: """删除指定配置文件 Args: conf_id: 配置文件的 UUID Returns: bool: 删除是否成功 Raises: ValueError: 如果试图删除默认配置文件 """ if conf_id == "default": raise ValueError("不能删除默认配置文件") # 从映射中移除 abconf_data = self.sp.get( "abconf_mapping", {}, scope="global", scope_id="global" ) if conf_id not in abconf_data: logger.warning(f"配置文件 {conf_id} 不存在于映射中") return False # 获取配置文件路径 conf_path = os.path.join( get_astrbot_config_path(), abconf_data[conf_id]["path"] ) # 删除配置文件 try: if os.path.exists(conf_path): os.remove(conf_path) logger.info(f"已删除配置文件: {conf_path}") except Exception as e: logger.error(f"删除配置文件 {conf_path} 失败: {e}") return False # 从内存中移除 if conf_id in self.confs: del self.confs[conf_id] # 从映射中移除 del abconf_data[conf_id] self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") logger.info(f"成功删除配置文件 {conf_id}") return True def update_conf_info( self, conf_id: str, name: str | None = None, umo_parts: list[str] | None = None ) -> bool: """更新配置文件信息 Args: conf_id: 配置文件的 UUID name: 新的配置文件名称 (可选) umo_parts: 新的 UMO 部分列表 (可选) Returns: bool: 更新是否成功 """ if conf_id == "default": raise ValueError("不能更新默认配置文件的信息") abconf_data = self.sp.get( "abconf_mapping", {}, scope="global", scope_id="global" ) if conf_id not in abconf_data: logger.warning(f"配置文件 {conf_id} 不存在于映射中") return False # 更新名称 if name is not None: abconf_data[conf_id]["name"] = name # 更新 UMO 部分 if umo_parts is not None: # 验证 UMO 部分格式 for part in umo_parts: if isinstance(part, MessageSession): part = str(part) elif not isinstance(part, str): raise ValueError( "umo_parts must be a list of strings or MessageSession instances" ) abconf_data[conf_id]["umop"] = umo_parts # 保存更新 self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") logger.info(f"成功更新配置文件 {conf_id} 的信息") return True def g( self, umo: str | None = None, key: str | None = None, default: _VT = None ) -> _VT: """获取配置项。umo 为 None 时使用默认配置""" if umo is None: return self.confs["default"].get(key, default) conf = self.get_conf(umo) return conf.get(key, default)