♻️ refactor(core): 重构 AI 能力与定时任务调度系统 (#2148)

* ♻️ refactor(core): 重构 AI 能力与定时任务调度系统

- 【AI 能力与工具】重构 Capability 注册与管理机制,引入 CapabilityManager 统一管理
- 移除全局能力注册表,改用声明式装饰器 `@capability` 进行解耦注册
- 重构工具解析器链,使用统一的 BaseToolResolver 代替原有的多个特定解析器
- 增强工具查询过滤,支持通配符匹配、工具箱过滤和排除标签
- 【定时任务调度】重构定时任务管理器,引入 SchedulerRegistry 统一管理任务元数据
- 引入 JobConfig 聚合定时任务配置,支持用户维度的定时任务调度
- 重构执行分发器,支持并发限制、串行间隔和随机延迟打散
- 【运行上下文】引入 ScheduledDeps 以支持后台和定时任务环境下的依赖注入
- 优化 RunContext,支持从定时任务上下文快速构造,并提供 emit 辅助方法
- 【日志与监控】引入 AILoggerProxy,实现 AI 各模块的专属日志输出
- 将各模块的全局 logger 替换为对应的模块专属日志代理
- 【其他优化】修复 Pydantic V1 兼容层中 model_validator 的装饰器兼容性问题
- 在非交互式环境(如定时任务)中自动隐藏 HITL 交互工具以节省 Token

* ♻️ refactor(core): 优化内部导入路径并提升 Pydantic 兼容性

- 【重构】将 `services/ai` 模块内的绝对导入重构为相对导入,优化包结构
- 【重构】移除不必要的 `if TYPE_CHECKING` 保护,通过 `from __future__ import annotations` 直接导入类型
- 【清理】清理 `core/messages/types.py` 中未使用的 `AssistantContentUnion` 等联合类型定义
- 【优化】在 `utils/pydantic_compat.py` 中新增 `model_rebuild` 兼容函数,统一 Pydantic V1/V2 的模型重建逻辑
- 【优化】将部分函数内部的延迟导入提升至模块顶部,规范代码结构

* ♻️ refactor(imports): 优化导入路径为相对导入并清理冗余导入

- 【重构】将 AI 服务相关模块中的绝对导入路径修改为相对导入,提升模块内聚性与可移植性
- 【清理】移除多处函数内部或类方法中未使用的冗余导入,避免循环引用和资源浪费
- 【格式化】微调部分工具装饰器和返回语句的格式与尾随逗号

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio
2026-07-10 09:14:06 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent 0b32d69c9c
commit 922d092650
132 changed files with 2372 additions and 1972 deletions
+2 -1
View File
@@ -13,7 +13,7 @@ from .glm import GLMAdapter
from .jina import JinaAdapter
from .mimo import MiMoAdapter
from .minimax import MiniMaxAdapter
from .openai import OpenAIAdapter, OpenAICompatAdapter
from .openai import OpenAIAdapter, OpenAICompatAdapter, OpenAIResponsesAdapter
from .openrouter import OpenRouterAdapter
LLMAdapterFactory.initialize()
@@ -30,6 +30,7 @@ __all__ = [
"MiniMaxAdapter",
"OpenAIAdapter",
"OpenAICompatAdapter",
"OpenAIResponsesAdapter",
"OpenRouterAdapter",
"RequestData",
"ResponseData",
+3 -4
View File
@@ -47,11 +47,11 @@ from zhenxun.services.ai.core.messages import (
ThoughtPart,
)
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.log_sanitizer import sanitize_for_logging
if TYPE_CHECKING:
from zhenxun.services.ai.llm.adapters.handlers.base import (
from .handlers.base import (
BaseAudioHandler,
BaseEmbeddingHandler,
BaseImageHandler,
@@ -496,8 +496,7 @@ class BaseAdapter(ABC):
async def parse_speech_response(
self, identity: ModelIdentity, raw_response: httpx.Response
) -> AudioResponse:
"""解析语音响应并委派给 `audio_handler`。
注意传入的是 httpx.Response 的 raw 对象"""
"""解析语音响应并委派给 `audio_handler`"""
if self.audio_handler:
return await self.audio_handler.parse_speech_response(
adapter=self, identity=identity, raw_response=raw_response
+3 -17
View File
@@ -6,12 +6,13 @@ from zhenxun.services.ai.core.models import (
ModelIdentity,
)
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .handlers.openai_handlers import (
OpenAIConfigMapper,
OpenAITextHandler,
OpenAIToolSerializer,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter
from .openai import OpenAICompatAdapter
class DeepSeekToolSerializer(OpenAIToolSerializer):
@@ -84,21 +85,6 @@ class DeepSeekConfigMapper(OpenAIConfigMapper):
if isinstance(rf, dict) and rf.get("type") == "json_schema":
params["response_format"] = {"type": "json_object"}
if config.common.reasoning_effort:
effort = str(config.common.reasoning_effort).lower()
if effort == "none":
params["thinking"] = {"type": "disabled"}
else:
params["thinking"] = {"type": "enabled"}
elif (
hasattr(config, "deepseek_options")
and config.deepseek_options.thinking is not None
):
if config.deepseek_options.thinking is True:
params["thinking"] = {"type": "enabled"}
elif config.deepseek_options.thinking is False:
params["thinking"] = {"type": "disabled"}
return params
+2 -1
View File
@@ -34,10 +34,11 @@ class LLMAdapterFactory:
from .jina import JinaAdapter
from .mimo import MiMoAdapter
from .minimax import MiniMaxAdapter
from .openai import OpenAIAdapter
from .openai import OpenAIAdapter, OpenAIResponsesAdapter
from .openrouter import OpenRouterAdapter
cls.register_adapter(OpenAIAdapter())
cls.register_adapter(OpenAIResponsesAdapter())
cls.register_adapter(OpenRouterAdapter())
cls.register_adapter(DeepSeekAdapter())
cls.register_adapter(JinaAdapter())
+4 -3
View File
@@ -1,13 +1,14 @@
from zhenxun.services.ai.core.messages import RerankRequest
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .base import BaseAdapter, RequestData
from .handlers.openai_handlers import (
OpenAIConfigMapper,
OpenAIEmbeddingHandler,
OpenAIRerankHandler,
OpenAITextHandler,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter
from .openai import OpenAICompatAdapter
class GLMRerankHandler(OpenAIRerankHandler):
@@ -56,7 +56,9 @@ from zhenxun.services.ai.llm.adapters.base import (
ResponseData,
process_image_data,
)
from zhenxun.services.ai.llm.adapters.handlers.base import (
from zhenxun.services.ai.utils.logger import log_llm as logger
from .base import (
BaseAudioHandler,
BaseEmbeddingHandler,
BaseImageHandler,
@@ -66,7 +68,6 @@ from zhenxun.services.ai.llm.adapters.handlers.base import (
ResponseParser,
ToolSerializer,
)
from zhenxun.services.log import logger
class GeminiConfigMapper(ConfigMapper):
@@ -121,22 +122,19 @@ class GeminiConfigMapper(ConfigMapper):
fc_config["allowedFunctionNames"] = user_funcs
params["toolConfig"] = {"functionCallingConfig": fc_config}
has_effort = bool(
config.common.reasoning_effort
and str(config.common.reasoning_effort).lower() != "none"
)
if (
has_effort
and capabilities
and capabilities.reasoning_mode == ReasoningMode.LEVEL
):
thinking_config = params.setdefault("thinkingConfig", {})
if config.common.reasoning_effort and capabilities:
effort = str(config.common.reasoning_effort).lower()
if capabilities.reasoning_effort_map:
effort = capabilities.reasoning_effort_map.get(effort, effort)
thinking_config["thinkingLevel"] = effort
if capabilities.reasoning_mode == ReasoningMode.LEVEL and effort != "none":
thinking_config = params.setdefault("thinkingConfig", {})
thinking_config["thinkingLevel"] = effort
elif (
capabilities.reasoning_mode == ReasoningMode.BUDGET and effort == "none"
):
thinking_config = params.setdefault("thinkingConfig", {})
thinking_config["thinkingBudget"] = 0
if config.gemini_options.include_thoughts is not None:
thinking_config = params.setdefault("thinkingConfig", {})
@@ -829,10 +827,6 @@ class GeminiEmbeddingHandler(BaseEmbeddingHandler):
url = f"{base_url}/v1beta/{api_model_name}:batchEmbedContents"
headers = adapter.get_base_headers(api_key)
from zhenxun.services.ai.llm.adapters.handlers.gemini_handlers import (
GeminiMessageConverter,
)
converter = GeminiMessageConverter()
requests_payload = []
@@ -16,13 +16,13 @@ from zhenxun.services.ai.core.messages import (
)
from zhenxun.services.ai.core.models import (
ModelCapabilities,
ModelDetail,
ModelIdentity,
)
from zhenxun.services.ai.core.options import GenerationConfig, TTSConfig
from zhenxun.services.ai.core.options import TTSConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.base import BaseAudioHandler
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .base import BaseAudioHandler
from .openai_handlers import (
OpenAIConfigMapper,
OpenAIMessageConverter,
OpenAITextHandler,
@@ -53,35 +53,6 @@ class MiMoToolSerializer(OpenAIToolSerializer):
return res
class MiMoConfigMapper(OpenAIConfigMapper):
"""MiMo 配置映射器,处理深度思考参数差异"""
def map_config(
self,
config: GenerationConfig,
model_detail: ModelDetail | None = None,
capabilities: ModelCapabilities | None = None,
) -> dict[str, Any]:
params = super().map_config(config, model_detail, capabilities)
if config.common.reasoning_effort:
effort = str(config.common.reasoning_effort).lower()
if effort == "none":
params["thinking"] = {"type": "disabled"}
else:
params["thinking"] = {"type": "enabled"}
elif (
hasattr(config, "deepseek_options")
and config.deepseek_options.thinking is not None
):
if config.deepseek_options.thinking is True:
params["thinking"] = {"type": "enabled"}
elif config.deepseek_options.thinking is False:
params["thinking"] = {"type": "disabled"}
return params
class MiMoMessageConverter(OpenAIMessageConverter):
"""MiMo 消息转换器,拦截处理特有的音视频多模态结构"""
@@ -141,7 +112,7 @@ class MiMoTextHandler(OpenAITextHandler):
super().__init__(api_type=api_type)
self.converter = MiMoMessageConverter(api_type=api_type)
self.serializer = MiMoToolSerializer(api_type=api_type)
self.mapper = MiMoConfigMapper(api_type=api_type)
self.mapper = OpenAIConfigMapper(api_type=api_type)
class MiMoAudioHandler(BaseAudioHandler):
@@ -53,7 +53,9 @@ from zhenxun.services.ai.llm.adapters.base import (
ResponseData,
process_image_data,
)
from zhenxun.services.ai.llm.adapters.handlers.base import (
from zhenxun.services.ai.utils.logger import log_llm as logger
from .base import (
BaseAudioHandler,
BaseEmbeddingHandler,
BaseImageHandler,
@@ -64,7 +66,6 @@ from zhenxun.services.ai.llm.adapters.handlers.base import (
ResponseParser,
ToolSerializer,
)
from zhenxun.services.log import logger
class OpenAIConfigMapper(ConfigMapper):
@@ -114,7 +115,13 @@ class OpenAIConfigMapper(ConfigMapper):
if capabilities and capabilities.reasoning_effort_map:
effort = capabilities.reasoning_effort_map.get(effort, effort)
if effort != "none":
if (
effort == "none"
and capabilities
and capabilities.supports_thinking_toggle
):
params["thinking"] = {"type": "disabled"}
else:
params["reasoning_effort"] = effort
if isinstance(config.output.response_format, dict):
@@ -1165,45 +1172,3 @@ class OpenAIAudioHandler(BaseAudioHandler):
usage=UsageInfo(),
model_name=identity.model_name,
)
class CompositeOpenAITextHandler(BaseTextHandler):
"""
OpenAI 复合文本对话处理器 (Composite Pattern)。
内部包装标准协议与 responses 协议 Handler,根据模型配置在请求时动态路由
"""
def __init__(self, api_type: str = "openai"):
self.api_type = api_type
self._standard_handler = OpenAITextHandler(api_type=api_type)
self._responses_handler = OpenAIResponsesTextHandler(
api_type="openai_responses"
)
def _get_active_handler(self, identity: ModelIdentity) -> BaseTextHandler:
current_api_type = identity.api_type
if current_api_type == "openai_responses":
return self._responses_handler
return self._standard_handler
async def prepare_text_request(
self,
adapter: BaseAdapter,
identity: ModelIdentity,
api_key: str,
request: ChatRequest,
) -> RequestData:
handler = self._get_active_handler(identity)
return await handler.prepare_text_request(adapter, identity, api_key, request)
def parse_text_response(
self,
adapter: BaseAdapter,
identity: ModelIdentity,
response_json: dict[str, Any],
is_advanced: bool = False,
) -> ResponseData:
handler = self._get_active_handler(identity)
return handler.parse_text_response(
adapter, identity, response_json, is_advanced
)
+5 -4
View File
@@ -1,12 +1,14 @@
from zhenxun.services.ai.core.messages import EmbeddingRequest
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.core.options import LLMEmbeddingConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from zhenxun.services.ai.utils.logger import log_llm as logger
from .base import BaseAdapter, RequestData
from .handlers.openai_handlers import (
OpenAIEmbeddingHandler,
OpenAIRerankHandler,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter
from .openai import OpenAICompatAdapter
class JinaEmbeddingHandler(OpenAIEmbeddingHandler):
@@ -36,7 +38,6 @@ class JinaEmbeddingHandler(OpenAIEmbeddingHandler):
TextPart,
VideoPart,
)
from zhenxun.services.log import logger
for payload in batch.payloads:
jina_content = []
+4 -3
View File
@@ -1,12 +1,13 @@
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.handlers.mimo_handlers import (
from .handlers.mimo_handlers import (
MiMoAudioHandler,
MiMoTextHandler,
)
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .handlers.openai_handlers import (
OpenAIImageHandler,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter
from .openai import OpenAICompatAdapter
class MiMoAdapter(OpenAICompatAdapter):
+2 -2
View File
@@ -19,9 +19,9 @@ from zhenxun.services.ai.core.models import (
ModelIdentity,
)
from zhenxun.services.ai.core.options import GenerationConfig, TTSConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.base import BaseAudioHandler
from .base import BaseAdapter, RequestData
from .handlers.base import BaseAudioHandler
from .handlers.openai_handlers import (
OpenAIConfigMapper,
OpenAIMessageConverter,
+29 -14
View File
@@ -15,11 +15,12 @@ from .base import (
RequestData,
)
from .handlers.openai_handlers import (
CompositeOpenAITextHandler,
OpenAIAudioHandler,
OpenAIEmbeddingHandler,
OpenAIImageHandler,
OpenAIRerankHandler,
OpenAIResponsesTextHandler,
OpenAITextHandler,
)
@@ -83,12 +84,12 @@ class OpenAICompatAdapter(BaseAdapter):
class OpenAIAdapter(OpenAICompatAdapter):
"""OpenAI 系列适配器,统一装配文本/图像/嵌入/重排处理链。"""
"""标准 OpenAI 系列适配器,统一装配文本/图像/嵌入/重排处理链。"""
def __init__(self):
"""初始化并挂载复合文本处理器与通用多模态处理器。"""
"""初始化并挂载标准文本处理器与通用多模态处理器。"""
super().__init__()
self.text_handler = CompositeOpenAITextHandler(api_type=self.api_type)
self.text_handler = OpenAITextHandler(api_type=self.api_type)
self.image_handler = OpenAIImageHandler()
self.embedding_handler = OpenAIEmbeddingHandler()
self.rerank_handler = OpenAIRerankHandler()
@@ -102,21 +103,35 @@ class OpenAIAdapter(OpenAICompatAdapter):
@property
def supported_api_types(self) -> list[str]:
"""支持的 API 类型及别名。"""
return [
"openai",
"openai_responses",
]
return ["openai"]
def get_chat_endpoint(self, identity: ModelIdentity) -> str:
"""返回聊天完成端点"""
current_api_type = identity.api_type
if current_api_type == "openai_responses":
return "/v1/responses"
if current_api_type == "doubao":
return "/api/v3/chat/completions"
return "/v1/chat/completions"
def get_embedding_endpoint(self, identity: ModelIdentity) -> str:
"""返回嵌入端点。"""
return "/v1/embeddings"
class OpenAIResponsesAdapter(OpenAICompatAdapter):
"""OpenAI v1/responses 协议专用适配器"""
def __init__(self):
super().__init__()
self.text_handler = OpenAIResponsesTextHandler(api_type=self.api_type)
self.image_handler = OpenAIImageHandler()
self.embedding_handler = OpenAIEmbeddingHandler()
self.rerank_handler = OpenAIRerankHandler()
self.audio_handler = OpenAIAudioHandler()
@property
def api_type(self) -> str:
return "openai_responses"
@property
def supported_api_types(self) -> list[str]:
return ["openai_responses"]
def get_chat_endpoint(self, identity: ModelIdentity) -> str:
return "/v1/responses"
@@ -10,18 +10,19 @@ from zhenxun.services.ai.core.messages import (
ThoughtPart,
)
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.base import (
from .base import (
BaseAdapter,
RequestData,
ResponseData,
process_image_data,
)
from zhenxun.services.ai.llm.adapters.handlers.base import BaseImageHandler
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
CompositeOpenAITextHandler,
from .handlers.base import BaseImageHandler
from .handlers.openai_handlers import (
OpenAIMessageConverter,
OpenAITextHandler,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAIAdapter
from .openai import OpenAIAdapter
class OpenRouterMessageConverter(OpenAIMessageConverter):
@@ -60,12 +61,12 @@ class OpenRouterMessageConverter(OpenAIMessageConverter):
return openai_messages
class OpenRouterTextHandler(CompositeOpenAITextHandler):
class OpenRouterTextHandler(OpenAITextHandler):
"""OpenRouter 专有文本处理器,挂载专有 Converter"""
def __init__(self, api_type: str = "openrouter"):
super().__init__(api_type=api_type)
self._standard_handler.converter = OpenRouterMessageConverter(api_type=api_type)
self.converter = OpenRouterMessageConverter(api_type=api_type)
class OpenRouterImageHandler(BaseImageHandler):