diff --git a/zhenxun/builtin_plugins/llm_manager/__init__.py b/zhenxun/builtin_plugins/llm_manager/__init__.py index 4967fa63..892f41c9 100644 --- a/zhenxun/builtin_plugins/llm_manager/__init__.py +++ b/zhenxun/builtin_plugins/llm_manager/__init__.py @@ -44,6 +44,11 @@ __plugin_meta__ = PluginMetadata( llm keys - 查看指定提供商的所有API Key状态。 + llm reset [ProviderName] + - 重置 API Key 的熔断与冷却状态。 + - 带参数: 仅重置指定提供商的所有 Key。 + - 不带参数: 全局重置所有提供商的所有 Key。 + llm mcp [action] [targets...] - 管理 MCP (Model Context Protocol) 服务。 - 不带参数: 查看当前配置的 MCP 服务列表及序号。 @@ -74,6 +79,9 @@ llm_cmd = on_alconna( "test", Args["model_name", str], alias=["ping"], help_text="测试模型连通性" ), Subcommand("keys", Args["provider_name", str], help_text="查看API密钥状态"), + Subcommand( + "reset", Args["provider_name", str, ""], help_text="重置API密钥状态" + ), Subcommand( "mcp", Option("添加", Args["json_strs", MultiVar(str)], alias=["add"]), @@ -173,6 +181,22 @@ async def handle_keys(arp: Arparma, provider_name: Match[str]): await llm_cmd.finish(MessageUtils.build_message(image)) +@llm_cmd.assign("reset") +async def handle_reset(arp: Arparma): + """处理 'llm reset' 命令""" + provider_name = arp.query("reset.provider_name", "").strip() + target_log = provider_name if provider_name else "ALL" + logger.info( + f"执行 API Key 重置操作: {target_log}", + command="LLM Manage", + session=arp.header_result, + ) + _success, msg = await DataSource.reset_keys( + provider_name if provider_name else None + ) + await llm_cmd.finish(msg) + + @llm_cmd.assign("mcp") async def handle_mcp(arp: Arparma, event: Event): """处理 'llm mcp' 命令""" diff --git a/zhenxun/builtin_plugins/llm_manager/data_source.py b/zhenxun/builtin_plugins/llm_manager/data_source.py index 3ab1a9ee..19da5c07 100644 --- a/zhenxun/builtin_plugins/llm_manager/data_source.py +++ b/zhenxun/builtin_plugins/llm_manager/data_source.py @@ -6,8 +6,10 @@ from zhenxun.configs.path_config import DATA_PATH from zhenxun.services.ai.core.exceptions import LLMException from zhenxun.services.ai.llm.api import chat from zhenxun.services.ai.llm.manager import ( + get_configured_providers, get_model_instance, list_available_models, + reset_key_status, ) from zhenxun.services.ai.tools.providers.mcp.provider import mcp_provider @@ -105,6 +107,34 @@ class DataSource: return sorted_stats_list + @staticmethod + async def reset_keys(provider_name: str | None = None) -> tuple[bool, str]: + """重置指定或所有提供商的 API Key 状态""" + providers = get_configured_providers() + + if provider_name: + target = next( + (p for p in providers if p.name.lower() == provider_name.lower()), None + ) + if not target: + return False, f"❌ 未找到提供商 '{provider_name}',请检查名称是否正确。" + await reset_key_status(target.name) + return ( + True, + f"✅ 已成功重置提供商 '{target.name}'" + "的所有 API Key 状态为健康 (HEALTHY)。", + ) + else: + count = 0 + for p in providers: + await reset_key_status(p.name) + count += 1 + return ( + True, + f"✅ 已成功重置所有提供商 (共 {count} 个) " + "的 API Key 状态为健康 (HEALTHY)。", + ) + @staticmethod async def get_mcp_list() -> list[dict[str, Any]]: """获取排序后的 MCP 列表""" diff --git a/zhenxun/services/ai/llm/adapters/deepseek.py b/zhenxun/services/ai/llm/adapters/deepseek.py index 3bcfdd01..66052077 100644 --- a/zhenxun/services/ai/llm/adapters/deepseek.py +++ b/zhenxun/services/ai/llm/adapters/deepseek.py @@ -1,24 +1,27 @@ from typing import Any +from zhenxun.services.ai.core.messages import ImagePart, LLMMessage from zhenxun.services.ai.core.models import ( ModelCapabilities, ModelDetail, - ModelIdentity, ) -from zhenxun.services.ai.core.options import GenerationConfig +from zhenxun.services.ai.core.options import GenerationConfig, ResponseFormat from .handlers.openai_handlers import ( - OpenAIConfigMapper, - OpenAITextHandler, - OpenAIToolSerializer, + OpenAIResponsesTextHandler, + ResponsesConfigMapper, + ResponsesMessageConverter, + ResponsesResponseParser, + ResponsesToolSerializer, ) -from .openai import OpenAICompatAdapter +from .openai import OpenAIResponsesAdapter -class DeepSeekToolSerializer(OpenAIToolSerializer): +class DeepSeekToolSerializer(ResponsesToolSerializer): """ 专门针对 DeepSeek 的工具序列化器。 负责抹平 Pydantic Schema 与 DeepSeek Strict Mode 之间的差异。 + 由于继承了 ResponsesToolSerializer,已自动获得 web_search 工具的原生格式支持。 """ def __init__(self, api_type: str = "deepseek"): @@ -68,7 +71,35 @@ class DeepSeekToolSerializer(OpenAIToolSerializer): return pipeline.run(schema) -class DeepSeekConfigMapper(OpenAIConfigMapper): +class DeepSeekMessageConverter(ResponsesMessageConverter): + """DeepSeek 专属 Responses 消息转换器""" + + def __init__(self, api_type: str = "deepseek"): + super().__init__(api_type=api_type) + + async def convert_messages_async( + self, messages: list[LLMMessage] + ) -> list[dict[str, Any]]: + input_items = await super().convert_messages_async(messages) + + image_parts = [ + p for msg in messages for p in msg.content if isinstance(p, ImagePart) + ] + img_idx = 0 + + for item in input_items: + if item.get("role") in ("user", "developer") and "content" in item: + for c in item["content"]: + if c.get("type") == "input_image": + if img_idx < len(image_parts): + part = image_parts[img_idx] + img_idx += 1 + if getattr(part, "media_resolution", None): + c["detail"] = str(part.media_resolution).lower() + return input_items + + +class DeepSeekConfigMapper(ResponsesConfigMapper): """DeepSeek 的专属配置映射器""" def map_config( @@ -77,39 +108,38 @@ class DeepSeekConfigMapper(OpenAIConfigMapper): model_detail: ModelDetail | None = None, capabilities: ModelCapabilities | None = None, ) -> dict[str, Any]: - """映射生成参数并处理 DeepSeek 专有 `thinking` 与响应格式差异。""" - params = super().map_config(config, model_detail, capabilities) + """拦截并处理 DeepSeek 目前无法严格遵循复杂 json_schema 的问题""" + if ( + config.output.response_format == ResponseFormat.JSON + and config.output.response_schema + ): + config.output.response_schema = None - if "response_format" in params: - rf = params["response_format"] - if isinstance(rf, dict) and rf.get("type") == "json_schema": - params["response_format"] = {"type": "json_object"} - - return params + return super().map_config(config, model_detail, capabilities) -class DeepSeekTextHandler(OpenAITextHandler): - """DeepSeek 专有文本处理器,替换了特定序列化组件""" +class DeepSeekTextHandler(OpenAIResponsesTextHandler): + """DeepSeek 专有 Responses 文本对话处理器,组装所有定制化子件""" def __init__(self, api_type: str = "deepseek"): - """替换 OpenAI 默认组件为 DeepSeek 专用实现。""" super().__init__(api_type=api_type) + self.converter = DeepSeekMessageConverter(api_type=api_type) self.serializer = DeepSeekToolSerializer(api_type=api_type) self.mapper = DeepSeekConfigMapper(api_type=api_type) + self.parser = ResponsesResponseParser() -class DeepSeekAdapter(OpenAICompatAdapter): - """DeepSeek 官方 API 适配器""" +class DeepSeekAdapter(OpenAIResponsesAdapter): + """DeepSeek Responses API 适配器""" def __init__(self): - """初始化 DeepSeek 适配器并挂载文本处理器。""" super().__init__() self.text_handler = DeepSeekTextHandler(api_type=self.api_type) @property def log_sanitization_context(self) -> str: - """返回 DeepSeek 请求日志清洗上下文。""" - return "openai_request" + """复用 Responses API 的日志清洗上下文。""" + return "openai_responses_request" @property def api_type(self) -> str: @@ -120,7 +150,3 @@ class DeepSeekAdapter(OpenAICompatAdapter): def supported_api_types(self) -> list[str]: """当前适配器支持的 API 类型列表。""" return ["deepseek"] - - def get_chat_endpoint(self, identity: ModelIdentity) -> str: - """返回对话端点,优先使用模型级自定义端点。""" - return "/v1/chat/completions" diff --git a/zhenxun/services/ai/llm/adapters/factory.py b/zhenxun/services/ai/llm/adapters/factory.py index 43d329f3..2ce48981 100644 --- a/zhenxun/services/ai/llm/adapters/factory.py +++ b/zhenxun/services/ai/llm/adapters/factory.py @@ -31,6 +31,7 @@ class LLMAdapterFactory: from .doubao import DoubaoAdapter from .gemini import GeminiAdapter from .glm import GLMAdapter + from .grok import GrokAdapter from .jina import JinaAdapter from .mimo import MiMoAdapter from .minimax import MiniMaxAdapter @@ -48,6 +49,7 @@ class LLMAdapterFactory: cls.register_adapter(MiMoAdapter()) cls.register_adapter(MiniMaxAdapter()) cls.register_adapter(DoubaoAdapter()) + cls.register_adapter(GrokAdapter()) @classmethod def register_adapter(cls, adapter: BaseAdapter) -> None: @@ -114,6 +116,7 @@ class SmartAdapter(BaseAdapter): ("*gemini*", "gemini"), ("*deepseek*", "deepseek"), ("*minimax*", "minimax"), + ("*grok*", "grok"), ("*gpt*", "openai_responses"), ] _DEFAULT_API_TYPE: ClassVar[str] = "openai" diff --git a/zhenxun/services/ai/llm/adapters/grok.py b/zhenxun/services/ai/llm/adapters/grok.py new file mode 100644 index 00000000..8cf1c015 --- /dev/null +++ b/zhenxun/services/ai/llm/adapters/grok.py @@ -0,0 +1,60 @@ +from typing import Any + +from zhenxun.services.ai.core.models import ModelCapabilities +from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import ( + OpenAIResponsesTextHandler, + ResponsesToolSerializer, +) +from zhenxun.services.ai.llm.adapters.openai import OpenAIResponsesAdapter + + +class GrokToolSerializer(ResponsesToolSerializer): + """Grok 专属工具序列化器""" + + def serialize_server_tools( + self, tools: list[Any], capabilities: ModelCapabilities + ) -> list[dict[str, Any]]: + res = [] + for t in tools: + type_id = getattr(t, "type_id", "unknown") + if type_id not in capabilities.supported_native_tools: + continue + + if type_id == "web_search": + res.append({"type": "web_search"}) + elif type_id == "x_search": + payload = {"type": "x_search"} + if getattr(t, "allowed_x_handles", None): + payload["allowed_x_handles"] = t.allowed_x_handles + if getattr(t, "excluded_x_handles", None): + payload["excluded_x_handles"] = t.excluded_x_handles + res.append(payload) + elif type_id == "code_execution": + res.append({"type": "code_interpreter"}) + elif type_id == "file_search": + res.append({"type": "file_search"}) + return res + + +class GrokTextHandler(OpenAIResponsesTextHandler): + """Grok 文本处理器""" + + def __init__(self, api_type: str = "grok"): + super().__init__(api_type=api_type) + self.serializer = GrokToolSerializer(api_type=api_type) + + +class GrokAdapter(OpenAIResponsesAdapter): + """xAI Grok API 适配器""" + + def __init__(self): + super().__init__() + self.text_handler = GrokTextHandler(api_type=self.api_type) + + @property + def api_type(self) -> str: + return "grok" + + @property + def supported_api_types(self) -> list[str]: + return ["grok"] diff --git a/zhenxun/services/ai/llm/adapters/handlers/openai_handlers.py b/zhenxun/services/ai/llm/adapters/handlers/openai_handlers.py index 3f502f53..3079e9e9 100644 --- a/zhenxun/services/ai/llm/adapters/handlers/openai_handlers.py +++ b/zhenxun/services/ai/llm/adapters/handlers/openai_handlers.py @@ -521,6 +521,9 @@ class ResponsesConfigMapper(OpenAIConfigMapper): class ResponsesMessageConverter(MessageConverter): """针对 OpenAI Responses API 的消息转换器""" + def __init__(self, api_type: str = "openai_responses"): + self.api_type = api_type + async def convert_messages_async( self, messages: list[LLMMessage] ) -> list[dict[str, Any]]: @@ -543,11 +546,14 @@ class ResponsesMessageConverter(MessageConverter): continue content_list: list[dict[str, Any]] = [] + thought_text = "" for part in msg.content: if part is None: continue - if isinstance(part, TextPart): + if isinstance(part, ThoughtPart): + thought_text += part.thought_text + elif isinstance(part, TextPart): c_type = "output_text" if role == "assistant" else "input_text" content_list.append({"type": c_type, "text": part.text}) elif isinstance(part, ImagePart): @@ -573,6 +579,26 @@ class ResponsesMessageConverter(MessageConverter): {"type": "input_image", "image_url": image_src} ) + if role == "assistant" and thought_text: + if self.api_type == "deepseek": + input_items.append( + { + "type": "reasoning", + "content": [ + {"type": "reasoning_text", "text": thought_text} + ], + "summary": [], + } + ) + else: + input_items.append( + { + "type": "reasoning", + "content": [], + "summary": [{"type": "summary_text", "text": thought_text}], + } + ) + if content_list: input_items.append({"role": role, "content": content_list}) @@ -661,6 +687,10 @@ class ResponsesResponseParser(OpenAIResponseParser): ) ) elif item.get("type") == "reasoning": + for r_content in item.get("content", []): + if r_content.get("type") == "reasoning_text": + thought_content += r_content.get("text", "") + for summary_item in item.get("summary", []): if summary_item.get("type") == "summary_text": thought_content += summary_item.get("text", "") @@ -868,7 +898,7 @@ class OpenAIResponsesTextHandler(OpenAITextHandler): def __init__(self, api_type: str = "openai_responses"): super().__init__(api_type=api_type) - self.converter = ResponsesMessageConverter() + self.converter = ResponsesMessageConverter(api_type=api_type) self.serializer = ResponsesToolSerializer(api_type=api_type) self.mapper = ResponsesConfigMapper(api_type=api_type) self.parser = ResponsesResponseParser() diff --git a/zhenxun/services/ai/llm/engine/middlewares.py b/zhenxun/services/ai/llm/engine/middlewares.py index ef8e01c4..98ac89b8 100644 --- a/zhenxun/services/ai/llm/engine/middlewares.py +++ b/zhenxun/services/ai/llm/engine/middlewares.py @@ -301,7 +301,7 @@ class LoggingMiddleware: smart_adapter = cast(SmartAdapter, self.adapter) delegate_adapter = smart_adapter._get_delegate_adapter(self.identity) - sanitizer_req_context = f"{delegate_adapter.api_type}_request" + sanitizer_req_context = delegate_adapter.log_sanitization_context else: sanitizer_req_context = self.adapter.log_sanitization_context diff --git a/zhenxun/services/ai/llm/manager.py b/zhenxun/services/ai/llm/manager.py index 5c6b345c..633d2336 100644 --- a/zhenxun/services/ai/llm/manager.py +++ b/zhenxun/services/ai/llm/manager.py @@ -58,6 +58,7 @@ def get_default_api_base_for_type(api_type: str) -> str | None: "glm": "https://open.bigmodel.cn", "gemini": "https://generativelanguage.googleapis.com", "openrouter": "https://openrouter.ai/api", + "grok": "https://api.x.ai/v1", "smart": None, "openai_responses": None, } diff --git a/zhenxun/services/ai/llm/system/capabilities.py b/zhenxun/services/ai/llm/system/capabilities.py index 215ed500..8fc92d79 100644 --- a/zhenxun/services/ai/llm/system/capabilities.py +++ b/zhenxun/services/ai/llm/system/capabilities.py @@ -9,6 +9,7 @@ from zhenxun.utils.pydantic_compat import model_copy CTX_1_05M = 1_050_000 CTX_1M = 1_000_000 +CTX_500K = 500_000 CTX_400K = 400_000 CTX_256K = 256_000 CTX_200K = 204_800 @@ -122,7 +123,16 @@ CAP_OPENAI_MULTIMODAL = ModelCapabilities( }, reasoning_effort_map={"max": "xhigh", "minimal": "none"}, ) -CAP_DEEPSEEK_V4 = ModelCapabilities( +CAP_GROK = ModelCapabilities( + input_modalities={ModelModality.TEXT, ModelModality.IMAGE}, + output_modalities={ModelModality.TEXT}, + supports_tool_calling=True, + reasoning_mode=ReasoningMode.EFFORT, + reasoning_visibility="visible", + supported_native_tools={"web_search", "x_search", "code_execution", "file_search"}, +) + +CAP_DEEPSEEK_PRO = ModelCapabilities( input_modalities={ModelModality.TEXT}, output_modalities={ModelModality.TEXT}, supports_tool_calling=True, @@ -130,6 +140,14 @@ CAP_DEEPSEEK_V4 = ModelCapabilities( reasoning_mode=ReasoningMode.EFFORT, reasoning_visibility="visible", reasoning_effort_map={"minimal": "low"}, + supported_native_tools={"web_search"}, +) + +CAP_DEEPSEEK_FLASH = model_copy( + CAP_DEEPSEEK_PRO, + update={ + "input_modalities": {ModelModality.TEXT, ModelModality.IMAGE}, + }, ) CAP_MINIMAX_REASONING = ModelCapabilities( input_modalities={ModelModality.TEXT}, @@ -279,7 +297,10 @@ _ROUTING_TABLE: list[tuple[list[str], ModelCapabilities, int]] = [ (["*gemini*image*", "*nano-banana*"], CAP_GEMINI_IMAGE, CTX_128K), (["glm-4.6v*"], CAP_GLM_MULTIMODAL, CTX_128K), (["glm-4.7-flash*"], STANDARD_TEXT_TOOL_CAPABILITIES, CTX_128K), - (["deepseek-v4-pro*", "deepseek-v4-flash*"], CAP_DEEPSEEK_V4, CTX_1M), + (["*grok-4.3*", "*grok-4.20*"], CAP_GROK, CTX_1M), + (["*grok*"], CAP_GROK, CTX_500K), + (["*deepseek*pro*"], CAP_DEEPSEEK_PRO, CTX_1M), + (["*deepseek*flash*"], CAP_DEEPSEEK_FLASH, CTX_1M), (["glm-4-long*"], STANDARD_TEXT_TOOL_CAPABILITIES, CTX_1M), (["*MiniMax-M3*"], CAP_MINIMAX_MULTIMODAL, CTX_1M), (["mimo-v2.5-pro*", "mimo-v2-pro*", "mimo-v2-flash*"], CAP_MIMO_TEXT, CTX_1M), diff --git a/zhenxun/services/ai/sandbox/addons/mcp_proxy.py b/zhenxun/services/ai/sandbox/addons/mcp_proxy.py index 16dac911..62385c00 100644 --- a/zhenxun/services/ai/sandbox/addons/mcp_proxy.py +++ b/zhenxun/services/ai/sandbox/addons/mcp_proxy.py @@ -3,6 +3,7 @@ from contextlib import asynccontextmanager import json from typing import Any +import anyio from anyio import create_memory_object_stream, create_task_group from mcp.shared.message import SessionMessage from mcp.types import JSONRPCMessage @@ -61,16 +62,25 @@ class UniversalMcpExtension(BaseMcpProxyExtension): if not line.strip(): continue try: - msg = model_validate( + msg_obj = model_validate( JSONRPCMessage, json.loads(line) ) - await read_prod.send(SessionMessage(message=msg)) + await read_prod.send( + SessionMessage(message=msg_obj) + ) except Exception as exc: await read_prod.send(exc) - except Exception: + except anyio.ClosedResourceError: pass + except BaseException as e: + logger.debug( + f"🔇 [MCP Universal] 读流异常: {type(e).__name__}: {e}" + ) finally: - await read_prod.aclose() + try: + await read_prod.aclose() + except Exception: + pass async def stream_writer(): try: @@ -82,8 +92,12 @@ class UniversalMcpExtension(BaseMcpProxyExtension): + b"\n" ) await process_stream.write(data) - except Exception: + except anyio.ClosedResourceError: pass + except BaseException as e: + logger.debug( + f"🔇 [MCP Universal] 写流异常: {type(e).__name__}: {e}" + ) async with create_task_group() as tg: tg.start_soon(stream_reader) diff --git a/zhenxun/services/ai/tools/providers/builtin/native.py b/zhenxun/services/ai/tools/providers/builtin/native.py index 4fa9bcd0..5ac71389 100644 --- a/zhenxun/services/ai/tools/providers/builtin/native.py +++ b/zhenxun/services/ai/tools/providers/builtin/native.py @@ -30,6 +30,23 @@ class CodeExecutionTool(ServerSideTool): self.timeout = timeout +class XSearchTool(ServerSideTool): + """原生 X (推特) 搜索工具 (Grok独占)""" + + type_id = "x_search" + + def __init__( + self, + allowed_x_handles: list[str] | None = None, + excluded_x_handles: list[str] | None = None, + ): + super().__init__( + name="x_search", description="在 X (Twitter) 上搜索推文、用户资料和时间线。" + ) + self.allowed_x_handles = allowed_x_handles + self.excluded_x_handles = excluded_x_handles + + class ComputerUseTool(ServerSideTool): """原生的桌面环境控制工具""" @@ -70,10 +87,7 @@ class UrlContextTool(ServerSideTool): class Native: """ - 云端原生工具命名空间工厂 (Namespace Factory)。 - - 为开发者提供统一的云端内置工具调用入口,享受顶级 IDE 补全体验。 - 此类工具仅会向大模型提供描述,物理执行发生在各大模型厂商的服务端。 + 云端原生工具命名空间工厂 """ @classmethod @@ -89,6 +103,17 @@ class Native: """ return WebSearchTool(name, description, dynamic_threshold, domain_filters) + @classmethod + def x_search( + cls, + allowed_x_handles: list[str] | None = None, + excluded_x_handles: list[str] | None = None, + ) -> XSearchTool: + """ + 原生 X (Twitter) 检索工具 (Grok 独占)。 + """ + return XSearchTool(allowed_x_handles, excluded_x_handles) + @classmethod def code_execution( cls, name: str = "code_execution", timeout: int | None = None diff --git a/zhenxun/services/ai/tools/providers/mcp/provider.py b/zhenxun/services/ai/tools/providers/mcp/provider.py index ec01ad83..72773f47 100644 --- a/zhenxun/services/ai/tools/providers/mcp/provider.py +++ b/zhenxun/services/ai/tools/providers/mcp/provider.py @@ -64,8 +64,10 @@ class MCPServerConfig(BaseModel): values["transport"] = values.pop("type") transport_val = values.get("transport") - if isinstance(transport_val, str) and transport_val.lower() == "streamablehttp": - values["transport"] = "streamable-http" + if isinstance(transport_val, str): + t_lower = transport_val.lower() + if t_lower in ("streamablehttp", "http"): + values["transport"] = "streamable-http" headers = values.get("headers") if isinstance(headers, dict): diff --git a/zhenxun/services/ai/tools/providers/mcp/toolkit.py b/zhenxun/services/ai/tools/providers/mcp/toolkit.py index 36205a1c..2091d4ce 100644 --- a/zhenxun/services/ai/tools/providers/mcp/toolkit.py +++ b/zhenxun/services/ai/tools/providers/mcp/toolkit.py @@ -98,8 +98,15 @@ async def filtered_stdio_client( await filtered_send.send(item) except anyio.ClosedResourceError: pass + except BaseException as e: + logger.debug( + f"🔇 [MCP Stdout Filter] 捕获到底层流异常: {type(e).__name__}: {e}" + ) finally: - await filtered_send.aclose() + try: + await filtered_send.aclose() + except Exception: + pass async with anyio.create_task_group() as tg: tg.start_soon(_forward_stdout) @@ -540,7 +547,12 @@ class MCPToolkit(BaseToolkit): await self._stop_event.wait() return - except Exception as e: + except BaseException as e: + if isinstance( + e, asyncio.CancelledError | KeyboardInterrupt | SystemExit + ): + raise e + from zhenxun.services.ai.core.exceptions import SandboxFatalError if isinstance(e, SandboxFatalError): @@ -548,23 +560,30 @@ class MCPToolkit(BaseToolkit): if attempt < max_attempts: logger.warning( - f"⚠️ [{self.server_name}] 进程启动或运行异常崩溃," + f"⚠️ [{self.server_name}] 进程启动或" + f"底层流异常崩溃 ({type(e).__name__})," "疑似环境损坏。触发自愈机制 (准备重试)..." ) - self._init_exception = e + self._init_exception = ( + e if isinstance(e, Exception) else Exception(str(e)) + ) self._shared_session = None if not self._is_initialized: self._is_initialized = False continue + if not isinstance(e, Exception): + raise Exception(f"底层致命异常: {e}") from e raise e - except Exception as e: - self._init_exception = e + except BaseException as e: + if isinstance(e, asyncio.CancelledError | KeyboardInterrupt | SystemExit): + raise e + self._init_exception = e if isinstance(e, Exception) else Exception(str(e)) logger.error( f"MCP 服务器 '{self.server_name}' " - f"初始化连接失败(模式: {self.transport})。" - f"错误原因: {e}" + f"初始化连接或运行期间发生底层故障(模式: {self.transport})。" + f"错误类型: {type(e).__name__}, 原因: {e}" ) if "Connection closed" in str(e): logger.error(