""" 入站分发壳:RouteDecision.intent → 唯一 handler。 本文件职责:维护意图→可调用对象表;dispatch_inbound 只查表调用。 禁止:在本文件写补问/建单/TMS 业务;禁止一个函数处理全部卡片。 """ from __future__ import annotations import logging from typing import Callable, Optional from agent.channel.wecom.models import InboundMessage from agent.handlers import abnormal, card_action, close_deal, continue_thread, new_inquiry from agent.handlers.echo_text import handle_echo_text from agent.handlers.text_inquiry import handle_text_inquiry, handle_text_other from agent.routing.decision import RouteDecision logger = logging.getLogger(__name__) # Handler 签名约定:只收 InboundMessage(出站/store 由 handler 自行取依赖) HandlerFn = Callable[[InboundMessage], None] def _noop(message: InboundMessage) -> None: """忽略类意图:不做事。""" logger.info("dispatch.noop sender=%s msg=%s", message.sender_id, message.message_id) def _wrap_echo(message: InboundMessage) -> None: """联调残留:仅 echo_text 意图使用。""" from agent.channel.queue import get_message_store from agent.config import get_settings settings = get_settings() handle_echo_text( message, get_message_store(), ytd_env=settings.ytd_env, reply_template=settings.wecom_callback_reply_text, ) def _wrap_text_inquiry(message: InboundMessage) -> None: """普通文字询价:空运闭环。""" handle_text_inquiry(message) def _wrap_text_other(message: InboundMessage) -> None: """闲聊:回首次引导;若正等按钮则当按钮。""" handle_text_other(message) def _wrap_new(message: InboundMessage) -> None: new_inquiry.handle_new_inquiry(message) def _wrap_continue(message: InboundMessage, *, thread_id: str = "") -> None: continue_thread.handle_continue_thread(message, thread_id=thread_id) def _wrap_card(message: InboundMessage) -> None: """ 卡片意图:把企微 TaskId / EventKey 原样交给流程,按当时工单号找回会话。 action 用正文(EventKey 或按钮文案),禁止写死 unknown。 """ from agent.policy import inquiry_copy as copy action = copy.canonical_button(message.content or "") or (message.content or "").strip() card_action.handle_card_action( sender_id=message.sender_id, thread_id="", wait_version=0, action=action or "unknown", payload=dict(message.raw or {}), ) def _wrap_close(message: InboundMessage) -> None: close_deal.handle_close_deal( sender_id=message.sender_id, inquiry_no="", outcome="unknown", ) def _wrap_abnormal(message: InboundMessage) -> None: abnormal.handle_abnormal(sender_id=message.sender_id, inquiry_no="", reason="dispatch") def _wrap_route_failed(message: InboundMessage) -> None: """模型没给出路由结论:请销售再说一次,不假装听懂。""" from agent.channel.queue import get_message_store from agent.policy import inquiry_copy as copy get_message_store().enqueue_outbound( touser=message.sender_id, content=copy.ROUTE_MODEL_FAILED, dedupe_key=f"route-fail:{message.message_id}", ) # 意图 → handler(空壳或联调 echo);扩展时只改表,不改 ChannelRuntime 分支森林 HANDLER_REGISTRY: dict[str, HandlerFn] = { "echo_text": _wrap_echo, "ordinary_text_inquiry": _wrap_text_inquiry, "ordinary_text_other": _wrap_text_other, "attachment_inquiry": _wrap_new, "image_inquiry": _wrap_new, "multi_segment_transport": _wrap_new, "tms_read_only_quote": _wrap_continue, "new_inquiry": _wrap_new, "continue_thread": _wrap_continue, "card_action": _wrap_card, "close_deal": _wrap_close, "abnormal": _wrap_abnormal, "reject_no_sender": _noop, "ignore_empty": _noop, "stub_needs_model": _wrap_echo, "route_model_failed": _wrap_route_failed, "route_parse_failed": _wrap_route_failed, } def resolve_handler(intent: str) -> Optional[HandlerFn]: """查表;未知意图返回 None。""" return HANDLER_REGISTRY.get(intent) def dispatch_inbound(message: InboundMessage, decision: RouteDecision) -> str: """ 按 RouteDecision 调用唯一 handler。 返回:实际调用的 intent 名,或 unknown_intent / skipped。 副作用:仅 handler 内产生;本函数不做业务。 """ if not message.has_sender() and decision.intent != "reject_no_sender": logger.warning("dispatch 拒绝:无 sender_id") HANDLER_REGISTRY["reject_no_sender"](message) return "reject_no_sender" fn = resolve_handler(decision.intent) if fn is None: logger.warning("dispatch 未知意图 intent=%s → noop", decision.intent) _noop(message) return "unknown_intent" logger.info( "dispatch intent=%s sender=%s msg=%s wo=%s", decision.intent, message.sender_id, message.message_id, decision.thread_id or "-", ) if decision.intent == "continue_thread": continue_thread.handle_continue_thread(message, thread_id=decision.thread_id) return decision.intent fn(message) return decision.intent