diff --git a/api/insurance/admin/ppt_admin_routes.py b/api/insurance/admin/ppt_admin_routes.py index f298623..57808dc 100644 --- a/api/insurance/admin/ppt_admin_routes.py +++ b/api/insurance/admin/ppt_admin_routes.py @@ -227,6 +227,12 @@ def export_history(): # ---- 系统配置 ---- +@ppt_admin_bp.route("/available-models", methods=["GET"]) +@jwt_required +def get_available_models(): + return jsonify(ppt_admin_service.get_available_models()) + + @ppt_admin_bp.route("/settings", methods=["GET"]) @jwt_required def get_settings(): diff --git a/api/insurance/admin/ppt_admin_service.py b/api/insurance/admin/ppt_admin_service.py index 82c5416..81a19cd 100644 --- a/api/insurance/admin/ppt_admin_service.py +++ b/api/insurance/admin/ppt_admin_service.py @@ -476,6 +476,59 @@ class PptAdminService: # ---- 系统配置 ---- + # 内置常用模型列表(Dify 不可用时的降级方案) + _BUILTIN_MODELS = [ + {"provider": "deepseek", "model": "deepseek-chat", "label": "DeepSeek Chat"}, + {"provider": "deepseek", "model": "deepseek-reasoner", "label": "DeepSeek Reasoner"}, + {"provider": "minimax", "model": "MiniMax-2.7-Flash", "label": "MiniMax 2.7 Flash"}, + {"provider": "gemini", "model": "gemini-2.5-flash", "label": "Gemini 2.5 Flash"}, + {"provider": "gemini", "model": "gemini-2.5-pro", "label": "Gemini 2.5 Pro"}, + {"provider": "openai", "model": "gpt-4o", "label": "GPT-4o"}, + {"provider": "openai", "model": "gpt-4o-mini", "label": "GPT-4o Mini"}, + {"provider": "openai", "model": "gpt-image-1", "label": "GPT Image 1"}, + ] + + def get_available_models(self) -> dict: + """获取可用 LLM 模型列表,优先从 Dify 获取,失败时返回内置列表。""" + # 尝试从 Dify 获取 + dify_models = self._fetch_dify_models() + if dify_models is not None: + return {"code": 0, "data": {"models": dify_models, "source": "dify"}} + return {"code": 0, "data": {"models": self._BUILTIN_MODELS, "source": "builtin"}} + + def _fetch_dify_models(self) -> list | None: + """调用 Dify API 获取可用 LLM 模型列表,失败返回 None。""" + import requests + try: + # 从 SystemSetting 读取 Dify Workspace API Key + key_setting = SystemSetting.query.filter_by(key="dify_workspace_api_key").first() + api_key = key_setting.value if key_setting and key_setting.value else "" + if not api_key: + return None + + base_url = os.getenv("DIFY_BASE_URL", "http://localhost:5001") + resp = requests.get( + f"{base_url}/v1/workspaces/current/models/model-types/llm", + headers={"Authorization": f"Bearer {api_key}"}, + timeout=10, + ) + if resp.status_code != 200: + logger.warning(f"Dify 模型查询失败: HTTP {resp.status_code}") + return None + + data = resp.json() + models = [] + for item in data.get("data", []): + provider = item.get("provider", "") + for model in item.get("models", []): + model_name = model.get("model", "") + label = model.get("label", {}).get("zh_Hans", "") or model.get("label", {}).get("en_US", "") or model_name + models.append({"provider": provider, "model": model_name, "label": label}) + return models if models else None + except Exception as e: + logger.warning(f"从 Dify 获取模型列表失败: {e}") + return None + def get_settings(self) -> dict: settings = SystemSetting.query.all() return { diff --git a/api/insurance/ppt/llm_client.py b/api/insurance/ppt/llm_client.py index ff4fa2a..24015c7 100644 --- a/api/insurance/ppt/llm_client.py +++ b/api/insurance/ppt/llm_client.py @@ -204,8 +204,13 @@ class LLMClient: self._configs: list[tuple[LLMProviderConfig, str]] = [] self._limiters: dict[str, RateLimiter] = {} self._active_idx = 0 + self._db_config_time: float = 0 # 上次从数据库加载配置的时间戳 + self._db_config_ttl: float = 60 # 配置缓存有效期(秒) - # 从环境变量加载 + self._load_env_config() + + def _load_env_config(self): + """从环境变量加载默认配置。""" deepseek_key = os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENAI_API_KEY", "") minimax_key = os.environ.get("MINIMAX_API_KEY", "") gemini_key = os.environ.get("GEMINI_API_KEY", "") @@ -223,6 +228,54 @@ class LLMClient: if not self._configs: logger.warning("[LLMClient] 未配置任何 API Key,使用 mock 模式") + def _try_load_db_config(self): + """尝试从数据库加载模型配置(带缓存,不阻塞)。""" + now = time.monotonic() + if now - self._db_config_time < self._db_config_ttl: + return + self._db_config_time = now + try: + from insurance.models.system_setting import SystemSetting + settings = {s.key: s.value for s in SystemSetting.query.filter( + SystemSetting.key.in_([ + "ppt_llm_provider", "ppt_llm_model", "ppt_llm_api_key", "ppt_llm_base_url", + ]) + ).all()} + provider = settings.get("ppt_llm_provider", "").strip() + api_key = settings.get("ppt_llm_api_key", "").strip() + if not provider or not api_key: + return + + model = settings.get("ppt_llm_model", "").strip() + base_url = settings.get("ppt_llm_base_url", "").strip() + + # 内置供应商:替换对应配置 + if provider in PROVIDERS and not base_url: + cfg = PROVIDERS[provider] + if model: + cfg = LLMProviderConfig( + name=cfg.name, base_url=cfg.base_url, model=model, + max_retries=cfg.max_retries, rate_limit=cfg.rate_limit, + ) + self._configs = [(cfg, api_key)] + self._limiters = {provider: RateLimiter(cfg.rate_limit)} + self._active_idx = 0 + logger.info(f"[LLMClient] 使用数据库配置: {provider}/{cfg.model}") + return + + # 自定义供应商 + if not base_url: + return + cfg = LLMProviderConfig( + name=provider, base_url=base_url, model=model or "gpt-4o-mini", + ) + self._configs = [(cfg, api_key)] + self._limiters = {provider: RateLimiter(0)} + self._active_idx = 0 + logger.info(f"[LLMClient] 使用自定义模型: {provider}/{cfg.model}") + except Exception: + pass # 无 Flask 上下文或数据库不可用,使用环境变量配置 + async def chat(self, prompt: str, system_prompt: str = "") -> LLMResponse: """简单聊天。""" messages = [] @@ -271,6 +324,7 @@ class LLMClient: async def _call(self, messages: list[dict], attempt: int = 0) -> LLMResponse: """多供应商自动切换调用。""" + self._try_load_db_config() if not self._configs: return LLMResponse(content="{}", provider="mock", latency_ms=0) diff --git a/frontend/src/pages/admin/PptSettingsAdmin.vue b/frontend/src/pages/admin/PptSettingsAdmin.vue index f87497c..4570a57 100644 --- a/frontend/src/pages/admin/PptSettingsAdmin.vue +++ b/frontend/src/pages/admin/PptSettingsAdmin.vue @@ -1,68 +1,233 @@ diff --git a/frontend/src/utils/ppt-admin-api.ts b/frontend/src/utils/ppt-admin-api.ts index c347742..8bf074b 100644 --- a/frontend/src/utils/ppt-admin-api.ts +++ b/frontend/src/utils/ppt-admin-api.ts @@ -102,4 +102,7 @@ export const pptAdminApi = { updateSettings(data: Record) { return api.put('/admin/ppt/settings', data) }, + getAvailableModels() { + return api.get('/admin/ppt/available-models') + }, }