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);
+ });
}
}
}