♻️ 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
-3
View File
@@ -1,7 +1,6 @@
from zhenxun.services.ai.core.models import CancellationToken
from .blackboard import BlackboardManager
from .capabilities import GLOBAL_CAPABILITIES, register_global_capability
from .context import (
NoneBotDeps,
RunContext,
@@ -19,7 +18,6 @@ from .session import session_manager
from .ui import UIController
__all__ = [
"GLOBAL_CAPABILITIES",
"AgentRunResult",
"AgentTask",
"BlackboardManager",
@@ -33,6 +31,5 @@ __all__ = [
"StreamedRunResult",
"UIController",
"get_current_run_context",
"register_global_capability",
"session_manager",
]
-40
View File
@@ -1,40 +0,0 @@
from __future__ import annotations
from collections import defaultdict
from zhenxun.services.ai.capabilities import (
AbstractCapability,
)
from zhenxun.services.log import logger
from zhenxun.utils.utils import infer_plugin_namespace
GLOBAL_CAPABILITIES: dict[str, list[AbstractCapability]] = defaultdict(list)
from zhenxun.services.ai.capabilities.builtin import (
BillingCapability,
GlobalCycleLimitCapability,
PermissionCapability,
ReflexionCapability,
StuckDetectionCapability,
ToolRetryAndReflectionCapability,
)
for _cap in [
GlobalCycleLimitCapability(),
StuckDetectionCapability(),
PermissionCapability(),
BillingCapability(),
ToolRetryAndReflectionCapability(),
ReflexionCapability(),
]:
GLOBAL_CAPABILITIES["global"].append(_cap)
def register_global_capability(
capability: AbstractCapability, scope: str | None = None
) -> None:
ns = scope if scope is not None else infer_plugin_namespace()
GLOBAL_CAPABILITIES[ns].append(capability)
logger.debug(
f"已注册全局 Capability: {capability.__class__.__name__} -> Namespace: {ns}"
)
+78 -36
View File
@@ -4,17 +4,31 @@ from contextlib import contextmanager
from contextvars import ContextVar
import dataclasses
import inspect
from typing import Any, Generic, cast, get_origin
from typing import TYPE_CHECKING, Any, Generic, cast, get_origin
from typing_extensions import TypeVar
import uuid
from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot.matcher import Matcher, current_bot, current_event, current_matcher
from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.capabilities.base import AbstractCapability
from zhenxun.services.ai.core.engine.append_only import AppendOnlyContextManager
from zhenxun.services.ai.core.messages import AgentEvent, AgentMessage
from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.core.protocols.tool import ToolExecutable
from zhenxun.services.ai.core.stream_events import AgentStreamEvent, EventBus
from zhenxun.services.ai.utils import ContextUtils
from zhenxun.services.ai.utils.scope import ScopeSelector
from zhenxun.services.scheduler.types import ScheduleContext
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.utils import infer_plugin_namespace
from .blackboard import BlackboardManager
if TYPE_CHECKING:
from zhenxun.services.ai.context.memory.facades import AgentSessionFacade
AgentDepsT = TypeVar("AgentDepsT", default=Any)
"""泛型类型变量:外部环境依赖对象 (Agent Dependencies)。"""
ProviderFunc = Callable[["RunContext"], Any | Awaitable[Any]]
@@ -40,19 +54,47 @@ class NoneBotDeps(BaseModel):
@classmethod
def get_current(cls) -> "NoneBotDeps | None":
"""利用 NoneBot 原生魔法,基于 ContextVars 隐式提取当前执行上下文"""
try:
from nonebot.matcher import current_bot, current_event, current_matcher
bot = current_bot.get(None)
event = current_event.get(None)
matcher = current_matcher.get(None)
if bot or event:
return cls(bot=bot, event=event, matcher=matcher)
bot = current_bot.get(None)
event = current_event.get(None)
matcher = current_matcher.get(None)
if bot or event:
return cls(bot=bot, event=event, matcher=matcher)
except Exception:
pass
return None
class ScheduledDeps(BaseModel):
"""后台/定时任务环境下的标准依赖容器。"""
model_config = ConfigDict(arbitrary_types_allowed=True)
bot: Bot | None = Field(default=None)
"""机器人实例"""
group_id: str | None = Field(default=None)
"""群组/频道 ID"""
user_id: str | None = Field(default=None)
"""用户 ID"""
schedule_id: int | str | None = Field(default=None)
"""定时任务/调度任务 ID"""
platform: str | None = Field(default=None)
"""适配器平台标识"""
@classmethod
def from_schedule_context(
cls, bot: Bot, context: ScheduleContext
) -> "ScheduledDeps":
"""从调度器上下文中快速构造并提取依赖"""
return cls(
bot=bot,
group_id=getattr(context, "group_id", None),
user_id=getattr(context, "user_id", None),
schedule_id=getattr(context, "schedule_id", None),
platform=getattr(context, "platform_scope", None)
or PlatformUtils.get_platform_scope(bot),
)
@dataclasses.dataclass
class SessionContext(Generic[AgentDepsT]):
"""
@@ -66,9 +108,7 @@ class SessionContext(Generic[AgentDepsT]):
"""强类型的外部依赖注入对象(如 Bot, Event),供跨工具共享。"""
shared_state: dict[str, Any] = dataclasses.field(default_factory=dict)
"""共享状态字典:全局引用穿透,用于主智能体与嵌套子智能体之间的数据通信。"""
auth_tokens: dict[str, str] = dataclasses.field(default_factory=dict)
"""授权凭证字典:保存用户针对各 Provider 的 OAuth 或 API Token。"""
blackboard: Any | None = None
blackboard: BlackboardManager | None = None
"""结构化黑板管理器,作为共享状态的高级替代方案,提供并发锁和强类型校验。"""
namespace: str = "global"
"""触发事件的插件命名空间"""
@@ -76,7 +116,7 @@ class SessionContext(Generic[AgentDepsT]):
"""用于大模型前缀缓存命中优化的追加写入管理器。"""
@property
def memory(self) -> Any:
def memory(self) -> "AgentSessionFacade":
"""
获取当前会话的持久化记忆访问门面 (AgentSessionFacade)。
提供极简的 history 和 slots 操作 API。
@@ -84,7 +124,6 @@ class SessionContext(Generic[AgentDepsT]):
from zhenxun.services.ai.context.memory.facades import AgentSessionFacade
from zhenxun.services.ai.context.memory.manager import memory_manager
from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.utils.scope import ScopeSelector
user_id = ContextUtils.extract_user_id(self.deps)
group_id = ContextUtils.extract_group_id(self.deps)
@@ -127,9 +166,9 @@ class AgentRunContext(Generic[AgentDepsT]):
"""子智能体委派深度标记,用于防范无限递归嵌套。"""
tool_retries: dict[str, int] = dataclasses.field(default_factory=dict)
"""记录当前轮次内各个工具的累积失败重试次数,用于系统熔断。"""
cancellation_token: Any | None = None
cancellation_token: CancellationToken | None = None
"""全局级联取消令牌,用于跨 Agent 的协程挂起中断。"""
event_bus: Any | None = None
event_bus: EventBus | None = None
"""底层的事件流发射器 (EventBus),由执行引擎在运行时挂载。"""
dynamic_prompts: dict[str, str] = dataclasses.field(default_factory=dict)
@@ -146,6 +185,11 @@ class AgentRunContext(Generic[AgentDepsT]):
"""向当前运行上下文中安全追加业务事件"""
self.messages.append(event)
async def emit(self, event: AgentStreamEvent) -> None:
"""安全地向事件总线发射流式事件(内置判空逻辑,消解业务层的样板代码)"""
if self.event_bus:
await self.event_bus.emit(event)
@dataclasses.dataclass
class ToolCallContext(Generic[AgentDepsT]):
@@ -162,7 +206,7 @@ class ToolCallContext(Generic[AgentDepsT]):
"""本次调用的工具名称。"""
retry_count: int = 0
"""当前工具调用的重试序号 (第几次重试)。"""
current_tool: Any | None = None
current_tool: ToolExecutable | None = None
"""当前工具的可执行实例 (ToolExecutable)。"""
@@ -188,7 +232,7 @@ class RunContext(Generic[AgentDepsT]):
upstream_results: dict[str, Any] = dataclasses.field(default_factory=dict)
"""前置节点产出字典:标准化的数据流载荷契约,键为 Agent Name,值为输出内容"""
capabilities: list[Any] = dataclasses.field(default_factory=list)
capabilities: list[AbstractCapability] = dataclasses.field(default_factory=list)
"""当前上下文绑定的拦截器 (Capabilities) 链,用于生命周期拦截"""
deps: AgentDepsT = dataclasses.field(default=cast(AgentDepsT, None))
@@ -211,6 +255,14 @@ class RunContext(Generic[AgentDepsT]):
)
"""标记 session_id 是否为框架隐式生成的。"""
@classmethod
def from_schedule(
cls, bot: Bot, context: ScheduleContext, **kwargs
) -> "RunContext[ScheduledDeps]":
"""极简构造语法糖:从定时调度任务上下文中直接生成 RunContext"""
deps = ScheduledDeps.from_schedule_context(bot, context)
return cast("RunContext[ScheduledDeps]", cls(deps=cast(Any, deps), **kwargs))
def get_bot(self) -> Bot | None:
"""强类型安全地提取 Bot 实例"""
if not self.deps:
@@ -256,21 +308,15 @@ class RunContext(Generic[AgentDepsT]):
bot = self.get_bot()
event = self.get_event()
if bot and event:
if bot:
meta = ContextUtils.generate_session_meta(
bot, event, scope_builder=Isolation.AGENT_USER()
bot=bot,
event=event,
deps=self.deps,
scope_builder=Isolation.AGENT_USER(),
)
self.session_id = meta.session_id
self._is_auto_session_id = True
else:
uid = self.get_user_id()
gid = self.get_group_id()
if uid and gid:
self.session_id = f"auto_{gid}_{uid}"
self._is_auto_session_id = True
elif uid:
self.session_id = f"auto_private_{uid}"
self._is_auto_session_id = True
ns = "global"
if self.deps:
@@ -286,9 +332,6 @@ class RunContext(Generic[AgentDepsT]):
shared_state=self.shared_state,
namespace=ns,
)
from zhenxun.services.ai.core.engine.append_only import (
AppendOnlyContextManager,
)
self.session.append_only_manager = AppendOnlyContextManager()
@@ -351,8 +394,6 @@ class RunContext(Generic[AgentDepsT]):
new_ctx.run.messages = []
new_ctx.capabilities = []
import uuid
base_sid = self.session_id or "default"
new_ctx.session_id = f"{base_sid}/sub_{member_name}_{uuid.uuid4().hex[:6]}"
new_ctx.session.session_id = new_ctx.session_id
@@ -403,6 +444,7 @@ __all__ = [
"AgentRunContext",
"NoneBotDeps",
"RunContext",
"ScheduledDeps",
"SessionContext",
"ToolCallContext",
"ToolsPrepareFunc",
+18 -1
View File
@@ -9,9 +9,9 @@ from nonebot.utils import is_coroutine_callable
from nonebot_plugin_session import EventSession, extract_session
from zhenxun.services.ai.context.memory.facades import AgentSessionFacade
from zhenxun.services.ai.run.blackboard import BlackboardManager
from zhenxun.utils.utils import infer_plugin_namespace
from .blackboard import BlackboardManager
from .context import ProviderFunc, RunContext, _is_run_context_type
from .hitl import HITLController
from .ui import UIController
@@ -105,6 +105,10 @@ class Inject:
"""
return _UpstreamResultMarker(step_name)
CurrentBlackboardState = Annotated[Any, Hidden(), _InjectMarker("blackboard_state")]
BlackboardState = CurrentBlackboardState
"""自动注入:当前挂载的强类型黑板底层 Pydantic 实例(需配合 Annotated 使用)"""
UserId = CurrentUserId
"""自动注入:触发当前任务的用户 ID"""
@@ -366,6 +370,19 @@ def _resolve_blackboard(ctx: RunContext):
Inject.register_provider("blackboard", _resolve_blackboard, scope="global")
def _resolve_blackboard_state(ctx: RunContext):
bb = ctx.session.blackboard
if bb is None:
raise ValueError(
"Inject.BlackboardState 注入失败:"
"当前会话上下文中未挂载 BlackboardManager 实例。"
)
return bb._state
Inject.register_provider("blackboard_state", _resolve_blackboard_state, scope="global")
def _resolve_session(ctx):
bot = ctx.get_bot()
event = ctx.get_event()
+8 -6
View File
@@ -1,17 +1,17 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
import time
from typing import TYPE_CHECKING
from nonebot.adapters import Event
from nonebot.permission import SUPERUSER
from nonebot_plugin_waiter import waiter
from zhenxun.services.ai.core.exceptions import AbortException, ToolFatalError
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_agent as logger
if TYPE_CHECKING:
from zhenxun.services.ai.run.context import RunContext
from .context import RunContext
CANCEL_WORDS = {"取消", "cancel", "0", "退出", "quit"}
CONFIRM_WORDS = {"y", "yes", "是", "1", "ok", "确认"}
@@ -24,7 +24,7 @@ class HITLController:
封装底层的物理环境隔离等待逻辑,供上层工具和中间件发起提问或审批。
"""
def __init__(self, context: "RunContext"):
def __init__(self, context: RunContext):
self.context = context
async def wait_event(
@@ -42,7 +42,9 @@ class HITLController:
if not bot or not event:
logger.warning("HITLController: 当前环境无 Bot/Event 实例,无法发起交互。")
return None
raise ToolFatalError(
"当前处于自动化后台调度环境,无法发起人工交互。任务已强行中止。"
)
if prompt_msg:
await bot.send(event, prompt_msg)
+16 -28
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
"""
运行时(Run)相关核心类型定义
"""
from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Callable
import json
from typing import Any, Generic, cast
from typing_extensions import TypeVar
@@ -10,12 +12,16 @@ from typing_extensions import TypeVar
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
from zhenxun.services.ai.core.messages import AgentMessage, LLMMessage, UsageInfo
from zhenxun.services.ai.core.options import BaseOutputDefinition
from zhenxun.services.ai.core.protocols.tool import ToolResolvable
from zhenxun.services.ai.core.stream_events import (
AgentStreamEvent,
EventBus,
ToolCallStartEvent,
ToolStreamChunkEvent,
)
from zhenxun.services.ai.guardrails import BaseGuardrail, GuardrailSource
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.utils.pydantic_compat import model_dump, model_validator
@@ -37,7 +43,7 @@ class ToolSummary(BaseModel):
"""工具执行失败次数"""
total_latency_ms: float = 0.0
"""工具执行总耗时(毫秒)"""
by_name: dict[str, dict[str, Any]] = Field(default_factory=dict)
by_name: dict[str, dict[str, float | int]] = Field(default_factory=dict)
"""按工具名称细分的执行状态统计"""
@@ -126,9 +132,8 @@ class StreamedRunResult(Generic[OutputDataT]):
self.is_complete: bool = False
self._result: AgentRunResult[OutputDataT] | None = None
async def stream_events(self) -> AsyncIterator[Any]:
async def stream_events(self) -> "AsyncIterator[AgentStreamEvent]":
"""获取底层的所有原始事件(包含工具调用过程等)"""
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunError
async for event in self._event_bus:
if isinstance(event, AgentRunEnd):
@@ -214,24 +219,6 @@ class StreamedRunResult(Generic[OutputDataT]):
return await self.get_run_result()
class TaskResult(BaseModel):
"""单个数据契约任务的执行结果"""
task_id: str
"""关联的任务唯一 ID"""
output: Any
"""任务的实际产出(如果是结构化任务,则为解析后的 Pydantic 实例;否则为纯文本)"""
raw_response: str | None = None
"""大模型返回的原始纯文本内容"""
usage: UsageInfo = Field(default_factory=UsageInfo)
"""该任务执行期间的 Token 消耗统计"""
model_config = ConfigDict(arbitrary_types_allowed=True)
class AgentTask(BaseModel):
"""标准化数据契约(意图载体 Payload),定义大模型需要做什么及产出什么格式"""
@@ -247,23 +234,25 @@ class AgentTask(BaseModel):
expected_output: str
"""预期输出的自然语言描述(指导大模型如何组织最终答案)"""
response_model: Any | None = None
response_model: type[BaseModel] | BaseOutputDefinition | None = None
"""强制要求返回的强类型结构 (Pydantic Model) 或
OutputDefinition,为空则返回普通文本"""
tools: list[str | Any] | None = None
tools: list[str | Callable | dict[str, Any] | BaseTool | ToolResolvable] | None = (
None
)
"""针对此特定任务动态追加或覆盖的工具列表"""
guardrails: list[Any] | None = None
guardrails: list[GuardrailSource] | None = None
"""护栏验证列表。支持传入函数、BaseGuardrail 实例,
或直接传入自然语言字符串规则(自动转为 LLM 裁判)"""
_parsed_guardrails: list[Any] = PrivateAttr(default_factory=list)
_parsed_guardrails: list[BaseGuardrail] = PrivateAttr(default_factory=list)
model_config = ConfigDict(arbitrary_types_allowed=True)
@model_validator(mode="after")
def _parse_and_set_guardrails(self) -> "AgentTask":
def _parse_and_set_guardrails(self) -> AgentTask:
from zhenxun.services.ai.guardrails import parse_guardrails
self._parsed_guardrails = parse_guardrails(self.guardrails)
@@ -275,5 +264,4 @@ __all__ = [
"AgentTask",
"OutputDataT",
"StreamedRunResult",
"TaskResult",
]
+1 -1
View File
@@ -108,7 +108,7 @@ class AgentSessionManager:
count += 1
if task and not task.done():
task.cancel()
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_agent as logger
if count > 0:
logger.info(
+27 -6
View File
@@ -13,9 +13,12 @@ from zhenxun.services.ai.core.stream_events import (
ToolStreamChunkEvent,
UserCustomEvent,
)
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunStart, AgentRunSummary
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
from .context import RunContext
from .models import AgentRunEnd, AgentRunStart, AgentRunSummary
class TelemetrySubscriber:
@@ -122,7 +125,7 @@ class DefaultUISubscriber:
async def _send_to_platform(self, display: Any):
"""将通用的显示内容或富文本消息部件渲染并发送至具体的聊天平台"""
if not self.bot or not self.event or not display:
if not self.bot or not display:
return
if (
@@ -144,9 +147,27 @@ class DefaultUISubscriber:
display = msg
if isinstance(display, UniMessage):
await display.send(self.event, bot=self.bot, reply_to=self.reply_to)
if self.event:
await display.send(self.event, bot=self.bot, reply_to=self.reply_to)
else:
target = PlatformUtils.get_target(
user_id=self.context.get_user_id(),
group_id=self.context.get_group_id(),
)
if target:
await display.send(target=target, bot=self.bot)
else:
await self.bot.send(self.event, str(display))
if self.event:
await self.bot.send(self.event, str(display))
else:
target = PlatformUtils.get_target(
user_id=self.context.get_user_id(),
group_id=self.context.get_group_id(),
)
if target:
await MessageUtils.build_message(str(display)).send(
target=target, bot=self.bot
)
async def on_tool_stream(self, event: ToolStreamChunkEvent):
"""处理工具流式数据块事件,仅在 verbose 开启时发送给平台"""
+34 -33
View File
@@ -1,17 +1,20 @@
from typing import TYPE_CHECKING, Any
from __future__ import annotations
from typing import Any
from nonebot_plugin_alconna import UniMessage
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent, UserCustomEvent
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
if TYPE_CHECKING:
from zhenxun.services.ai.run.context import RunContext
from .context import RunContext
class UIController:
"""前端 UI 富交互流式控制器 (按需生成模式)"""
def __init__(self, context: "RunContext"):
def __init__(self, context: RunContext):
self.context = context
@property
@@ -19,38 +22,31 @@ class UIController:
"""从上下文中动态获取当前调用的工具名"""
return getattr(self.context.call, "tool_name", "UnknownTool")
@property
def _event_bus(self) -> Any | None:
"""从运行时上下文中获取底层的事件发射器"""
return getattr(self.context.run, "event_bus", None)
async def send_text(self, text: str, status: str = "running") -> None:
"""向前端流式反馈执行进度文本"""
if self._event_bus:
await self._event_bus.emit(
ToolStreamChunkEvent(
tool_name=self.tool_name, content=text, metadata={"status": status}
)
await self.context.run.emit(
ToolStreamChunkEvent(
tool_name=self.tool_name, content=text, metadata={"status": status}
)
)
async def send_image(self, image: bytes | str) -> None:
"""向前端发送富文本图片气泡(字节流或 URL)"""
if self._event_bus:
display_msg = UniMessage()
if isinstance(image, bytes):
display_msg = display_msg.image(raw=image)
else:
display_msg = display_msg.image(url=image)
await self._event_bus.emit(UserCustomEvent(display=display_msg))
display_msg = UniMessage()
if isinstance(image, bytes):
display_msg = display_msg.image(raw=image)
else:
display_msg = display_msg.image(url=image)
await self.context.run.emit(UserCustomEvent(display=display_msg))
async def send_display(self, display: Any) -> None:
"""向前端发送任意展示载体"""
if self._event_bus and display is not None:
await self._event_bus.emit(UserCustomEvent(display=display))
if display is not None:
await self.context.run.emit(UserCustomEvent(display=display))
@staticmethod
async def handle_control_flow_exit_display(
e: BaseException, context: "RunContext | None", reply_to: bool = False
e: BaseException, context: RunContext | None, reply_to: bool = False
) -> None:
"""统一处理 ControlFlowExit 异常带来的 UI 反馈逻辑"""
from zhenxun.services.ai.core.exceptions import ControlFlowExit
@@ -63,19 +59,24 @@ class UIController:
return
try:
from nonebot_plugin_alconna import UniMessage
bot = context.get_bot() if context else None
event = context.get_event() if context else None
if bot and event:
if isinstance(display_msg, UniMessage):
await display_msg.send(event, bot=bot, reply_to=reply_to)
else:
await bot.send(event, str(display_msg))
if bot:
msg_to_send = (
display_msg
if isinstance(display_msg, UniMessage)
else MessageUtils.build_message(str(display_msg))
)
if event:
await msg_to_send.send(event, bot=bot, reply_to=reply_to)
elif context:
target = PlatformUtils.get_target(
user_id=context.get_user_id(), group_id=context.get_group_id()
)
if target:
await msg_to_send.send(target=target, bot=bot)
else:
from zhenxun.utils.message import MessageUtils
await MessageUtils.build_message(str(display_msg)).send(
reply_to=reply_to
)