Files
inquiry_robot/inquiry-agent/agent/channel/runtime.py
T

130 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
通道消费循环:inbox → RouteDecision → dispatch → handler;outbox → 企微发送。
本文件职责:在 HTTP 进程内独立线程跑快路径,不与 LibreOffice 串队。
禁止:在回调请求线程调用本循环的阻塞逻辑;禁止 Redis 全局单例锁。
"""
from __future__ import annotations
import logging
import threading
from typing import Optional
from agent.channel.outbox.sender import PERMANENT_SEND_ERRORS, WeComAppClient
from agent.channel.queue import get_message_store
from agent.config import Settings
from agent.routing import IntentRouter
from agent.routing.dispatch import dispatch_inbound
logger = logging.getLogger(__name__)
# 避免 httpx 把 access_token / secret 打进 INFO
logging.getLogger("httpx").setLevel(logging.WARNING)
class ChannelRuntime:
"""
企微通道运行时:inbox/outbox 各一条守护线程。
启动时机:FastAPI lifespan;停止:进程退出。
inbox:IntentRouter.route(默认 stub)→ dispatch 查表;无业务分支森林。
"""
def __init__(self, settings: Settings) -> None:
self._settings = settings
self._store = get_message_store()
self._stop = threading.Event()
self._threads: list[threading.Thread] = []
self._router = IntentRouter(allow_network=False)
agent_id = int(settings.wecom_agent_id or "0") or 0
self._client = WeComAppClient(
corp_id=settings.wecom_corp_id,
secret=settings.wecom_secret,
agent_id=agent_id,
)
@property
def store(self):
return self._store
def start(self) -> None:
"""启动 inbox/outbox 消费线程。"""
self._stop.clear()
self._threads = [
threading.Thread(target=self._inbox_loop, name="wecom-inbox", daemon=True),
threading.Thread(target=self._outbox_loop, name="wecom-outbox", daemon=True),
]
for t in self._threads:
t.start()
logger.info("ChannelRuntime 已启动(inbox/outbox 独立线程;dispatch 分发)")
def stop(self) -> None:
self._stop.set()
def _inbox_loop(self) -> None:
while not self._stop.is_set():
item = self._store.claim_inbound()
if item is None:
self._store.wait_inbox(1.0)
continue
try:
decision = self._router.route(
text=item.message.content or "",
sender_id=item.message.sender_id or "",
)
dispatch_inbound(item.message, decision)
self._store.complete_inbound(item.id)
except Exception as exc: # noqa: BLE001
logger.exception("inbox 处理失败 id=%s", item.id)
self._store.complete_inbound(item.id, error=str(exc))
def _outbox_loop(self) -> None:
while not self._stop.is_set():
item = self._store.claim_outbound()
if item is None:
self._store.wait_outbox(1.0)
continue
# 过期丢弃:超过 30 分钟的出站不再发(对齐「过期不计业务成功」)
import time
if time.time() - item.created_at > 1800:
logger.warning("outbox 过期丢弃 id=%s", item.id)
self._store.complete_outbound(item.id)
continue
result = self._client.send_text(touser=item.touser, content=item.content)
if result.ok:
self._store.complete_outbound(item.id)
elif result.errcode in PERMANENT_SEND_ERRORS:
logger.error(
"outbox 永久失败不再重试 id=%s err=%s:%s",
item.id,
result.errcode,
result.errmsg,
)
self._store.complete_outbound(
item.id,
error=f"permanent:{result.errcode}:{result.errmsg}",
permanent=True,
)
else:
self._store.complete_outbound(
item.id,
error=f"{result.errcode}:{result.errmsg}",
)
_RUNTIME: Optional[ChannelRuntime] = None
def get_channel_runtime() -> Optional[ChannelRuntime]:
return _RUNTIME
def start_channel_runtime(settings: Settings) -> ChannelRuntime:
"""幂等启动通道运行时。"""
global _RUNTIME
if _RUNTIME is None:
_RUNTIME = ChannelRuntime(settings)
_RUNTIME.start()
return _RUNTIME