ForcePilot/backend/package/yuxi/storage/postgres/models_channel.py
Kris 9a8a27bf36 feat(channel): 新增渠道网关模块完整实现
本次提交新增了完整的多渠道消息网关系统,包括:
1. 支持飞书、钉钉、Web、Hook 四种渠道的适配器与配置
2. 领域模型层:消息、会话、绑定、出箱等核心实体
3. 应用服务层:管道、中间件、DTO 与业务逻辑
4. 基础设施层:持久化、过滤器、队列等端口实现
5. 接口层:REST API、SSE、WebSocket 通信端点
6. 前端页面与路由配置,添加渠道管理菜单
7. 新增相关依赖包与 docker-compose 部署配置
2026-05-30 21:53:09 +08:00

167 lines
8.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.

"""PostgreSQL 渠道网关模型 - 渠道绑定、出站重试、消息审计日志"""
from sqlalchemy import (
JSON,
Boolean,
Column,
DateTime,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
)
from yuxi.storage.postgres.models_business import Base
from yuxi.utils.datetime_utils import format_utc_datetime, utc_now_naive
class ChannelBinding(Base):
"""渠道 Agent 绑定路由表 — Worker Step 3 查询,决定消息路由到哪个 Agent"""
__tablename__ = "channel_bindings"
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键")
channel_type = Column(String(20), nullable=False, index=True, comment="渠道类型: feishu/dingtalk/hooks")
account_id = Column(String(64), nullable=False, default="", comment="渠道账户标识")
group_id = Column(String(128), nullable=False, default="", comment="群组标识DM 为空字符串")
agent_config_id = Column(Integer, ForeignKey("agent_configs.id"), nullable=False, comment="绑定的 Agent 配置 ID")
session_key_strategy = Column(String(20), nullable=False, default="auto", comment="会话键策略: auto/main/channel_group/custom")
is_enabled = Column(Boolean, nullable=False, default=True, index=True, comment="是否启用")
created_by = Column(String(64), nullable=True)
updated_by = Column(String(64), nullable=True)
created_at = Column(DateTime, nullable=False, default=utc_now_naive, comment="创建时间")
updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive, comment="更新时间")
is_deleted = Column(Integer, nullable=False, default=0, index=True, comment="软删除标记: 0=否, 1=是")
deleted_at = Column(DateTime, nullable=True, comment="删除时间")
__table_args__ = (
UniqueConstraint("channel_type", "account_id", "group_id", name="uq_channel_binding"),
)
def to_dict(self):
return {
"id": self.id,
"channel_type": self.channel_type,
"account_id": self.account_id,
"group_id": self.group_id,
"agent_config_id": self.agent_config_id,
"session_key_strategy": self.session_key_strategy,
"is_enabled": bool(self.is_enabled),
"created_by": self.created_by,
"updated_by": self.updated_by,
"created_at": format_utc_datetime(self.created_at),
"updated_at": format_utc_datetime(self.updated_at),
"is_deleted": self.is_deleted,
"deleted_at": format_utc_datetime(self.deleted_at),
}
class ChannelOutbox(Base):
"""出站消息重试队列 — 适配器发送失败后持久化OutboxRetryWorker 按指数退避重试"""
__tablename__ = "channel_outbox"
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键")
message_id = Column(String(64), nullable=False, index=True, comment="关联的原始消息 ID")
session_id = Column(String(64), nullable=False, index=True, comment="会话 ID (thread_id)")
channel_type = Column(String(20), nullable=False, index=True, comment="目标渠道类型")
content = Column(Text, nullable=False, comment="待发送内容 (标准 Markdown)")
status = Column(
String(20), nullable=False, default="pending", index=True,
comment="状态: pending/retrying/sent/failed/dead",
)
retry_count = Column(Integer, nullable=False, default=0, comment="已重试次数")
max_retries = Column(Integer, nullable=False, default=5, comment="最大重试次数")
next_retry_at = Column(DateTime, nullable=True, index=True, comment="下次重试时间")
last_error = Column(Text, nullable=True, comment="最后一次发送错误信息")
trace_id = Column(String(64), nullable=True, index=True, comment="链路追踪 ID")
extra_metadata = Column(JSON, nullable=True, comment="扩展元数据")
created_by = Column(String(64), nullable=True)
updated_by = Column(String(64), nullable=True)
created_at = Column(DateTime, nullable=False, default=utc_now_naive, comment="创建时间")
updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive, comment="更新时间")
is_deleted = Column(Integer, nullable=False, default=0, index=True, comment="软删除标记: 0=否, 1=是")
deleted_at = Column(DateTime, nullable=True, comment="删除时间")
def to_dict(self):
return {
"id": self.id,
"message_id": self.message_id,
"session_id": self.session_id,
"channel_type": self.channel_type,
"content": self.content,
"status": self.status,
"retry_count": self.retry_count,
"max_retries": self.max_retries,
"next_retry_at": format_utc_datetime(self.next_retry_at),
"last_error": self.last_error,
"trace_id": self.trace_id,
"extra_metadata": self.extra_metadata or {},
"created_by": self.created_by,
"updated_by": self.updated_by,
"created_at": format_utc_datetime(self.created_at),
"updated_at": format_utc_datetime(self.updated_at),
"is_deleted": self.is_deleted,
"deleted_at": format_utc_datetime(self.deleted_at),
}
class ChannelMessageLog(Base):
"""渠道消息审计日志 — 记录入站/出站消息处理轨迹,支持 trace_id 链路追踪"""
__tablename__ = "channel_message_logs"
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键")
trace_id = Column(String(64), nullable=False, index=True, comment="链路追踪 ID")
message_id = Column(String(64), nullable=False, index=True, comment="消息 ID")
channel_type = Column(String(20), nullable=False, index=True, comment="渠道类型")
conversation_id = Column(Integer, nullable=True, index=True, comment="关联的 Conversation ID")
session_id = Column(String(64), nullable=True, index=True, comment="会话 ID")
direction = Column(String(10), nullable=False, index=True, comment="方向: inbound/outbound")
sender_id = Column(String(64), nullable=True, comment="发送者 ID")
content_summary = Column(String(500), nullable=True, comment="内容摘要(前 500 字符)")
agent_config_id = Column(Integer, nullable=True, index=True, comment="Agent 配置 ID")
status = Column(String(20), nullable=False, default="received", index=True, comment="处理状态")
pipeline_result = Column(String(20), nullable=True, comment="管道处理结果: accepted/aborted/skipped")
abort_reason = Column(String(64), nullable=True, comment="管道中止原因码")
worker_result = Column(String(20), nullable=True, comment="Worker 处理结果")
error_message = Column(Text, nullable=True, comment="错误信息")
processing_time_ms = Column(Integer, nullable=True, comment="总处理耗时(毫秒)")
extra_metadata = Column(JSON, nullable=True, comment="扩展元数据")
created_by = Column(String(64), nullable=True)
updated_by = Column(String(64), nullable=True)
created_at = Column(DateTime, nullable=False, default=utc_now_naive, comment="创建时间")
updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive, comment="更新时间")
is_deleted = Column(Integer, nullable=False, default=0, index=True, comment="软删除标记: 0=否, 1=是")
deleted_at = Column(DateTime, nullable=True, comment="删除时间")
def to_dict(self):
return {
"id": self.id,
"trace_id": self.trace_id,
"message_id": self.message_id,
"channel_type": self.channel_type,
"conversation_id": self.conversation_id,
"session_id": self.session_id,
"direction": self.direction,
"sender_id": self.sender_id,
"content_summary": self.content_summary,
"agent_config_id": self.agent_config_id,
"status": self.status,
"pipeline_result": self.pipeline_result,
"abort_reason": self.abort_reason,
"worker_result": self.worker_result,
"error_message": self.error_message,
"processing_time_ms": self.processing_time_ms,
"extra_metadata": self.extra_metadata or {},
"created_by": self.created_by,
"updated_by": self.updated_by,
"created_at": format_utc_datetime(self.created_at),
"updated_at": format_utc_datetime(self.updated_at),
"is_deleted": self.is_deleted,
"deleted_at": format_utc_datetime(self.deleted_at),
}