import uuid from sqlalchemy import ( BigInteger, Boolean, Column, DateTime, Float, Index, Integer, JSON, String, Text, UniqueConstraint, ) from sqlalchemy.sql import func from database import Base def _iso(dt): return dt.isoformat() if dt else None class Avatar(Base): __tablename__ = "avatars" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) owner_id = Column(String, default="", index=True) # 归属用户(会会 huihui_user_id);空=未归属/种子数据 name = Column(String, nullable=False) display_name = Column(String, default="") description = Column(Text, default="") photo_url = Column(String, default="") emoji = Column(String, default="🤖") status = Column(String, default="active") # active | inactive | training share_token = Column(String, nullable=True, default=None, unique=True, index=True) # 对外分享使用的不可猜测令牌 token_balance = Column(Integer, default=0) config = Column(JSON, default=dict) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) def to_dict(self): return { "id": self.id, "ownerId": self.owner_id, "name": self.name, "displayName": self.display_name, "description": self.description, "photoUrl": self.photo_url, "emoji": self.emoji, "status": self.status, "shareToken": self.share_token or "", "tokenBalance": self.token_balance, "config": self.config or {}, "createdAt": _iso(self.created_at), "updatedAt": _iso(self.updated_at), } class Authorization(Base): __tablename__ = "authorizations" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) avatar_id = Column(String, nullable=False, default="") target_type = Column(String, default="user") # user | organization | application target_id = Column(String, default="") target_name = Column(String, default="") permissions = Column(JSON, default=list) status = Column(String, default="active") # active | inactive takeover_enabled = Column(Boolean, default=False) # 是否开启分身接管 takeover_mode = Column(String, default="immediate") # immediate | delayed takeover_delay_seconds = Column(Integer, default=180) # 延迟秒数,默认 3 分钟 created_at = Column(DateTime, server_default=func.now()) def to_dict(self): return { "id": self.id, "avatarId": self.avatar_id, "targetType": self.target_type, "targetId": self.target_id, "targetName": self.target_name, "permissions": self.permissions or [], "status": self.status, "takeoverEnabled": self.takeover_enabled, "takeoverMode": self.takeover_mode, "takeoverDelaySeconds": self.takeover_delay_seconds, "createdAt": _iso(self.created_at), } class TakeoverCursor(Base): """Durable BOXIM polling cursor for one avatar owner.""" __tablename__ = "takeover_cursors" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) avatar_id = Column(String, nullable=False, unique=True, index=True) owner_id = Column(String, nullable=False, default="", index=True) boxim_owner_id = Column(String, default="") last_message_id = Column(String, default="0") initialized = Column(Boolean, default=False) last_polled_at = Column(DateTime) last_error = Column(Text, default="") created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) class TakeoverMessage(Base): """BOXIM message receipt used for audit, deduplication, and chat context.""" __tablename__ = "takeover_messages" __table_args__ = ( UniqueConstraint("owner_id", "boxim_message_id", name="uq_takeover_message_owner_boxim"), Index("ix_takeover_message_conversation", "owner_id", "peer_id", "send_time"), ) id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) avatar_id = Column(String, nullable=False, index=True) owner_id = Column(String, nullable=False, index=True) boxim_message_id = Column(String, nullable=False) boxim_local_id = Column(String, nullable=True) peer_id = Column(String, nullable=False, index=True) direction = Column(String, nullable=False) # incoming | outgoing message_type = Column(Integer, default=0) content = Column(Text, default="") attachment_id = Column(String, nullable=True) is_avatar = Column(Boolean, default=False) send_time = Column(DateTime, nullable=False) created_at = Column(DateTime, server_default=func.now()) class TakeoverReplyTask(Base): """Restart-safe delayed BOXIM reply task.""" __tablename__ = "takeover_reply_tasks" __table_args__ = ( UniqueConstraint("owner_id", "trigger_message_id", name="uq_takeover_task_owner_trigger"), Index("ix_takeover_task_due", "status", "scheduled_at"), Index("ix_takeover_task_conversation", "owner_id", "peer_id", "status"), ) id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) avatar_id = Column(String, nullable=False, index=True) owner_id = Column(String, nullable=False, index=True) peer_id = Column(String, nullable=False, index=True) trigger_message_id = Column(String, nullable=False) source_message_ids = Column(JSON, default=list) prompt = Column(Text, default="") response_text = Column(Text, default="") status = Column(String, default="pending") scheduled_at = Column(DateTime, nullable=False) locked_at = Column(DateTime) sent_at = Column(DateTime) attempts = Column(Integer, default=0) last_error = Column(Text, default="") cancel_reason = Column(String, default="") boxim_local_id = Column(String, nullable=False) boxim_sent_message_id = Column(String, default="") created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) class Organization(Base): __tablename__ = "organizations" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) name = Column(String, nullable=False) description = Column(Text, default="") emoji = Column(String, default="🏢") org_type = Column(String, default="team") # team | company | community role = Column(String, default="admin") # admin | member | viewer member_count = Column(Integer, default=1) created_at = Column(DateTime, server_default=func.now()) def to_dict(self): return { "id": self.id, "name": self.name, "description": self.description, "emoji": self.emoji, "type": self.org_type, "role": self.role, "memberCount": self.member_count, "createdAt": _iso(self.created_at), } class KnowledgeDoc(Base): __tablename__ = "knowledge_docs" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) avatar_id = Column(String, nullable=False, default="") filename = Column(String, default="") file_type = Column(String, default="") # pdf | doc | docx | xlsx file_size = Column(Integer, default=0) file_url = Column(String, default="") status = Column(String, default="uploaded") # uploaded | parsing | ready | failed error_message = Column(String, default="") # 建立索引失败原因 vectorized = Column(Boolean, default=False) # 是否已向量化 embedding_model = Column(String, default="") # 向量模型标识 chunk_count = Column(Integer, default=0) # 切片数量 vectorized_at = Column(DateTime) # 向量化时间 created_at = Column(DateTime, server_default=func.now()) def to_dict(self): return { "id": self.id, "avatarId": self.avatar_id, "filename": self.filename, "fileType": self.file_type, "fileSize": self.file_size, "fileUrl": self.file_url, "status": self.status, "errorMessage": self.error_message or "", "vectorized": bool(self.vectorized), "embeddingModel": self.embedding_model, "chunkCount": self.chunk_count, "vectorizedAt": _iso(self.vectorized_at), "createdAt": _iso(self.created_at), } class QAPair(Base): __tablename__ = "qa_pairs" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) avatar_id = Column(String, nullable=False, default="") question = Column(Text, default="") answer = Column(Text, default="") enabled = Column(Boolean, default=True) # 是否启用(关闭后不参与作答) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) def to_dict(self): return { "id": self.id, "avatarId": self.avatar_id, "question": self.question, "answer": self.answer, "enabled": bool(self.enabled), "createdAt": _iso(self.created_at), "updatedAt": _iso(self.updated_at), } class KnowledgeChunk(Base): __tablename__ = "knowledge_chunks" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) doc_id = Column(String, default="") # 关联 KnowledgeDoc.id avatar_id = Column(String, default="") content = Column(Text, default="") # 切片文本 vector = Column(Text, default="") # JSON 编码的向量 chunk_index = Column(Integer, default=0) embedding_model = Column(String, default="") created_at = Column(DateTime, server_default=func.now()) def to_dict(self): return { "id": self.id, "docId": self.doc_id, "avatarId": self.avatar_id, "content": self.content, "chunkIndex": self.chunk_index, "embeddingModel": self.embedding_model, "createdAt": _iso(self.created_at), } class ChatAttachment(Base): """Private, avatar-scoped result of one chat image analysis.""" __tablename__ = "chat_attachments" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) avatar_id = Column(String, nullable=False, default="", index=True) uploader_kind = Column(String, default="owner") # owner | public | boxim filename = Column(String, default="") mime_type = Column(String, default="") file_size = Column(Integer, default=0) status = Column(String, default="processing") # processing | ready | failed category = Column(String, default="general_image") summary = Column(Text, default="") extracted_text = Column(Text, default="") structured_data = Column(JSON, default=dict) warning = Column(Text, default="") vision_model = Column(String, default="") ocr_model = Column(String, default="") used_at = Column(DateTime) expires_at = Column(DateTime, nullable=False) created_at = Column(DateTime, server_default=func.now()) def to_dict(self): return { "id": self.id, "avatarId": self.avatar_id, "filename": self.filename, "mimeType": self.mime_type, "fileSize": self.file_size, "status": self.status, "category": self.category, "summary": self.summary, "warning": self.warning, "expiresAt": _iso(self.expires_at), "createdAt": _iso(self.created_at), } class TokenAccount(Base): __tablename__ = "token_account" id = Column(Integer, primary_key=True) user_id = Column(String, nullable=False, default="", index=True) balance = Column(BigInteger, default=1_000_000) total_granted = Column(BigInteger, default=1_000_000) total_consumed = Column(BigInteger, default=0) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) class TokenUsage(Base): __tablename__ = "token_usage" __table_args__ = ( Index("ix_token_usage_user_created", "user_id", "created_at"), Index("ix_token_usage_avatar_created", "avatar_id", "created_at"), ) id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) user_id = Column(String, nullable=False, index=True) avatar_id = Column(String, nullable=False, default="", index=True) source = Column(String, nullable=False, default="chat") model = Column(String, default="") status = Column(String, nullable=False, default="reserved") reserved_tokens = Column(BigInteger, default=0) prompt_tokens = Column(BigInteger, default=0) completion_tokens = Column(BigInteger, default=0) total_tokens = Column(BigInteger, default=0) balance_after = Column(BigInteger, default=0) failure_reason = Column(String, default="") created_at = Column(DateTime, server_default=func.now()) settled_at = Column(DateTime) class TokenPlan(Base): __tablename__ = "token_plans" id = Column(String, primary_key=True) name = Column(String, default="") amount = Column(BigInteger, default=0) price = Column(Float, default=0) badge = Column(String, default="") desc = Column(String, default="") def to_dict(self): return { "id": self.id, "name": self.name, "amount": self.amount, "price": self.price, "badge": self.badge, "desc": self.desc, } class TokenPaymentOrder(Base): __tablename__ = "token_payment_orders" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) order_no = Column(String, nullable=False, unique=True, index=True) user_id = Column(String, nullable=False, index=True) plan_id = Column(String, nullable=False) payment_method = Column(String, nullable=False) pay_type = Column(String, nullable=False) pay_way = Column(String, nullable=False) points_amount = Column(BigInteger, nullable=False) price_cents = Column(Integer, nullable=False) status = Column(String, nullable=False, default="pending", index=True) provider_order_id = Column(String, default="") provider_order_no = Column(String, default="") provider_status = Column(String, default="") pay_message = Column(Text, default="") failure_reason = Column(String, default="") created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) paid_at = Column(DateTime) def to_dict(self): return { "id": self.id, "orderNo": self.order_no, "planId": self.plan_id, "paymentMethod": self.payment_method, "payType": self.pay_type, "payWay": self.pay_way, "pointsAmount": self.points_amount, "price": self.price_cents / 100, "status": self.status, "providerStatus": self.provider_status, "payMessage": self.pay_message, "failureReason": self.failure_reason, "createdAt": _iso(self.created_at), "paidAt": _iso(self.paid_at), } class User(Base): """会会用户 ↔ 本地用户体系映射(短信验证码登录落库)""" __tablename__ = "users" id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex) huihui_user_id = Column(String, default="", index=True) # 会会 userId(唯一标识) phone = Column(String, default="", index=True) nickname = Column(String, default="") avatar_url = Column(String, default="") huihui_token = Column(String, default="") # 会会 access_token app_token = Column(String, default="") # 本系统会话 token last_login_at = Column(DateTime) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) def to_dict(self): return { "id": self.id, "huihuiUserId": self.huihui_user_id, "phone": self.phone, "nickname": self.nickname, "avatarUrl": self.avatar_url, "createdAt": _iso(self.created_at), "lastLoginAt": _iso(self.last_login_at), }