feat(system-settings): add unified tools for querying and updating system settings

This commit is contained in:
jxxghp
2026-05-12 13:55:52 +08:00
parent ac090af606
commit ea88f272a6
10 changed files with 1191 additions and 1 deletions

View File

@@ -0,0 +1,331 @@
"""系统设置工具共用的键解析与分组元数据。"""
from dataclasses import dataclass
from typing import Optional
from app.core.config import Settings
from app.schemas.types import SystemConfigKey
@dataclass(frozen=True)
class SettingSpec:
"""描述一个可被 Agent 读写的系统设置项。"""
key: str
source: str
group: str
label: str
SYSTEMCONFIG_SETTING_METADATA = {
SystemConfigKey.Downloaders.value: {
"group": "downloaders",
"label": "下载器配置",
},
SystemConfigKey.MediaServers.value: {
"group": "media_servers",
"label": "媒体服务器配置",
},
SystemConfigKey.Notifications.value: {
"group": "notifications",
"label": "消息通知配置",
},
SystemConfigKey.NotificationSwitchs.value: {
"group": "notification_switches",
"label": "通知场景开关",
},
SystemConfigKey.Directories.value: {
"group": "directories",
"label": "目录配置",
},
SystemConfigKey.Storages.value: {
"group": "storages",
"label": "存储配置",
},
SystemConfigKey.IndexerSites.value: {
"group": "search_sites",
"label": "搜索站点范围",
},
SystemConfigKey.RssSites.value: {
"group": "subscribe_sites",
"label": "订阅站点范围",
},
SystemConfigKey.UserSiteAuthParams.value: {
"group": "site_auth",
"label": "站点认证参数",
},
SystemConfigKey.AIAgentConfig.value: {
"group": "ai_agent",
"label": "AI 智能体配置",
},
SystemConfigKey.CustomIdentifiers.value: {
"group": "custom_identifiers",
"label": "自定义识别词",
},
SystemConfigKey.CustomReleaseGroups.value: {
"group": "customization",
"label": "自定义制作组/字幕组",
},
SystemConfigKey.Customization.value: {
"group": "customization",
"label": "自定义占位符",
},
SystemConfigKey.TransferExcludeWords.value: {
"group": "transfer",
"label": "整理屏蔽词",
},
SystemConfigKey.TorrentsPriority.value: {
"group": "filter_rules",
"label": "种子优先级规则",
},
SystemConfigKey.CustomFilterRules.value: {
"group": "filter_rules",
"label": "用户自定义规则",
},
SystemConfigKey.UserFilterRuleGroups.value: {
"group": "filter_rules",
"label": "用户规则组",
},
SystemConfigKey.SearchFilterRuleGroups.value: {
"group": "filter_rules",
"label": "搜索默认过滤规则组",
},
SystemConfigKey.SubscribeFilterRuleGroups.value: {
"group": "filter_rules",
"label": "订阅默认过滤规则组",
},
SystemConfigKey.BestVersionFilterRuleGroups.value: {
"group": "filter_rules",
"label": "洗版默认过滤规则组",
},
SystemConfigKey.SubscribeDefaultParams.value: {
"group": "subscribe_defaults",
"label": "订阅默认参数",
},
SystemConfigKey.DefaultMovieSubscribeConfig.value: {
"group": "subscribe_defaults",
"label": "默认电影订阅规则",
},
SystemConfigKey.DefaultTvSubscribeConfig.value: {
"group": "subscribe_defaults",
"label": "默认电视剧订阅规则",
},
SystemConfigKey.UserInstalledPlugins.value: {
"group": "plugins",
"label": "已安装插件列表",
},
SystemConfigKey.PluginFolders.value: {
"group": "plugins",
"label": "插件文件夹分组配置",
},
SystemConfigKey.PluginInstallReport.value: {
"group": "plugins",
"label": "插件安装统计",
},
SystemConfigKey.NotificationSendTime.value: {
"group": "notifications",
"label": "通知发送时间",
},
SystemConfigKey.NotificationTemplates.value: {
"group": "notifications",
"label": "通知模板",
},
SystemConfigKey.ScrapingSwitchs.value: {
"group": "scraping",
"label": "刮削开关设置",
},
SystemConfigKey.FollowSubscribers.value: {
"group": "subscribe_sites",
"label": "Follow 订阅分享者",
},
}
LIST_ITEM_MATCH_FIELD_DEFAULTS = {
SystemConfigKey.Downloaders.value: "name",
SystemConfigKey.MediaServers.value: "name",
SystemConfigKey.Notifications.value: "name",
SystemConfigKey.NotificationSwitchs.value: "type",
SystemConfigKey.Directories.value: "name",
SystemConfigKey.Storages.value: "name",
}
GROUP_ALIASES = {
"all": "all",
"全部": "all",
"settings": "settings",
"basic": "settings",
"基础设置": "settings",
"基础配置": "settings",
"systemconfig": "systemconfig",
"system_config": "systemconfig",
"系统设置": "systemconfig",
"系统配置": "systemconfig",
"downloaders": "downloaders",
"downloader": "downloaders",
"下载器": "downloaders",
"media_servers": "media_servers",
"mediaservers": "media_servers",
"media-servers": "media_servers",
"媒体服务器": "media_servers",
"notifications": "notifications",
"notification": "notifications",
"消息通知": "notifications",
"通知": "notifications",
"notification_switches": "notification_switches",
"notification_switchs": "notification_switches",
"通知开关": "notification_switches",
"storages": "storages",
"storage": "storages",
"存储": "storages",
"directories": "directories",
"directory": "directories",
"目录": "directories",
"search_sites": "search_sites",
"indexer_sites": "search_sites",
"搜索站点": "search_sites",
"subscribe_sites": "subscribe_sites",
"rss_sites": "subscribe_sites",
"订阅站点": "subscribe_sites",
"site_auth": "site_auth",
"site_auth_params": "site_auth",
"站点认证": "site_auth",
"ai_agent": "ai_agent",
"agent": "ai_agent",
"智能体": "ai_agent",
"custom_identifiers": "custom_identifiers",
"自定义识别词": "custom_identifiers",
"filter_rules": "filter_rules",
"过滤规则": "filter_rules",
"subscribe_defaults": "subscribe_defaults",
"订阅默认": "subscribe_defaults",
"plugins": "plugins",
"插件": "plugins",
"customization": "customization",
"自定义": "customization",
"transfer": "transfer",
"整理": "transfer",
"scraping": "scraping",
"刮削": "scraping",
"misc": "misc",
"其他": "misc",
}
def _normalize_token(value: str) -> str:
return str(value).strip().lower().replace("-", "_")
def _build_specs() -> tuple[dict[str, SettingSpec], dict[str, SettingSpec]]:
core_specs = {
key: SettingSpec(key=key, source="settings", group="settings", label=key)
for key in Settings.model_fields.keys()
}
system_specs = {}
for item in SystemConfigKey:
metadata = SYSTEMCONFIG_SETTING_METADATA.get(item.value, {})
system_specs[item.value] = SettingSpec(
key=item.value,
source="systemconfig",
group=metadata.get("group", "misc"),
label=metadata.get("label", item.value),
)
return core_specs, system_specs
CORE_SETTING_SPECS, SYSTEMCONFIG_SETTING_SPECS = _build_specs()
ALL_SETTING_SPECS = {**CORE_SETTING_SPECS, **SYSTEMCONFIG_SETTING_SPECS}
SETTING_KEY_ALIASES = {}
for key in CORE_SETTING_SPECS:
SETTING_KEY_ALIASES[_normalize_token(key)] = key
for item in SystemConfigKey:
SETTING_KEY_ALIASES[_normalize_token(item.value)] = item.value
SETTING_KEY_ALIASES[_normalize_token(item.name)] = item.value
SINGLE_KEY_GROUP_ALIASES = {
_normalize_token(alias): next(
(
spec.key
for spec in SYSTEMCONFIG_SETTING_SPECS.values()
if spec.group == canonical_group
),
None,
)
for alias, canonical_group in GROUP_ALIASES.items()
if canonical_group not in {"all", "settings", "systemconfig"}
and len(
[
spec.key
for spec in SYSTEMCONFIG_SETTING_SPECS.values()
if spec.group == canonical_group
]
)
== 1
}
def normalize_group(group: Optional[str]) -> str:
if not group:
return "all"
normalized = GROUP_ALIASES.get(_normalize_token(group))
if not normalized:
raise ValueError(
"group 不支持,支持值包括 all/settings/systemconfig 以及"
" downloaders、media_servers、notifications、storages、directories、"
"search_sites、subscribe_sites、site_auth、ai_agent 等分类别名"
)
return normalized
def resolve_setting_spec(setting_key: Optional[str]) -> Optional[SettingSpec]:
"""把精确键名、枚举名或单键分组别名解析为统一的设置定义。"""
if not setting_key:
return None
normalized = _normalize_token(setting_key)
resolved_key = SETTING_KEY_ALIASES.get(normalized) or SINGLE_KEY_GROUP_ALIASES.get(
normalized
)
if not resolved_key:
return None
return ALL_SETTING_SPECS.get(resolved_key)
def list_setting_specs(
group: Optional[str] = "all", keyword: Optional[str] = None
) -> list[SettingSpec]:
"""按分组和关键字筛选可查询的设置项。"""
normalized_group = normalize_group(group)
if normalized_group == "all":
specs = list(ALL_SETTING_SPECS.values())
elif normalized_group == "settings":
specs = list(CORE_SETTING_SPECS.values())
elif normalized_group == "systemconfig":
specs = list(SYSTEMCONFIG_SETTING_SPECS.values())
else:
specs = [
spec
for spec in SYSTEMCONFIG_SETTING_SPECS.values()
if spec.group == normalized_group
]
if keyword:
normalized_keyword = _normalize_token(keyword)
specs = [
spec
for spec in specs
if normalized_keyword in _normalize_token(spec.key)
or normalized_keyword in _normalize_token(spec.group)
or normalized_keyword in _normalize_token(spec.label)
]
return sorted(specs, key=lambda spec: (spec.source, spec.group, spec.key))
def get_default_list_match_field(setting_key: str) -> Optional[str]:
return LIST_ITEM_MATCH_FIELD_DEFAULTS.get(setting_key)

View File

@@ -27,6 +27,7 @@ class QueryCustomIdentifiersTool(MoviePilotTool):
"Returns the list of identifier rules used for preprocessing torrent/file names before media recognition. "
"Use this tool to check existing rules before adding new ones to avoid duplicates."
)
require_admin: bool = True
args_schema: Type[BaseModel] = QueryCustomIdentifiersInput
def get_tool_message(self, **kwargs) -> Optional[str]:

View File

@@ -24,6 +24,7 @@ class QueryDirectorySettingsInput(BaseModel):
class QueryDirectorySettingsTool(MoviePilotTool):
name: str = "query_directory_settings"
description: str = "Query system directory configuration settings (NOT file listings). Returns configured directory paths, storage types, transfer modes, and other directory-related settings. Use 'list_directory' to list actual files and folders in a directory."
require_admin: bool = True
args_schema: Type[BaseModel] = QueryDirectorySettingsInput
def get_tool_message(self, **kwargs) -> Optional[str]:

View File

@@ -19,6 +19,7 @@ class QueryDownloadersInput(BaseModel):
class QueryDownloadersTool(MoviePilotTool):
name: str = "query_downloaders"
description: str = "Query downloader configuration and list all available downloaders. Shows downloader status, connection details, and configuration settings."
require_admin: bool = True
args_schema: Type[BaseModel] = QueryDownloadersInput
def get_tool_message(self, **kwargs) -> Optional[str]:

View File

@@ -0,0 +1,186 @@
"""统一查询系统设置工具。"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.impl._system_setting_utils import (
SettingSpec,
list_setting_specs,
resolve_setting_spec,
)
from app.core.config import settings
from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
class QuerySystemSettingsInput(BaseModel):
"""查询系统设置工具的输入参数模型。"""
explanation: str = Field(
...,
description="Clear explanation of why this tool is being used in the current context",
)
setting_key: Optional[str] = Field(
None,
description=(
"Exact setting key to query. Supports Settings field names like 'APP_DOMAIN' or 'TMDB_API_KEY', "
"SystemConfigKey values like 'Downloaders' or 'MediaServers', enum names, and some single-key aliases "
"such as 'downloaders', 'directories', 'search_sites', 'subscribe_sites', 'site_auth', 'ai_agent', "
"and 'custom_identifiers'."
),
)
group: Optional[str] = Field(
"all",
description=(
"Optional group filter when setting_key is not provided. Supports 'all', 'settings', 'systemconfig', "
"and category aliases such as 'downloaders', 'media_servers', 'notifications', 'notification_switches', "
"'storages', 'directories', 'search_sites', 'subscribe_sites', 'site_auth', 'ai_agent', 'filter_rules', "
"'subscribe_defaults', 'plugins', and 'custom_identifiers'. Chinese aliases are also accepted."
),
)
keyword: Optional[str] = Field(
None,
description=(
"Optional keyword used to fuzzy match setting keys, group names, or labels when listing settings."
),
)
include_values: Optional[bool] = Field(
None,
description=(
"Whether to include full setting values. Default behavior: when a single setting is matched it returns the full value; "
"when multiple settings are matched it returns summaries only unless this is explicitly set to true."
),
)
class QuerySystemSettingsTool(MoviePilotTool):
name: str = "query_system_settings"
description: str = (
"Query system settings across both the basic Settings module and all SystemConfig-backed categories. "
"Use this tool to inspect downloaders, media servers, notification channels, storages, directories, search-site ranges, "
"subscribe-site ranges, site auth params, AI agent config, and any other system setting before making changes."
)
require_admin: bool = True
args_schema: Type[BaseModel] = QuerySystemSettingsInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据查询参数生成友好的提示消息。"""
setting_key = kwargs.get("setting_key")
group = kwargs.get("group", "all")
keyword = kwargs.get("keyword")
if setting_key:
return f"查询系统设置: {setting_key}"
if keyword:
return f"筛选系统设置: {group} / {keyword}"
return f"查询系统设置分组: {group}"
@staticmethod
def _load_setting_value(spec: SettingSpec):
if spec.source == "settings":
return getattr(settings, spec.key)
return SystemConfigOper().get(spec.key)
@staticmethod
def _summarize_value(value) -> dict:
summary = {
"has_value": value is not None,
"value_type": type(value).__name__,
}
if isinstance(value, list):
summary["item_count"] = len(value)
if value:
summary["item_type"] = type(value[0]).__name__
elif isinstance(value, dict):
keys = list(value.keys())
summary["item_count"] = len(keys)
summary["keys_preview"] = keys[:10]
if len(keys) > 10:
summary["keys_truncated"] = True
elif isinstance(value, str):
summary["length"] = len(value)
preview = value[:200]
if preview:
summary["value_preview"] = preview
if len(value) > len(preview):
summary["value_truncated"] = True
elif value is not None:
summary["value_preview"] = value
return summary
async def run(
self,
setting_key: Optional[str] = None,
group: Optional[str] = "all",
keyword: Optional[str] = None,
include_values: Optional[bool] = None,
**kwargs,
) -> str:
logger.info(
"执行工具: %s, setting_key=%s, group=%s, keyword=%s",
self.name,
setting_key,
group,
keyword,
)
try:
if setting_key:
spec = resolve_setting_spec(setting_key)
if not spec:
return json.dumps(
{
"success": False,
"message": f"系统设置项 '{setting_key}' 不存在",
},
ensure_ascii=False,
)
specs = [spec]
else:
specs = list_setting_specs(group=group, keyword=keyword)
if not specs:
return json.dumps(
{
"success": False,
"message": "没有找到匹配的系统设置项",
},
ensure_ascii=False,
)
should_include_values = (
include_values if include_values is not None else len(specs) == 1
)
settings_payload = []
for spec in specs:
value = self._load_setting_value(spec)
item = {
"setting_key": spec.key,
"source": spec.source,
"group": spec.group,
"label": spec.label,
}
item.update(self._summarize_value(value))
if should_include_values:
item["value"] = value
settings_payload.append(item)
return json.dumps(
{
"success": True,
"matched_count": len(settings_payload),
"include_values": should_include_values,
"settings": settings_payload,
},
ensure_ascii=False,
indent=2,
default=str,
)
except Exception as e:
logger.error(f"查询系统设置失败: {e}", exc_info=True)
return json.dumps(
{"success": False, "message": f"查询系统设置时发生错误: {str(e)}"},
ensure_ascii=False,
)

View File

@@ -52,6 +52,7 @@ class UpdateCustomIdentifiersTool(MoviePilotTool):
"Lines starting with '#' are comments. "
"The replacement target supports: {[tmdbid=xxx;type=movie/tv;s=xxx;e=xxx]} for direct TMDB ID matching."
)
require_admin: bool = True
args_schema: Type[BaseModel] = UpdateCustomIdentifiersInput
def get_tool_message(self, **kwargs) -> Optional[str]:

View File

@@ -0,0 +1,305 @@
"""统一更新系统设置工具。"""
import copy
import json
from typing import Any, Literal, Optional, Type, Union
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.impl._system_setting_utils import (
SettingSpec,
get_default_list_match_field,
resolve_setting_spec,
)
from app.core.config import settings
from app.core.event import eventmanager
from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
from app.schemas.event import ConfigChangeEventData
from app.schemas.types import EventType
SettingValue = Optional[Union[list, dict, bool, int, float, str]]
class UpdateSystemSettingsInput(BaseModel):
"""更新系统设置工具的输入参数模型。"""
explanation: str = Field(
...,
description="Clear explanation of why this tool is being used in the current context",
)
setting_key: str = Field(
...,
description=(
"Exact setting key to update. Supports Settings field names, SystemConfigKey values, enum names, and common aliases "
"such as 'downloaders', 'directories', 'search_sites', 'subscribe_sites', 'site_auth', 'ai_agent', and 'custom_identifiers'."
),
)
value: SettingValue = Field(
None,
description=(
"The new value or list item payload. For replace: this becomes the entire setting value. For merge_dict: this should be a dict of keys to merge. "
"For upsert_list_item/remove_list_item: this can be a dict item or a scalar list item."
),
)
operation: Literal[
"replace",
"merge_dict",
"upsert_list_item",
"remove_list_item",
] = Field(
"replace",
description=(
"Update operation. replace replaces the whole value; merge_dict merges dict keys (optionally with remove_keys); "
"upsert_list_item inserts or replaces one item inside a list; remove_list_item removes one item from a list."
),
)
remove_keys: Optional[list[str]] = Field(
None,
description="Optional dict keys to delete when operation is merge_dict.",
)
match_field: Optional[str] = Field(
None,
description=(
"Optional match field for list item upsert/remove. If omitted, common SystemConfig categories use built-in defaults such as 'name' or 'type'."
),
)
match_value: SettingValue = Field(
None,
description=(
"Optional explicit value used to locate a list item when operation is upsert_list_item or remove_list_item."
),
)
class UpdateSystemSettingsTool(MoviePilotTool):
name: str = "update_system_settings"
description: str = (
"Update system settings across both the basic Settings module and all SystemConfig-backed categories. "
"Supports full replacement, shallow dict merge, and generic list item upsert/remove so the agent can manage downloaders, media servers, notification channels, storages, directories, search-site ranges, subscribe-site ranges, site auth params, AI agent config, and other system settings through one tool."
)
require_admin: bool = True
args_schema: Type[BaseModel] = UpdateSystemSettingsInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据更新参数生成友好的提示消息。"""
setting_key = kwargs.get("setting_key", "")
operation = kwargs.get("operation", "replace")
action_map = {
"replace": "覆盖系统设置",
"merge_dict": "合并系统设置",
"upsert_list_item": "更新列表项",
"remove_list_item": "移除列表项",
}
return f"{action_map.get(operation, '更新系统设置')}: {setting_key}"
@staticmethod
def _load_setting_value(spec: SettingSpec):
if spec.source == "settings":
return getattr(settings, spec.key)
return SystemConfigOper().get(spec.key)
@staticmethod
def _normalize_systemconfig_value(value: Any):
if isinstance(value, list):
filtered = [item for item in value if item is not None]
return filtered or None
return value
@staticmethod
def _resolve_list_match(
spec: SettingSpec,
operation: str,
value: Any,
match_field: Optional[str],
match_value: Any,
) -> tuple[Optional[str], Any]:
resolved_field = match_field or get_default_list_match_field(spec.key)
resolved_value = match_value
if isinstance(value, dict):
if not resolved_field:
raise ValueError(
f"{operation} 需要提供 match_field或使用带默认匹配字段的系统配置项"
)
if resolved_value is None:
resolved_value = value.get(resolved_field)
if resolved_value is None:
raise ValueError(
f"{operation} 缺少匹配值,请在 value.{resolved_field} 或 match_value 中提供"
)
else:
if resolved_value is None:
resolved_value = value
return resolved_field, resolved_value
@classmethod
def _prepare_next_value(
cls,
spec: SettingSpec,
current_value: Any,
value: Any,
operation: str,
remove_keys: Optional[list[str]] = None,
match_field: Optional[str] = None,
match_value: Any = None,
) -> Any:
remove_keys = remove_keys or []
if operation == "replace":
return value
if operation == "merge_dict":
if remove_keys and not isinstance(remove_keys, list):
raise ValueError("remove_keys 必须是字符串列表")
if current_value is not None and not isinstance(current_value, dict):
raise ValueError("merge_dict 仅支持当前值为 dict 的设置项")
if value is not None and not isinstance(value, dict):
raise ValueError("merge_dict 的 value 必须是 dict 或 null")
next_value = dict(current_value or {})
if value:
next_value.update(value)
for key in remove_keys:
next_value.pop(key, None)
return next_value
if operation not in {"upsert_list_item", "remove_list_item"}:
raise ValueError(f"不支持的操作: {operation}")
if current_value is not None and not isinstance(current_value, list):
raise ValueError(f"{operation} 仅支持当前值为 list 的设置项")
next_items = list(copy.deepcopy(current_value or []))
resolved_field, resolved_match_value = cls._resolve_list_match(
spec, operation, value, match_field, match_value
)
if operation == "upsert_list_item":
if value is None:
raise ValueError("upsert_list_item 必须提供 value")
replaced = False
for index, item in enumerate(next_items):
if resolved_field:
if isinstance(item, dict) and item.get(resolved_field) == resolved_match_value:
next_items[index] = value
replaced = True
break
elif item == resolved_match_value:
next_items[index] = value
replaced = True
break
if not replaced:
next_items.append(value)
return next_items
return [
item
for item in next_items
if not (
isinstance(item, dict)
and resolved_field
and item.get(resolved_field) == resolved_match_value
)
and not (not resolved_field and item == resolved_match_value)
]
async def run(
self,
setting_key: str,
value: SettingValue = None,
operation: str = "replace",
remove_keys: Optional[list[str]] = None,
match_field: Optional[str] = None,
match_value: SettingValue = None,
**kwargs,
) -> str:
logger.info(
"执行工具: %s, setting_key=%s, operation=%s",
self.name,
setting_key,
operation,
)
try:
spec = resolve_setting_spec(setting_key)
if not spec:
return json.dumps(
{
"success": False,
"message": f"系统设置项 '{setting_key}' 不存在",
},
ensure_ascii=False,
)
current_value = self._load_setting_value(spec)
next_value = self._prepare_next_value(
spec=spec,
current_value=current_value,
value=value,
operation=operation,
remove_keys=remove_keys,
match_field=match_field,
match_value=match_value,
)
event_value = next_value
changed = False
message = ""
if spec.source == "settings":
success, message = settings.update_setting(spec.key, next_value)
if success is False:
return json.dumps(
{
"success": False,
"message": message or f"更新设置 {spec.key} 失败",
},
ensure_ascii=False,
)
changed = success is True
else:
normalized_value = self._normalize_systemconfig_value(next_value)
event_value = normalized_value
success = await SystemConfigOper().async_set(spec.key, normalized_value)
changed = success is True
if changed:
await eventmanager.async_send_event(
etype=EventType.ConfigChanged,
data=ConfigChangeEventData(
key=spec.key,
value=event_value,
change_type="update",
),
)
saved_value = self._load_setting_value(spec)
if not changed and not message:
message = "配置值未发生变化"
return json.dumps(
{
"success": True,
"message": message or f"系统设置 {spec.key} 已更新",
"changed": changed,
"operation": operation,
"setting": {
"setting_key": spec.key,
"source": spec.source,
"group": spec.group,
"label": spec.label,
},
"previous_value": current_value,
"saved_value": saved_value,
},
ensure_ascii=False,
indent=2,
default=str,
)
except Exception as e:
logger.error(f"更新系统设置失败: {e}", exc_info=True)
return json.dumps(
{"success": False, "message": f"更新系统设置时发生错误: {str(e)}"},
ensure_ascii=False,
)