本次提交新增了渠道消息处理的完整核心模块,包含以下核心功能: 1. 新增会话围栏类,实现会话并发控制与过期清理 2. 新增媒体清理器,实现过期媒体文件自动清理 3. 新增熔断器组件,实现服务降级与故障隔离 4. 新增消息处理器,完成渠道消息的完整流转处理 5. 新增限流器组件,实现渠道级和账户级流量控制 6. 新增链路追踪模块,集成Langfuse实现调用链路监控 7. 新增指标统计模块,实现消息处理全链路指标采集 8. 新增统一消息模型,封装全渠道消息格式 9. 新增块回复流水线,实现流式回复的合并与去重 10. 新增本地媒体存储模块,实现媒体文件的本地管理 11. 新增回复分发器,实现回复内容的有序发送与延迟处理 12. 完善__init__.py导出所有核心模块与工具类
284 lines
7.8 KiB
Python
284 lines
7.8 KiB
Python
import hashlib
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
from yuxi.channel.routing.models import PeerKind
|
|
|
|
|
|
class MessageType(StrEnum):
|
|
TEXT = "text"
|
|
IMAGE = "image"
|
|
VOICE = "voice"
|
|
FILE = "file"
|
|
EVENT = "event"
|
|
|
|
|
|
class ReplyStage(StrEnum):
|
|
TOOL = "tool"
|
|
BLOCK = "block"
|
|
FINAL = "final"
|
|
|
|
|
|
class ChunkType(StrEnum):
|
|
TEXT_DELTA = "text-delta"
|
|
REASONING = "reasoning"
|
|
TOOL_CALL = "tool-call"
|
|
TOOL_RESULT = "tool-result"
|
|
HEARTBEAT = "heartbeat"
|
|
CITATION = "citation"
|
|
STATUS = "status"
|
|
ERROR = "error"
|
|
|
|
|
|
class MentionSource(StrEnum):
|
|
EXPLICIT_BOT = "explicit_bot"
|
|
SUBTEAM = "subteam"
|
|
MENTION_PATTERN = "mention_pattern"
|
|
IMPLICIT_THREAD = "implicit_thread"
|
|
COMMAND_BYPASS = "command_bypass"
|
|
NONE = "none"
|
|
|
|
|
|
@dataclass
|
|
class PeerInfo:
|
|
kind: PeerKind
|
|
id: str
|
|
display_name: str | None = None
|
|
username: str | None = None
|
|
tag: str | None = None
|
|
is_bot: bool = False
|
|
is_self: bool = False
|
|
display_label: str | None = None
|
|
roles: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class GroupContext:
|
|
id: str | None = None
|
|
name: str | None = None
|
|
guild_id: str | None = None
|
|
team_id: str | None = None
|
|
thread_id: str | None = None
|
|
roles: list[str] = field(default_factory=list)
|
|
kind: str | None = None
|
|
space_id: str | None = None
|
|
parent_id: str | None = None
|
|
native_channel_id: str | None = None
|
|
route_peer_kind: str | None = None
|
|
route_peer_id: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class UnifiedMessage:
|
|
msg_id: str
|
|
channel_type: str
|
|
account_id: str
|
|
content: str
|
|
sender: PeerInfo
|
|
channel_config_id: str | None = None
|
|
message_type: MessageType = MessageType.TEXT
|
|
media_urls: list[str] = field(default_factory=list)
|
|
image_base64: str | None = None
|
|
group: GroupContext | None = None
|
|
timestamp: datetime | None = None
|
|
raw_payload: dict[str, Any] = field(default_factory=dict)
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
|
|
body_for_agent: str | None = None
|
|
command_body: str | None = None
|
|
body_for_commands: str | None = None
|
|
inbound_history: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
reply_chain: list[dict[str, Any]] = field(default_factory=list)
|
|
reply_to_id: str | None = None
|
|
reply_to_id_full: str | None = None
|
|
root_message_id: str | None = None
|
|
message_thread_id: str | None = None
|
|
reply_to_tag: bool = False
|
|
reply_to_current: bool = False
|
|
|
|
forwarded_from: str | None = None
|
|
forwarded_from_type: str | None = None
|
|
forwarded_from_id: str | None = None
|
|
forwarded_from_username: str | None = None
|
|
forwarded_date: float | None = None
|
|
|
|
media_path: str | None = None
|
|
media_paths: list[str] = field(default_factory=list)
|
|
media_types: list[str] = field(default_factory=list)
|
|
|
|
conversation_label: str | None = None
|
|
group_channel: str | None = None
|
|
group_space: str | None = None
|
|
group_members: str | None = None
|
|
group_system_prompt: str | None = None
|
|
member_role_ids: list[str] = field(default_factory=list)
|
|
|
|
was_mentioned: bool = False
|
|
explicitly_mentioned_bot: bool = False
|
|
mentioned_user_ids: list[str] = field(default_factory=list)
|
|
mention_source: MentionSource | None = None
|
|
|
|
surface: str | None = None
|
|
originating_channel: str | None = None
|
|
originating_to: str | None = None
|
|
native_channel_id: str | None = None
|
|
native_direct_user_id: str | None = None
|
|
thread_parent_id: str | None = None
|
|
|
|
location_lat: float | None = None
|
|
location_lon: float | None = None
|
|
|
|
|
|
@dataclass
|
|
class DispatchResult:
|
|
success: bool
|
|
thread_id: str | None = None
|
|
agent_config_id: int | None = None
|
|
error: str | None = None
|
|
session_key: str | None = None
|
|
matched_by: str | None = None
|
|
handled_by: str | None = None
|
|
command_response: str | None = None
|
|
internal_user_id: str | None = None
|
|
reply_sent: bool = False
|
|
explicit_target: str | None = None
|
|
queued_final: bool = False
|
|
counts: dict[str, int] = field(default_factory=dict)
|
|
failed_counts: dict[str, int] = field(default_factory=dict)
|
|
source_reply_delivery_mode: str | None = None
|
|
before_agent_run_blocked: bool = False
|
|
|
|
|
|
@dataclass
|
|
class StreamingChunk:
|
|
"""Agent 推理流式块"""
|
|
|
|
stage: ReplyStage = ReplyStage.BLOCK
|
|
chunk_type: ChunkType = ChunkType.TEXT_DELTA
|
|
content: str | None = None
|
|
tool_name: str | None = None
|
|
tool_input: dict[str, Any] | None = None
|
|
tool_output: str | None = None
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
agent_run_id: str | None = None
|
|
is_final: bool = False
|
|
item_id: str | None = None
|
|
title: str | None = None
|
|
phase: str | None = None
|
|
steps: list[str] | None = None
|
|
|
|
|
|
@dataclass
|
|
class ReplyPayload:
|
|
"""出站回复载荷(经过去重和合并后)"""
|
|
|
|
target_id: str
|
|
content: str
|
|
reply_to_id: str | None = None
|
|
thread_id: str | None = None
|
|
media_urls: list[str] = field(default_factory=list)
|
|
presentation: dict[str, Any] | None = None
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
btw: dict[str, str] | None = None
|
|
audio_as_voice: bool = False
|
|
spoken_text: str | None = None
|
|
is_error: bool = False
|
|
is_reasoning: bool = False
|
|
is_compaction_notice: bool = False
|
|
channel_data: dict[str, Any] | None = None
|
|
trusted_local_media: bool = False
|
|
sensitive_media: bool = False
|
|
delivery: dict[str, Any] | None = None
|
|
reply_to_tag: bool = False
|
|
reply_to_current: bool = False
|
|
|
|
@property
|
|
def payload_key(self) -> str:
|
|
key = f"{self.content}|{','.join(sorted(self.media_urls))}|{self.reply_to_id or ''}"
|
|
if self.presentation:
|
|
stable_hash = hashlib.md5(
|
|
json.dumps(self.presentation, sort_keys=True, ensure_ascii=False).encode()
|
|
).hexdigest()
|
|
key += f"|{stable_hash}"
|
|
return key
|
|
|
|
@property
|
|
def content_key(self) -> str:
|
|
return f"{self.content}|{','.join(sorted(self.media_urls))}"
|
|
|
|
|
|
class StreamStatus(StrEnum):
|
|
"""流执行终止状态"""
|
|
|
|
COMPLETED = "completed"
|
|
CANCELLED = "cancelled"
|
|
ERROR = "error"
|
|
TIMEOUT = "timeout"
|
|
|
|
|
|
@dataclass
|
|
class StreamResult:
|
|
"""流执行完成后的结构化结果,对应 openclaw 的 MessageReceipt 概念"""
|
|
|
|
status: StreamStatus = StreamStatus.COMPLETED
|
|
accumulated_text: str = ""
|
|
chunk_count: int = 0
|
|
text_chunk_count: int = 0
|
|
tool_chunk_count: int = 0
|
|
reasoning_chunk_count: int = 0
|
|
heartbeat_count: int = 0
|
|
citation_count: int = 0
|
|
error_count: int = 0
|
|
start_time: float = 0.0
|
|
end_time: float = 0.0
|
|
token_usage: dict[str, int] | None = None
|
|
error_message: str | None = None
|
|
|
|
@property
|
|
def elapsed_ms(self) -> float:
|
|
return (self.end_time - self.start_time) * 1000 if self.start_time > 0 else 0.0
|
|
|
|
@property
|
|
def is_success(self) -> bool:
|
|
return self.status == StreamStatus.COMPLETED
|
|
|
|
|
|
class StreamCancellationToken:
|
|
"""流取消令牌,对应 openclaw 的 AbortSignal"""
|
|
|
|
def __init__(self) -> None:
|
|
self._cancelled = False
|
|
|
|
def cancel(self) -> None:
|
|
self._cancelled = True
|
|
|
|
@property
|
|
def is_cancelled(self) -> bool:
|
|
return self._cancelled
|
|
|
|
def throw_if_cancelled(self) -> None:
|
|
if self._cancelled:
|
|
raise StreamCancelledError()
|
|
|
|
|
|
class StreamCancelledError(Exception):
|
|
pass
|
|
|
|
|
|
class StreamTimeoutError(Exception):
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class MessageReceipt:
|
|
platform_message_id: str
|
|
thread_id: str | None = None
|
|
reply_to_id: str | None = None
|
|
sent_at: float | None = None
|
|
parts: list[dict[str, Any]] = field(default_factory=list)
|
|
raw: list[dict[str, Any]] = field(default_factory=list)
|