ForcePilot/backend/package/yuxi/storage/postgres/models_channel.py
Kris 9e503becd3
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat(plugin): 实现完整的插件注册管理系统
新增了插件相关的完整领域模型、应用服务、基础设施实现,包括:
1. 插件状态、注册模式、来源等基础枚举和数据结构
2. 插件清单解析、发现、加载工具类
3. 插件注册表领域服务和内存存储实现
4. 插件相关的命令、查询、事件定义
5. 插件REST API接口和DTO映射
6. 集成了原有通道适配器到插件系统
7. 新增内置插件注册和自动发现能力
2026-05-31 16:44:13 +08:00

314 lines
16 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),
}
class ChannelPlugin(Base):
"""渠道插件注册表 — 存储插件元数据、状态与配置
对应领域模型PluginRecord 实体
覆盖来源workspace(本地开发) + installed(第三方安装)
bundled(内置) 插件不入库,由代码自动注册
"""
__tablename__ = "channel_plugins"
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键")
plugin_id = Column(String(64), nullable=False, unique=True, index=True, comment="插件唯一标识")
name = Column(String(128), nullable=False, comment="插件显示名称")
version = Column(String(32), nullable=False, default="0.0.0", comment="插件版本号")
channel_type = Column(String(64), nullable=False, index=True, comment="渠道类型标识")
origin = Column(String(32), nullable=False, comment="来源: bundled/workspace/installed")
status = Column(String(32), nullable=False, default="discovered", comment="状态: discovered/registered/configured/active/error/disabled")
root_dir = Column(String(512), nullable=True, comment="插件根目录路径")
entry_file = Column(String(512), nullable=True, comment="入口文件路径")
manifest = Column(JSON, nullable=True, comment="完整插件清单(yuxi.plugin.yaml内容)")
capabilities = Column(JSON, nullable=True, comment="渠道能力声明")
config_schema = Column(JSON, nullable=True, comment="配置校验schema")
config = Column(JSON, nullable=True, default=dict, comment="当前生效配置")
error = Column(Text, nullable=True, comment="错误信息")
is_enabled = Column(Boolean, nullable=False, default=True, index=True, comment="是否启用")
registered_at = Column(DateTime, nullable=True, comment="注册时间")
activated_at = Column(DateTime, 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,
"plugin_id": self.plugin_id,
"name": self.name,
"version": self.version,
"channel_type": self.channel_type,
"origin": self.origin,
"status": self.status,
"root_dir": self.root_dir,
"entry_file": self.entry_file,
"manifest": self.manifest or {},
"capabilities": self.capabilities or {},
"config_schema": self.config_schema or {},
"config": self.config or {},
"error": self.error,
"is_enabled": bool(self.is_enabled),
"registered_at": format_utc_datetime(self.registered_at),
"activated_at": format_utc_datetime(self.activated_at),
"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 ChannelPluginConfigHistory(Base):
"""插件配置历史表 — 记录配置变更,支持回滚
对应领域事件PluginConfiguredEvent
生命周期:每次调用 set_config() 时写入一条记录
"""
__tablename__ = "channel_plugin_config_history"
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键")
plugin_id = Column(String(64), nullable=False, index=True, comment="插件标识")
config = Column(JSON, nullable=False, comment="配置快照")
changed_by = Column(String(128), nullable=True, comment="变更人")
change_reason = Column(String(255), 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,
"plugin_id": self.plugin_id,
"config": self.config or {},
"changed_by": self.changed_by,
"change_reason": self.change_reason,
"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 ChannelPluginAuditLog(Base):
"""插件操作审计日志 — 记录插件生命周期关键操作
对应领域事件PluginRegisteredEvent / PluginActivatedEvent /
PluginDeactivatedEvent / PluginReloadEvent
"""
__tablename__ = "channel_plugin_audit_logs"
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键")
plugin_id = Column(String(64), nullable=False, index=True, comment="插件标识")
action = Column(String(32), nullable=False, comment="操作: register/activate/deactivate/reload/error")
old_status = Column(String(32), nullable=True, comment="操作前状态")
new_status = Column(String(32), nullable=True, comment="操作后状态")
old_version = Column(String(32), nullable=True, comment="操作前版本(重载场景)")
new_version = Column(String(32), nullable=True, comment="操作后版本(重载场景)")
detail = Column(JSON, nullable=True, comment="操作详情")
error_message = Column(Text, 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,
"plugin_id": self.plugin_id,
"action": self.action,
"old_status": self.old_status,
"new_status": self.new_status,
"old_version": self.old_version,
"new_version": self.new_version,
"detail": self.detail or {},
"error_message": self.error_message,
"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),
}