"""PostgreSQL 渠道相关数据模型""" from typing import Any from sqlalchemy import ( JSON, Boolean, Column, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.ext.declarative import declarative_base from yuxi.channel.transport.qr_login import LoginState from yuxi.utils.datetime_utils import format_utc_datetime, utc_now_naive Base = declarative_base() JSON_VALUE = JSON().with_variant(JSONB, "postgresql") class ChannelConfig(Base): """渠道配置表""" __tablename__ = "channel_configs" __table_args__ = ( UniqueConstraint("channel_type", "account_id", name="uq_channel_configs_type_account"), Index("ix_channel_configs_enabled", "enabled"), ) id = Column(UUID(as_uuid=True), primary_key=True, default=func.gen_random_uuid()) channel_type = Column(String(32), nullable=False) account_id = Column(String(128), nullable=False) name = Column(String(100), nullable=True) config_json = Column(JSON_VALUE, nullable=False, default=dict) enabled = Column(Boolean, nullable=False, default=False) created_by = Column(String(100), nullable=True) updated_by = Column(String(100), nullable=True) created_at = Column(DateTime(timezone=False), default=utc_now_naive) updated_at = Column(DateTime(timezone=False), default=utc_now_naive, onupdate=utc_now_naive) def to_dict(self) -> dict[str, Any]: return { "id": str(self.id) if self.id else None, "channel_type": self.channel_type, "account_id": self.account_id, "name": self.name, "config_json": self.config_json or {}, "enabled": bool(self.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), } class ChannelSession(Base): """渠道会话映射表""" __tablename__ = "channel_sessions" __table_args__ = ( UniqueConstraint("session_key", name="uq_channel_sessions_session_key"), UniqueConstraint("conversation_id", name="uq_channel_sessions_conversation"), Index("ix_channel_sessions_key", "session_key"), Index("ix_channel_sessions_type_account", "channel_type", "account_id"), ) id = Column(UUID(as_uuid=True), primary_key=True, default=func.gen_random_uuid()) session_key = Column(String(512), nullable=False) channel_type = Column(String(32), nullable=False) account_id = Column(String(128), nullable=False) chat_type = Column(String(32), nullable=True) channel_sender_id = Column(String(255), nullable=True) conversation_id = Column(Integer, ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False) channel_metadata = Column(JSON_VALUE, nullable=True) created_at = Column(DateTime(timezone=False), default=utc_now_naive) updated_at = Column(DateTime(timezone=False), default=utc_now_naive, onupdate=utc_now_naive) def to_dict(self) -> dict[str, Any]: return { "id": str(self.id) if self.id else None, "session_key": self.session_key, "channel_type": self.channel_type, "account_id": self.account_id, "chat_type": self.chat_type, "channel_sender_id": self.channel_sender_id, "conversation_id": self.conversation_id, "channel_metadata": self.channel_metadata or {}, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } def update_channel_metadata(self, updates: dict[str, Any]) -> None: """合并更新 channel_metadata 并标记字段已修改。""" from sqlalchemy.orm.attributes import flag_modified merged = dict(self.channel_metadata or {}) merged.update(updates) self.channel_metadata = merged flag_modified(self, "channel_metadata") class ChannelBinding(Base): """运行时绑定记录""" __tablename__ = "channel_bindings" __table_args__ = ( UniqueConstraint( "channel_type", "account_id", "binding_rule_hash", "session_key_pattern", name="uq_channel_bindings_rule", ), ) id = Column(UUID(as_uuid=True), primary_key=True, default=func.gen_random_uuid()) channel_type = Column(String(32), nullable=False) account_id = Column(String(128), nullable=False) binding_rule_hash = Column(String(64), nullable=False) agent_id = Column(String(80), nullable=False) session_key_pattern = Column(String(512), nullable=False, default="") match = Column(JSON_VALUE, nullable=True) session_override = Column(JSON_VALUE, nullable=True) created_by = Column(String(100), nullable=True) created_at = Column(DateTime(timezone=False), default=utc_now_naive) updated_at = Column(DateTime(timezone=False), default=utc_now_naive, onupdate=utc_now_naive) def to_dict(self) -> dict[str, Any]: return { "id": str(self.id) if self.id else None, "channel_type": self.channel_type, "account_id": self.account_id, "binding_rule_hash": self.binding_rule_hash, "agent_id": self.agent_id, "session_key_pattern": self.session_key_pattern, "match": self.match or {}, "session_override": self.session_override or {}, "created_by": self.created_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ChannelPairingRecord(Base): """配对码记录表""" __tablename__ = "channel_pairing_records" __table_args__ = ( Index( "ix_channel_pairing_records_lookup", "channel_type", "account_id", "peer_id", "status", ), ) id = Column(UUID(as_uuid=True), primary_key=True, default=func.gen_random_uuid()) channel_type = Column(String(32), nullable=False) account_id = Column(String(128), nullable=False) peer_id = Column(String(255), nullable=False) pairing_code = Column(String(6), nullable=False) pairing_token = Column(String(64), nullable=False) status = Column(String(20), nullable=False, default="pending") expires_at = Column(DateTime(timezone=False), nullable=False) paired_at = Column(DateTime(timezone=False), nullable=True) platform_user_id = Column(String(255), nullable=True) qr_content = Column(String(2048), nullable=True) pairing_mode = Column(String(32), nullable=False, default="code") created_at = Column(DateTime(timezone=False), default=utc_now_naive) def to_dict(self) -> dict[str, Any]: return { "id": str(self.id) if self.id else None, "channel_type": self.channel_type, "account_id": self.account_id, "peer_id": self.peer_id, "pairing_code": self.pairing_code, "pairing_token": self.pairing_token, "status": self.status, "expires_at": format_utc_datetime(self.expires_at), "paired_at": format_utc_datetime(self.paired_at), "platform_user_id": self.platform_user_id, "qr_content": self.qr_content, "pairing_mode": self.pairing_mode, "created_at": format_utc_datetime(self.created_at), } class ChannelIdentityLink(Base): """渠道用户身份与平台用户身份的绑定关系""" __tablename__ = "channel_identity_links" __table_args__ = ( UniqueConstraint( "channel_type", "account_id", "channel_sender_id", name="uq_channel_identity_link", ), Index("ix_channel_identity_links_user", "platform_user_id"), ) id = Column(UUID(as_uuid=True), primary_key=True, default=func.gen_random_uuid()) channel_type = Column(String(32), nullable=False) account_id = Column(String(128), nullable=False) channel_sender_id = Column(String(255), nullable=False) platform_user_id = Column(String(255), nullable=False) paired_by = Column(String(32), nullable=False, default="code") created_at = Column(DateTime(timezone=False), default=utc_now_naive) updated_at = Column(DateTime(timezone=False), default=utc_now_naive, onupdate=utc_now_naive) def to_dict(self) -> dict[str, Any]: return { "id": str(self.id) if self.id else None, "channel_type": self.channel_type, "account_id": self.account_id, "channel_sender_id": self.channel_sender_id, "platform_user_id": self.platform_user_id, "paired_by": self.paired_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ChannelLoginRecord(Base): """渠道二维码登录记录表""" __tablename__ = "channel_login_records" __table_args__ = ( UniqueConstraint("channel_type", "account_id", name="uq_channel_login_records_type_account"), Index("ix_channel_login_records_status", "status"), Index("ix_channel_login_records_ticket", "current_ticket"), ) id = Column(UUID(as_uuid=True), primary_key=True, default=func.gen_random_uuid()) channel_type = Column(String(32), nullable=False) account_id = Column(String(128), nullable=False) status = Column(String(32), nullable=False, default=LoginState.PENDING.value) current_ticket = Column(String(512), nullable=True) qr_content = Column(String(2048), nullable=True) qr_image_url = Column(String(2048), nullable=True) qr_image_base64 = Column(Text, nullable=True) credentials = Column(JSON_VALUE, nullable=True) credential_expires_at = Column(DateTime(timezone=False), nullable=True) expires_at = Column(DateTime(timezone=False), nullable=True) scanned_at = Column(DateTime(timezone=False), nullable=True) logged_in_at = Column(DateTime(timezone=False), nullable=True) error_message = Column(String(1024), nullable=True) created_at = Column(DateTime(timezone=False), default=utc_now_naive) updated_at = Column(DateTime(timezone=False), default=utc_now_naive, onupdate=utc_now_naive) def to_dict(self) -> dict[str, Any]: return { "id": str(self.id) if self.id else None, "channel_type": self.channel_type, "account_id": self.account_id, "status": self.status, "current_ticket": self.current_ticket, "qr_content": self.qr_content, "qr_image_url": self.qr_image_url, "qr_image_base64": self.qr_image_base64, "credentials": self.credentials or {}, "credential_expires_at": format_utc_datetime(self.credential_expires_at), "expires_at": format_utc_datetime(self.expires_at), "scanned_at": format_utc_datetime(self.scanned_at), "logged_in_at": format_utc_datetime(self.logged_in_at), "error_message": self.error_message, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), }