diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 2d214b77..4b25c977 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -9,6 +9,7 @@ from astrbot.core.platform.register import platform_registry from astrbot.core.provider.register import provider_registry from astrbot.core.star.star import star_registry from astrbot.core import logger +import asyncio def try_cast(value: str, type_: str): @@ -164,10 +165,84 @@ class ConfigRoute(Route): "/config/provider/update": ("POST", self.post_update_provider), "/config/provider/delete": ("POST", self.post_delete_provider), "/config/llmtools": ("GET", self.get_llm_tools), + "/config/provider/check_status": ("GET", self.check_all_providers_status), "/config/provider/list": ("GET", self.get_provider_config_list), } self.register_routes() + async def _test_single_provider(self, provider): + """辅助函数:测试单个 provider 的可用性""" + meta = provider.meta() + provider_name = provider.provider_config.get("id", "Unknown Provider") + if not provider_name and meta: + provider_name = meta.id + elif not provider_name: + provider_name = "Unknown Provider" + status_info = { + "id": meta.id if meta else "Unknown ID", + "model": meta.model if meta else "Unknown Model", + "type": meta.type if meta else "Unknown Type", + "name": provider_name, + "status": "unavailable", # 默认为不可用 + "error": None, + } + logger.debug(f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})") + try: + logger.debug(f"Sending 'Ping' to provider: {status_info['name']}") + response = await asyncio.wait_for(provider.text_chat(prompt="Ping"), timeout=20.0) # 超时 20 秒 + logger.debug(f"Received response from {status_info['name']}: {response}") + # 只要 text_chat 调用成功返回一个 LLMResponse 对象 (即 response 不为 None),就认为可用 + if response is not None: + status_info["status"] = "available" + response_text_snippet = "" + if hasattr(response, 'completion_text') and response.completion_text: + response_text_snippet = response.completion_text[:70] + "..." if len(response.completion_text) > 70 else response.completion_text + elif hasattr(response, 'result_chain') and response.result_chain: + try: + response_text_snippet = response.result_chain.get_plain_text()[:70] + "..." if len(response.result_chain.get_plain_text()) > 70 else response.result_chain.get_plain_text() + except: + pass + logger.info(f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'") + else: + # 这个分支理论上不应该被走到,除非 text_chat 实现可能返回 None + status_info["error"] = "Test call returned None, but expected an LLMResponse object." + logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.") + + except asyncio.TimeoutError: + status_info["error"] = "Connection timed out after 10 seconds during test call." + logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.") + except Exception as e: + error_message = str(e) + status_info["error"] = error_message + logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}") + logger.debug(f"Traceback for {status_info['name']}:\n{traceback.format_exc()}") + return status_info + + async def check_all_providers_status(self): + """ + API 接口: 检查所有 LLM Providers 的状态 + """ + logger.info("API call received: /config/provider/check_status") + try: + all_providers: typing.List = self.core_lifecycle.star_context.get_all_providers() + logger.debug(f"Found {len(all_providers)} providers to check.") + + if not all_providers: + logger.info("No providers found to check.") + return Response().ok([]).__dict__ + + tasks = [self._test_single_provider(p) for p in all_providers] + logger.debug(f"Created {len(tasks)} tasks for concurrent provider checks.") + + results = await asyncio.gather(*tasks) + logger.info(f"Provider status check completed. Results: {results}") + + return Response().ok(results).__dict__ + except Exception as e: + logger.error(f"Critical error in check_all_providers_status: {str(e)}") + logger.error(traceback.format_exc()) + return Response().error(f"检查 Provider 状态时发生严重错误: {str(e)}").__dict__ + async def get_configs(self): # plugin_name 为空时返回 AstrBot 配置 # 否则返回指定 plugin_name 的插件配置 diff --git a/dashboard/src/views/ProviderPage.vue b/dashboard/src/views/ProviderPage.vue index 53e7c8bc..55c61f70 100644 --- a/dashboard/src/views/ProviderPage.vue +++ b/dashboard/src/views/ProviderPage.vue @@ -87,6 +87,51 @@ + + + + mdi-heart-pulse + 供应商可用性 + + + mdi-refresh + 刷新状态 + + + + 通过测试模型对话可用性判断,可能产生API费用 + + + + + + + 点击"刷新状态"按钮获取供应商可用性 + + + + + + + + + {{ status.status === 'available' ? 'mdi-check-circle' : 'mdi-alert-circle' }} + + {{ status.id }} + + {{ status.status === 'available' ? '可用' : '不可用' }} + + + + 错误信息: {{ status.error }} + + + + + + + + @@ -251,6 +296,10 @@ export default { save_message_success: "success", showConsole: false, + + // 供应商状态相关 + providerStatuses: [], + loadingStatus: false, // 新增提供商对话框相关 showAddProviderDialog: false, @@ -497,6 +546,22 @@ export default { this.save_message = message; this.save_message_success = "error"; this.save_message_snack = true; + }, + + // 获取供应商状态 + fetchProviderStatus() { + this.loadingStatus = true; + axios.get('/api/config/provider/check_status').then((res) => { + if (res.data && res.data.status === 'ok') { + this.providerStatuses = res.data.data || []; + } else { + this.showError(res.data?.message || "获取供应商状态失败"); + } + this.loadingStatus = false; + }).catch((err) => { + this.loadingStatus = false; + this.showError(err.response?.data?.message || err.message); + }); } } }