diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 864a65547..5db069747 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -110,6 +110,17 @@ _PUBLIC_SYSTEM_CONFIG_KEYS = { _PUBLIC_SETTINGS_KEYS = {"PLUGIN_MARKET"} +def _project_public_storages(value: Any) -> list[dict[str, Any]]: + """存储配置只公开名称和类型,与 /storage/options 一致,config 中的连接配置不返回。""" + if not isinstance(value, list): + return [] + return [ + {"name": item.get("name") or item["type"], "type": item["type"]} + for item in value + if isinstance(item, dict) and item.get("type") + ] + + async def _get_image_proxy_allowed_domains() -> set[str]: """合并站点快照与当前图片代理配置,仍逐请求执行 DNS 安全校验。""" runtime_settings = get_runtime_settings() @@ -660,7 +671,7 @@ async def get_progress( ) async def get_public_setting(key: str, _: ApiPrincipal = Depends(get_current_active_user_async)) -> _SchemaResponse: """ - 查询普通用户可读取的非敏感系统设置 + 查询普通用户可读取的非敏感系统设置,存储配置只返回名称和类型 """ if key in _PUBLIC_SETTINGS_KEYS: return _SchemaResponse( @@ -669,7 +680,10 @@ async def get_public_setting(key: str, _: ApiPrincipal = Depends(get_current_act ) if key not in _PUBLIC_SYSTEM_CONFIG_KEYS: raise HTTPException(status_code=404, detail="配置项不存在") - value = get_configured_system_config().get(_PUBLIC_SYSTEM_CONFIG_KEYS[key]) + config_key = _PUBLIC_SYSTEM_CONFIG_KEYS[key] + value = get_configured_system_config().get(config_key) + if config_key is SystemConfigKey.Storages: + value = _project_public_storages(value) return _SchemaResponse(success=True, data={"value": value}) diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 7629a63b8..46dc1a8d4 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -743,7 +743,7 @@ AMLL 使用无需鉴权的原生搜索与获取接口,先尝试 ISRC,再核 | GET | `/api/v1/dashboard/schedule` | 查询所有后台定时服务,包含当前完成百分比、进度文本和执行状态 | | GET | `/api/v1/dashboard/schedule/{job_id}/progress` | 查询指定后台定时服务的实时进度详情 | | GET | `/api/v1/dashboard/schedule2/{job_id}/progress` | 使用 API_TOKEN 查询指定后台定时服务的实时进度详情 | -| GET | `/api/v1/system/setting/public/{key}` | 登录用户读取白名单内非敏感系统设置,仅支持目录、存储、站点范围、默认订阅规则、Follow 订阅者和插件市场地址等前端必需配置 | +| GET | `/api/v1/system/setting/public/{key}` | 登录用户读取白名单内非敏感系统设置,仅支持目录、存储、站点范围、默认订阅规则、Follow 订阅者和插件市场地址等前端必需配置;存储只返回 `name` 和 `type`,与 `/api/v1/storage/options` 一致,不含连接配置 | | POST | `/api/v1/system/setting/PLUGIN_MARKET/sync-wiki` | 管理员从 MoviePilot Wiki 的插件文档同步公开插件仓库清单,和本地 `PLUGIN_MARKET` 合并去重后写入配置 | | GET | `/api/v1/system/module-catalog` | 查询宿主模块及其服务类型目录,供前端选择器构造选项 | | GET | `/api/v1/system/modulelist` | 查询已启用模块,保留 `name` 原始中文字段,并提供 `name_i18n` 和 `name_key` 给多语言前端展示 | diff --git a/tests/test_api_authorization.py b/tests/test_api_authorization.py index 7f481cd8d..6b35d7d4b 100644 --- a/tests/test_api_authorization.py +++ b/tests/test_api_authorization.py @@ -232,6 +232,65 @@ def test_system_public_setting_allows_only_non_sensitive_keys(monkeypatch): assert exc_info.value.detail == "配置项不存在" +def test_system_public_setting_storages_returns_name_and_type_only(monkeypatch): + """公开设置读取存储时只返回名称和类型,与 /storage/options 一致,不含连接配置。""" + storages = [ + {"type": "local", "name": "本地存储", "config": {}}, + { + "type": "smb", + "name": "NAS", + "config": {"host": "192.168.1.2", "username": "smb-user", "password": "smb-password"}, + }, + {"type": "u115", "config": {"refresh_token": "u115-refresh", "access_token": "u115-access"}}, + {"name": "缺少类型", "config": {"token": "untyped-token"}}, + ] + fake_config = SimpleNamespace(get=lambda key: storages if key == SystemConfigKey.Storages else None) + monkeypatch.setattr(system_endpoint, "get_configured_system_config", lambda: fake_config) + monkeypatch.setattr("app.application.storage.get_configured_system_config", lambda: fake_config) + + response = asyncio.run(system_endpoint.get_public_setting(SystemConfigKey.Storages.value)) + + assert response.success is True + assert response.data == { + "value": [ + {"name": "本地存储", "type": "local"}, + {"name": "NAS", "type": "smb"}, + {"name": "u115", "type": "u115"}, + ] + } + assert response.data["value"] == [option.model_dump() for option in storage_endpoint.storage_options()] + serialized = response.model_dump_json() + for value in ("smb-user", "smb-password", "192.168.1.2", "u115-refresh", "u115-access", "untyped-token"): + assert value not in serialized + + +@pytest.mark.parametrize( + "config_key", + [ + SystemConfigKey.Directories, + SystemConfigKey.IndexerSites, + SystemConfigKey.EpisodeFormatRuleTable, + SystemConfigKey.DefaultMovieSubscribeConfig, + SystemConfigKey.DefaultTvSubscribeConfig, + SystemConfigKey.DefaultMusicSubscribeConfig, + SystemConfigKey.FollowSubscribers, + ], +) +def test_system_public_setting_returns_other_keys_as_stored(monkeypatch, config_key): + """存储以外的公开配置仍按原值返回。""" + stored = [{"name": "配置项", "config": {"nested": True}}] + monkeypatch.setattr( + system_endpoint, + "get_configured_system_config", + lambda: SimpleNamespace(get=lambda key: stored if key == config_key else None), + ) + + response = asyncio.run(system_endpoint.get_public_setting(config_key.value)) + + assert response.success is True + assert response.data == {"value": stored} + + def test_system_ping_returns_success(): """服务存活检测接口返回标准成功响应。""" response = asyncio.run(system_endpoint.ping())