"""运行时配置 API:GET/PUT /api/v1/settings、GET /api/v1/settings/schema、POST /api/v1/settings/reset GET 任何登录用户可读;PUT/reset 需 admin。PUT 后会清空 LLM 客户端 / 解析插件 / 去重策略三处进程级缓存,使新配置立即对后续请求生效。 """ from typing import Any import structlog from fastapi import APIRouter, Depends from pydantic import BaseModel from app.api.response import ApiError, ok from app.core.auth import AuthUser, get_current_user, require_admin from app.core.dedup import invalidate_dedup_strategy_cache from app.core.file_parser import ( list_docx_plugins, list_ocr_plugins, list_pdf_plugins, invalidate_parser_plugin_cache, ) from app.core.runtime_settings import ( RuntimeSettings, get_runtime_settings, reset_runtime_settings, update_runtime_settings, ) from app.services.llm import invalidate_llm_client_cache logger = structlog.get_logger() router = APIRouter(prefix="/api/v1", tags=["settings"]) class SettingsUpdateRequest(BaseModel): """Settings PATCH body:任意子树可缺省,缺省字段保留原值 例:{"models": {"summarize": {"model": "qwen2.5:3b"}}}、 {"parsers": {"ocr": {"plugin": "tesseract"}}}、 {"dedup": {"strategy": "simhash", "simhash_threshold": 5}} """ models: dict[str, Any] | None = None parsers: dict[str, Any] | None = None dedup: dict[str, Any] | None = None @router.get("/settings") async def get_settings(user: AuthUser = Depends(get_current_user)) -> dict[str, Any]: """返回当前 RuntimeSettings(任何登录用户可读)""" cfg = get_runtime_settings() return ok(cfg.model_dump(mode="json")) @router.put("/settings") async def update_settings( body: SettingsUpdateRequest, user: AuthUser = Depends(require_admin), ) -> dict[str, Any]: """部分更新 RuntimeSettings(仅 admin) 更新成功后清空 LLM 客户端 / 解析插件 / 去重策略三处进程级缓存, 使新配置立即对后续请求生效。 """ patch = body.model_dump(exclude_none=True) if not patch: raise ApiError(1001, "请求体为空,未提供任何待更新字段") try: cfg = update_runtime_settings(patch) except Exception as exc: logger.error("RuntimeSettings 更新失败", error=str(exc), exc_info=True) raise ApiError(2000, f"配置更新失败: {exc}") from exc # 清缓存:让后续读取拿到新配置 invalidate_llm_client_cache() invalidate_parser_plugin_cache() invalidate_dedup_strategy_cache() logger.info("RuntimeSettings 已更新并清理缓存", operator=user.username) return ok(cfg.model_dump(mode="json")) @router.get("/settings/schema") async def get_settings_schema( user: AuthUser = Depends(get_current_user), ) -> dict[str, Any]: """返回可选插件与策略列表(前端 Settings 页渲染选项用)""" return ok( { "llm_providers": ["ollama", "openai_compatible"], "pdf_plugins": list_pdf_plugins(), "docx_plugins": list_docx_plugins(), "ocr_plugins": list_ocr_plugins(), "dedup_strategies": ["none", "sha256", "simhash"], } ) @router.post("/settings/reset") async def reset_settings( user: AuthUser = Depends(require_admin), ) -> dict[str, Any]: """重置 RuntimeSettings 为默认值(仅 admin),同时清缓存""" try: cfg = reset_runtime_settings() except Exception as exc: logger.error("RuntimeSettings 重置失败", error=str(exc), exc_info=True) raise ApiError(2000, f"配置重置失败: {exc}") from exc invalidate_llm_client_cache() invalidate_parser_plugin_cache() invalidate_dedup_strategy_cache() logger.info("RuntimeSettings 已重置为默认值", operator=user.username) return ok(cfg.model_dump(mode="json"))