From 2b7559b8cc969f3f7bbfe756768f1487f241e44e Mon Sep 17 00:00:00 2001 From: Ronifue Date: Tue, 2 Dec 2025 16:00:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BF=9D=E5=AD=98?= =?UTF-8?q?toml=E6=97=B6=E5=81=B6=E5=8F=91=E7=9A=84=E7=A9=BA=E8=A1=8C?= =?UTF-8?q?=E7=B4=AF=E8=AE=A1bug=E5=92=8C=E6=B3=A8=E9=87=8A=E4=B8=A2?= =?UTF-8?q?=E5=A4=B1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/toml_utils.py | 66 +++++++++++++++++++++++++++++++++++--- src/webui/config_routes.py | 44 +++---------------------- 2 files changed, 67 insertions(+), 43 deletions(-) diff --git a/src/common/toml_utils.py b/src/common/toml_utils.py index 0c34f6ab..0a88c458 100644 --- a/src/common/toml_utils.py +++ b/src/common/toml_utils.py @@ -4,6 +4,7 @@ TOML 工具函数 提供 TOML 文件的格式化保存功能,确保数组等元素以美观的多行格式输出。 """ +import re from typing import Any import tomlkit from tomlkit.items import AoT, Table, Array @@ -54,14 +55,71 @@ def _format_toml_value(obj: Any, threshold: int, depth: int = 0) -> Any: return obj -def save_toml_with_format(data: Any, file_path: str, multiline_threshold: int = 1) -> None: - """格式化 TOML 数据并保存到文件""" +def _update_toml_doc(target: Any, source: Any) -> None: + """ + 递归合并字典,将 source 的值更新到 target 中,保留 target 的注释和格式。 + - 已存在的键:更新值(递归处理嵌套字典) + - 新增的键:添加到 target + - 跳过 version 字段 + """ + if isinstance(source, list) or not isinstance(source, dict) or not isinstance(target, dict): + return + + for key, value in source.items(): + if key == "version": + continue + if key in target: + # 已存在的键:递归更新或直接赋值 + target_value = target[key] + if isinstance(value, dict) and isinstance(target_value, dict): + _update_toml_doc(target_value, value) + else: + try: + target[key] = tomlkit.item(value) + except (TypeError, ValueError): + target[key] = value + else: + # 新增的键:添加到 target + try: + target[key] = tomlkit.item(value) + except (TypeError, ValueError): + target[key] = value + + +def save_toml_with_format( + data: Any, file_path: str, multiline_threshold: int = 1, preserve_comments: bool = True +) -> None: + """ + 格式化 TOML 数据并保存到文件。 + + Args: + data: 要保存的数据(dict 或 tomlkit 文档) + file_path: 保存路径 + multiline_threshold: 数组多行格式化阈值,-1 表示不格式化 + preserve_comments: 是否保留原文件的注释和格式(默认 True) + 若为 True 且文件已存在且 data 不是 tomlkit 文档,会先读取原文件,再将 data 合并进去 + """ + import os + from tomlkit import TOMLDocument + + # 如果需要保留注释、文件存在、且 data 不是已有的 tomlkit 文档,先读取原文件再合并 + if preserve_comments and os.path.exists(file_path) and not isinstance(data, TOMLDocument): + with open(file_path, "r", encoding="utf-8") as f: + doc = tomlkit.load(f) + _update_toml_doc(doc, data) + data = doc + formatted = _format_toml_value(data, multiline_threshold) if multiline_threshold >= 0 else data + output = tomlkit.dumps(formatted) + # 规范化:将 3+ 连续空行压缩为 1 个空行,防止空行累积 + output = re.sub(r'\n{3,}', '\n\n', output) with open(file_path, "w", encoding="utf-8") as f: - tomlkit.dump(formatted, f) + f.write(output) def format_toml_string(data: Any, multiline_threshold: int = 1) -> str: """格式化 TOML 数据并返回字符串""" formatted = _format_toml_value(data, multiline_threshold) if multiline_threshold >= 0 else data - return tomlkit.dumps(formatted) \ No newline at end of file + output = tomlkit.dumps(formatted) + # 规范化:将 3+ 连续空行压缩为 1 个空行,防止空行累积 + return re.sub(r'\n{3,}', '\n\n', output) \ No newline at end of file diff --git a/src/webui/config_routes.py b/src/webui/config_routes.py index 5e54c8f7..63803d5e 100644 --- a/src/webui/config_routes.py +++ b/src/webui/config_routes.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, HTTPException, Body from typing import Any, Annotated from src.common.logger import get_logger -from src.common.toml_utils import save_toml_with_format +from src.common.toml_utils import save_toml_with_format, _update_toml_doc from src.config.config import Config, APIAdapterConfig, CONFIG_DIR, PROJECT_ROOT from src.config.official_configs import ( BotConfig, @@ -51,40 +51,6 @@ PathBody = Annotated[dict[str, str], Body()] router = APIRouter(prefix="/config", tags=["config"]) -# ===== 辅助函数 ===== - - -def _update_dict_preserve_comments(target: Any, source: Any) -> None: - """ - 递归合并字典,保留 target 中的注释和格式 - 将 source 的值更新到 target 中(仅更新已存在的键) - - Args: - target: 目标字典(tomlkit 对象,包含注释) - source: 源字典(普通 dict 或 list) - """ - # 如果 source 是列表,直接替换(数组表没有注释保留的意义) - if isinstance(source, list): - return # 调用者需要直接赋值 - - # 如果都是字典,递归合并 - if isinstance(source, dict) and isinstance(target, dict): - for key, value in source.items(): - if key == "version": - continue # 跳过版本号 - if key in target: - target_value = target[key] - # 递归处理嵌套字典 - if isinstance(value, dict) and isinstance(target_value, dict): - _update_dict_preserve_comments(target_value, value) - else: - # 使用 tomlkit.item 保持类型 - try: - target[key] = tomlkit.item(value) - except (TypeError, ValueError): - target[key] = value - - # ===== 架构获取接口 ===== @@ -238,7 +204,7 @@ async def update_bot_config(config_data: ConfigBody): except Exception as e: raise HTTPException(status_code=400, detail=f"配置数据验证失败: {str(e)}") from e - # 保存配置文件(格式化数组为多行) + # 保存配置文件(自动保留注释和格式) config_path = os.path.join(CONFIG_DIR, "bot_config.toml") save_toml_with_format(config_data, config_path) @@ -261,7 +227,7 @@ async def update_model_config(config_data: ConfigBody): except Exception as e: raise HTTPException(status_code=400, detail=f"配置数据验证失败: {str(e)}") from e - # 保存配置文件(格式化数组为多行) + # 保存配置文件(自动保留注释和格式) config_path = os.path.join(CONFIG_DIR, "model_config.toml") save_toml_with_format(config_data, config_path) @@ -300,7 +266,7 @@ async def update_bot_config_section(section_name: str, section_data: SectionBody config_data[section_name] = section_data elif isinstance(section_data, dict) and isinstance(config_data[section_name], dict): # 字典递归合并 - _update_dict_preserve_comments(config_data[section_name], section_data) + _update_toml_doc(config_data[section_name], section_data) else: # 其他类型直接替换 config_data[section_name] = section_data @@ -398,7 +364,7 @@ async def update_model_config_section(section_name: str, section_data: SectionBo config_data[section_name] = section_data elif isinstance(section_data, dict) and isinstance(config_data[section_name], dict): # 字典递归合并 - _update_dict_preserve_comments(config_data[section_name], section_data) + _update_toml_doc(config_data[section_name], section_data) else: # 其他类型直接替换 config_data[section_name] = section_data From dd7303091924c72668de02e0112ec4fc88b5dcc9 Mon Sep 17 00:00:00 2001 From: Ronifue Date: Tue, 2 Dec 2025 16:20:44 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E7=AE=80=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/webui/plugin_routes.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/webui/plugin_routes.py b/src/webui/plugin_routes.py index 1f2d85da..9eedc9d7 100644 --- a/src/webui/plugin_routes.py +++ b/src/webui/plugin_routes.py @@ -1420,18 +1420,8 @@ async def update_plugin_config( shutil.copy(config_path, backup_path) logger.info(f"已备份配置文件: {backup_path}") - # 写入新配置(使用 tomlkit 保留注释) - import tomlkit - - # 先读取原配置以保留注释和格式 - existing_doc = tomlkit.document() - if config_path.exists(): - with open(config_path, "r", encoding="utf-8") as f: - existing_doc = tomlkit.load(f) - # 更新值 - for key, value in request.config.items(): - existing_doc[key] = value - save_toml_with_format(existing_doc, str(config_path)) + # 写入新配置(自动保留注释和格式) + save_toml_with_format(request.config, str(config_path)) logger.info(f"已更新插件配置: {plugin_id}")