本次提交新增了渠道消息处理的完整核心模块,包含以下核心功能: 1. 新增会话围栏类,实现会话并发控制与过期清理 2. 新增媒体清理器,实现过期媒体文件自动清理 3. 新增熔断器组件,实现服务降级与故障隔离 4. 新增消息处理器,完成渠道消息的完整流转处理 5. 新增限流器组件,实现渠道级和账户级流量控制 6. 新增链路追踪模块,集成Langfuse实现调用链路监控 7. 新增指标统计模块,实现消息处理全链路指标采集 8. 新增统一消息模型,封装全渠道消息格式 9. 新增块回复流水线,实现流式回复的合并与去重 10. 新增本地媒体存储模块,实现媒体文件的本地管理 11. 新增回复分发器,实现回复内容的有序发送与延迟处理 12. 完善__init__.py导出所有核心模块与工具类
184 lines
5.6 KiB
Python
184 lines
5.6 KiB
Python
import logging
|
|
|
|
from sqlalchemy import select
|
|
|
|
from yuxi.channel.message.bridge import AgentBridge
|
|
from yuxi.channel.message.block_reply_pipeline import create_block_reply_pipeline
|
|
from yuxi.channel.message.models import ChunkType, DispatchResult, ReplyStage, StreamingChunk, UnifiedMessage
|
|
from yuxi.channel.protocols import OutboundProtocol
|
|
from yuxi.channel.plugins.registry import ChannelPluginRegistry
|
|
from yuxi.channel.session import SessionRecorder
|
|
from yuxi.repositories.conversation_repository import ConversationRepository
|
|
from yuxi.services.chat_service import stream_agent_chat
|
|
from yuxi.storage.postgres.manager import pg_manager
|
|
from yuxi.storage.postgres.models_business import User
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def channel_message_handler(
|
|
msg: UnifiedMessage,
|
|
result: DispatchResult,
|
|
channel_manager=None,
|
|
) -> None:
|
|
channel_type = msg.channel_type
|
|
account_id = msg.account_id
|
|
|
|
logger.info(
|
|
"channel_turn start: msg_id=%s channel=%s agent_config_id=%s",
|
|
msg.msg_id,
|
|
channel_type,
|
|
result.agent_config_id,
|
|
)
|
|
|
|
user = await _resolve_user(result, msg.msg_id, channel_type)
|
|
if user is None:
|
|
logger.error(
|
|
"channel_turn drop: msg_id=%s channel=%s reason=no_user",
|
|
msg.msg_id,
|
|
channel_type,
|
|
)
|
|
return
|
|
|
|
if channel_manager:
|
|
channel_manager.inc_active_runs(channel_type, account_id)
|
|
channel_manager.update_last_message_at(channel_type, account_id)
|
|
|
|
try:
|
|
bridge = AgentBridge(stream_fn=stream_agent_chat, channel_manager=channel_manager)
|
|
|
|
async with pg_manager.get_async_session_context() as db:
|
|
await _record_session(msg, result, user, db)
|
|
|
|
pipeline = create_block_reply_pipeline(
|
|
send_fn=_make_send_fn(msg, result, channel_manager),
|
|
msg=msg,
|
|
group_msg_id=msg.group.id if msg.group else None,
|
|
)
|
|
|
|
async for chunk in bridge.invoke_stream(
|
|
msg,
|
|
result,
|
|
result.agent_config_id,
|
|
user,
|
|
db,
|
|
):
|
|
await pipeline.enqueue(chunk)
|
|
|
|
await pipeline.wait_idle()
|
|
|
|
if result.thread_id:
|
|
await _touch_session(msg, result, db)
|
|
|
|
except Exception:
|
|
logger.exception(
|
|
"channel_turn error: msg_id=%s channel=%s",
|
|
msg.msg_id,
|
|
channel_type,
|
|
)
|
|
finally:
|
|
if channel_manager:
|
|
channel_manager.dec_active_runs(channel_type, account_id)
|
|
|
|
|
|
def _make_send_fn(msg: UnifiedMessage, result: DispatchResult, channel_manager):
|
|
channel_type = msg.channel_type
|
|
plugin = ChannelPluginRegistry.get(channel_type)
|
|
if plugin is None or not isinstance(plugin, OutboundProtocol):
|
|
return _noop_send_fn
|
|
|
|
target_id = msg.group.id if msg.group and msg.group.id else msg.sender.id
|
|
|
|
async def send_fn(chunk: StreamingChunk) -> None:
|
|
try:
|
|
if chunk.stage == ReplyStage.BLOCK and chunk.chunk_type == ChunkType.TEXT_DELTA:
|
|
await plugin.send_text(
|
|
target_id,
|
|
chunk.content,
|
|
reply_to_id=result.thread_id,
|
|
)
|
|
except Exception:
|
|
logger.exception(
|
|
"channel_turn deliver_reply error: msg_id=%s channel=%s",
|
|
msg.msg_id,
|
|
channel_type,
|
|
)
|
|
|
|
return send_fn
|
|
|
|
|
|
async def _noop_send_fn(chunk: StreamingChunk) -> None:
|
|
pass
|
|
|
|
|
|
async def _record_session(
|
|
msg: UnifiedMessage,
|
|
result: DispatchResult,
|
|
user: User,
|
|
db,
|
|
) -> None:
|
|
session_key = result.session_key or f"{msg.channel_type}:{msg.account_id}:{msg.sender.id}"
|
|
try:
|
|
conv_repo = ConversationRepository(db)
|
|
recorder = SessionRecorder(conv_repo)
|
|
await recorder.record_or_update(
|
|
msg,
|
|
result,
|
|
session_key,
|
|
user_id=str(user.id),
|
|
title=msg.sender.display_name or str(msg.sender.id),
|
|
)
|
|
except Exception:
|
|
logger.exception(
|
|
"channel_turn session record failed: msg_id=%s channel=%s",
|
|
msg.msg_id,
|
|
msg.channel_type,
|
|
)
|
|
|
|
|
|
async def _touch_session(
|
|
msg: UnifiedMessage,
|
|
result: DispatchResult,
|
|
db,
|
|
) -> None:
|
|
try:
|
|
conv_repo = ConversationRepository(db)
|
|
recorder = SessionRecorder(conv_repo)
|
|
await recorder.touch(msg, result.thread_id)
|
|
except Exception:
|
|
logger.exception(
|
|
"channel_turn session touch failed: msg_id=%s thread=%s",
|
|
msg.msg_id,
|
|
result.thread_id,
|
|
)
|
|
|
|
|
|
async def _resolve_user(
|
|
result: DispatchResult,
|
|
msg_id: str,
|
|
channel_type: str,
|
|
) -> User | None:
|
|
async with pg_manager.get_async_session_context() as db:
|
|
if result.internal_user_id:
|
|
try:
|
|
uid = int(result.internal_user_id)
|
|
except (ValueError, TypeError):
|
|
uid = None
|
|
if uid is not None:
|
|
user = (await db.execute(select(User).where(User.id == uid))).scalar_one_or_none()
|
|
if user is not None:
|
|
logger.info(
|
|
"channel_turn resolve_user: msg_id=%s channel=%s resolved_by=internal_user_id",
|
|
msg_id,
|
|
channel_type,
|
|
)
|
|
return user
|
|
|
|
logger.warning(
|
|
"channel_turn resolve_user: msg_id=%s channel=%s internal_user_id=%s no match",
|
|
msg_id,
|
|
channel_type,
|
|
result.internal_user_id,
|
|
)
|
|
return None
|