Files
huihuiSquare/digital-avatar-app/backend/models.py

418 lines
16 KiB
Python

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="")
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 three-second 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
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,
"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
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),
}