421 lines
16 KiB
Python
421 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="")
|
|
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),
|
|
}
|