main
mula.liu 2026-08-03 16:25:47 +08:00
parent 06d58be444
commit 194928e0e1
17 changed files with 553 additions and 1 deletions

2
.gitignore vendored
View File

@ -33,7 +33,7 @@ logs/
*.temp *.temp
# Local models # Local models
models backend/models
# AI # AI
.gemini-clipboard/ .gemini-clipboard/

View File

@ -0,0 +1,37 @@
"""
导出所有数据库模型
"""
from app.core.database import Base
from app.models.user import User
from app.models.role import Role, UserRole
from app.models.menu import SystemMenu, RoleMenu
from app.models.project import Project, ProjectMember, ProjectMemberRole
from app.models.document import DocumentMeta
from app.models.document_vector import DocumentVector
from app.models.share import ShareLink
from app.models.log import OperationLog
from app.models.mcp_bot import MCPBot
from app.models.llm_model_config import LLMModelConfig
from app.models.chat_session import ChatSession, ChatMessage
from app.models.project_vectorization_task import ProjectVectorizationTask
__all__ = [
"Base",
"User",
"Role",
"UserRole",
"SystemMenu",
"RoleMenu",
"Project",
"ProjectMember",
"ProjectMemberRole",
"DocumentMeta",
"DocumentVector",
"ShareLink",
"OperationLog",
"MCPBot",
"LLMModelConfig",
"ChatSession",
"ChatMessage",
"ProjectVectorizationTask",
]

View File

@ -0,0 +1,44 @@
"""
知识库对话会话模型
"""
from sqlalchemy import Column, BigInteger, String, Integer, DateTime, Text, Boolean
from sqlalchemy.sql import func
from app.core.database import Base
class ChatSession(Base):
"""对话会话表"""
__tablename__ = "chat_session"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="会话ID")
project_id = Column(BigInteger, nullable=False, index=True, comment="项目ID")
user_id = Column(BigInteger, nullable=False, index=True, comment="用户ID")
llm_config_id = Column(BigInteger, nullable=False, comment="LLM配置ID")
title = Column(String(255), nullable=False, comment="会话标题")
description = Column(Text, comment="会话描述")
is_active = Column(Boolean, nullable=False, default=True, comment="是否激活")
message_count = Column(Integer, nullable=False, default=0, comment="消息数")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
def __repr__(self):
return f"<ChatSession(id={self.id}, project_id={self.project_id}, user_id={self.user_id})>"
class ChatMessage(Base):
"""对话消息表"""
__tablename__ = "chat_message"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="消息ID")
session_id = Column(BigInteger, nullable=False, index=True, comment="会话ID")
role = Column(String(32), nullable=False, comment="角色(user/assistant)")
content = Column(Text, nullable=False, comment="消息内容")
referenced_files = Column(Text, comment="参考文件(JSON数组)")
tokens_used = Column(Integer, comment="消耗的token数")
is_deleted = Column(Boolean, nullable=False, default=False, comment="是否已删除")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
def __repr__(self):
return f"<ChatMessage(id={self.id}, session_id={self.session_id}, role='{self.role}')>"

View File

@ -0,0 +1,28 @@
"""
文档元数据模型
"""
from sqlalchemy import Column, BigInteger, String, Integer, DateTime
from sqlalchemy.sql import func
from app.core.database import Base
class DocumentMeta(Base):
"""文档元数据表模型"""
__tablename__ = "document_meta"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="元数据ID")
project_id = Column(BigInteger, nullable=False, index=True, comment="项目ID")
file_path = Column(String(500), nullable=False, comment="文件相对路径")
title = Column(String(200), comment="文档标题")
tags = Column(String(500), comment="标签JSON数组")
author_id = Column(BigInteger, index=True, comment="作者ID")
word_count = Column(Integer, default=0, comment="字数统计")
view_count = Column(Integer, default=0, comment="浏览次数")
last_editor_id = Column(BigInteger, comment="最后编辑者ID")
last_edited_at = Column(DateTime, comment="最后编辑时间")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
def __repr__(self):
return f"<DocumentMeta(id={self.id}, project_id={self.project_id}, file_path='{self.file_path}')>"

View File

@ -0,0 +1,36 @@
"""
文档向量化模型
"""
from sqlalchemy import Column, BigInteger, Integer, String, DateTime, Index, Text
from sqlalchemy.sql import func
from app.core.database import Base
class DocumentVector(Base):
"""文档向量表模型"""
__tablename__ = "document_vector"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="向量ID")
project_id = Column(BigInteger, nullable=False, index=True, comment="项目ID")
file_path = Column(String(500), nullable=False, comment="文件相对路径")
chunk_index = Column(Integer, nullable=False, default=0, comment="分块序号0起同一文件可有多个分块")
chunk_text = Column(Text, comment="分块首段文本,作为点击引用时的定位锚点")
content_hash = Column(String(64), comment="整个文件内容哈希值,用于判断文件是否变更")
zvec_id = Column(String(256), comment="ZVec返回的向量ID每个分块独立")
zvec_response = Column(Text, comment="ZVec完整响应JSON")
status = Column(String(32), nullable=False, default="success", comment="向量化状态success/failed/pending")
error_message = Column(String(500), comment="错误信息")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
__table_args__ = (
Index("idx_project_file", "project_id", "file_path"),
Index("idx_project_file_chunk", "project_id", "file_path", "chunk_index"),
)
def __repr__(self):
return (
f"<DocumentVector(id={self.id}, project_id={self.project_id}, "
f"file_path='{self.file_path}', chunk_index={self.chunk_index}, status='{self.status}')>"
)

View File

@ -0,0 +1,30 @@
"""
项目Git仓库模型
"""
from sqlalchemy import Column, BigInteger, String, Integer, DateTime, SmallInteger, ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.core.database import Base
class ProjectGitRepo(Base):
"""项目Git仓库表模型"""
__tablename__ = "project_git_repos"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="ID")
project_id = Column(BigInteger, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True, comment="项目ID")
name = Column(String(50), nullable=False, comment="仓库别名")
repo_url = Column(String(255), nullable=False, comment="Git仓库地址")
branch = Column(String(50), default="main", comment="Git分支")
username = Column(String(100), comment="Git用户名")
token = Column(String(255), comment="Git访问令牌/密码")
is_default = Column(SmallInteger, default=0, comment="是否默认仓库")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
# 关系
# project = relationship("Project", back_populates="git_repos")
def __repr__(self):
return f"<ProjectGitRepo(id={self.id}, name='{self.name}', repo_url='{self.repo_url}')>"

View File

@ -0,0 +1,63 @@
"""
LLM 模型配置模型
"""
from sqlalchemy import Column, BigInteger, String, Integer, DateTime, Boolean, JSON
from sqlalchemy.sql import func
from app.core.database import Base
class LLMModelConfig(Base):
"""大模型配置表模型"""
__tablename__ = "llm_model_config"
config_id = Column(BigInteger, primary_key=True, autoincrement=True, comment="配置ID")
model_code = Column(String(128), nullable=False, unique=True, index=True, comment="模型编码")
model_name = Column(String(255), nullable=False, comment="模型名称")
model_type = Column(String(32), nullable=False, default="chat", index=True, comment="模型类型: chat/embedding")
provider = Column(String(64), comment="模型提供方")
endpoint_url = Column(String(512), comment="接口地址")
api_key = Column(String(512), comment="API Key")
llm_model_name = Column(String(128), nullable=False, comment="模型名称/部署名")
llm_timeout = Column(Integer, nullable=False, default=120, comment="超时时间(秒)")
type_config = Column(JSON, nullable=False, default=dict, comment="模型类型差异参数")
description = Column(String(500), comment="描述")
is_active = Column(Boolean, nullable=False, default=True, index=True, comment="是否启用")
is_default = Column(Boolean, nullable=False, default=False, comment="是否默认")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
def _type_config_value(self, key, default=None):
return (self.type_config or {}).get(key, default)
@property
def llm_temperature(self):
return self._type_config_value("temperature", 0.70)
@property
def llm_top_p(self):
return self._type_config_value("top_p", 0.90)
@property
def llm_max_tokens(self):
return self._type_config_value("max_tokens", 8192)
@property
def llm_system_prompt(self):
return self._type_config_value("system_prompt")
@property
def embedding_dimension(self):
return self._type_config_value("dimension")
@property
def chunk_size(self):
return self._type_config_value("chunk_size", 800)
@property
def chunk_overlap(self):
return self._type_config_value("chunk_overlap", 150)
def __repr__(self):
return f"<LLMModelConfig(config_id={self.config_id}, model_code='{self.model_code}')>"

View File

@ -0,0 +1,28 @@
"""
操作日志模型
"""
from sqlalchemy import Column, BigInteger, String, Integer, DateTime, SmallInteger, Text
from sqlalchemy.sql import func
from app.core.database import Base
class OperationLog(Base):
"""操作日志表模型"""
__tablename__ = "operation_logs"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="日志ID")
user_id = Column(BigInteger, index=True, comment="操作用户ID")
username = Column(String(50), comment="用户名")
operation_type = Column(String(50), nullable=False, comment="操作类型")
resource_type = Column(String(50), nullable=False, index=True, comment="资源类型")
resource_id = Column(BigInteger, index=True, comment="资源ID")
detail = Column(Text, comment="操作详情JSON")
ip_address = Column(String(50), comment="IP地址")
user_agent = Column(String(500), comment="用户代理")
status = Column(SmallInteger, default=1, comment="状态0-失败 1-成功")
error_message = Column(Text, comment="错误信息")
created_at = Column(DateTime, server_default=func.now(), index=True, comment="操作时间")
def __repr__(self):
return f"<OperationLog(id={self.id}, operation_type='{self.operation_type}')>"

View File

@ -0,0 +1,25 @@
"""
MCP bot credential model.
"""
from sqlalchemy import Column, BigInteger, String, DateTime, SmallInteger
from sqlalchemy.sql import func
from app.core.database import Base
class MCPBot(Base):
"""Stores MCP access credentials mapped to a NexDocs user."""
__tablename__ = "mcp_bots"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="Bot credential ID")
user_id = Column(BigInteger, nullable=False, unique=True, index=True, comment="Owner user ID")
bot_id = Column(String(64), nullable=False, unique=True, index=True, comment="External MCP bot id")
bot_secret = Column(String(255), nullable=False, comment="External MCP bot secret")
status = Column(SmallInteger, default=1, index=True, comment="Status: 0-disabled 1-enabled")
last_used_at = Column(DateTime, comment="Last successful MCP access time")
created_at = Column(DateTime, server_default=func.now(), comment="Created at")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="Updated at")
def __repr__(self):
return f"<MCPBot(user_id={self.user_id}, bot_id='{self.bot_id}')>"

View File

@ -0,0 +1,44 @@
"""
菜单模型
"""
from sqlalchemy import Column, BigInteger, String, Integer, DateTime, SmallInteger
from sqlalchemy.sql import func
from app.core.database import Base
class SystemMenu(Base):
"""系统菜单表模型"""
__tablename__ = "system_menus"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="菜单ID")
parent_id = Column(BigInteger, default=0, comment="父菜单ID0表示根菜单")
menu_name = Column(String(50), nullable=False, comment="菜单名称")
menu_code = Column(String(50), nullable=False, unique=True, index=True, comment="菜单编码")
menu_type = Column(SmallInteger, nullable=False, comment="菜单类型1-目录 2-菜单 3-按钮/权限点")
path = Column(String(255), comment="路由路径")
component = Column(String(255), comment="组件路径")
icon = Column(String(100), comment="图标")
sort_order = Column(Integer, default=0, comment="排序号")
visible = Column(SmallInteger, default=1, comment="是否可见0-隐藏 1-显示")
status = Column(SmallInteger, default=1, index=True, comment="状态0-禁用 1-启用")
permission = Column(String(100), comment="权限字符串")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
def __repr__(self):
return f"<SystemMenu(id={self.id}, menu_name='{self.menu_name}')>"
class RoleMenu(Base):
"""角色菜单授权表模型"""
__tablename__ = "role_menus"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="关联ID")
role_id = Column(BigInteger, nullable=False, index=True, comment="角色ID")
menu_id = Column(BigInteger, nullable=False, index=True, comment="菜单ID")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
def __repr__(self):
return f"<RoleMenu(role_id={self.role_id}, menu_id={self.menu_id})>"

View File

@ -0,0 +1,26 @@
"""
通知模型
"""
from sqlalchemy import Column, BigInteger, String, DateTime, SmallInteger, Text, ForeignKey
from sqlalchemy.sql import func
from app.core.database import Base
class Notification(Base):
"""用户通知表模型"""
__tablename__ = "notifications"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="通知ID")
user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, comment="接收用户ID")
type = Column(String(20), default="info", comment="类型info, success, warning, error")
category = Column(String(50), default="system", comment="分类system, project, collaboration")
title = Column(String(200), nullable=False, comment="标题")
content = Column(Text, comment="内容")
link = Column(String(255), comment="跳转链接")
is_read = Column(SmallInteger, default=0, comment="是否已读0-未读 1-已读")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
read_at = Column(DateTime, comment="阅读时间")
def __repr__(self):
return f"<Notification(id={self.id}, user_id={self.user_id}, title='{self.title}')>"

View File

@ -0,0 +1,59 @@
"""
项目模型
"""
from sqlalchemy import Column, BigInteger, String, Integer, DateTime, SmallInteger, Enum
from sqlalchemy.sql import func
from app.core.database import Base
import enum
class ProjectMemberRole(str, enum.Enum):
"""项目成员角色枚举"""
ADMIN = "admin"
EDITOR = "editor"
VIEWER = "viewer"
class Project(Base):
"""项目表模型"""
__tablename__ = "projects"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="项目ID")
name = Column(String(100), nullable=False, index=True, comment="项目名称")
description = Column(String(500), comment="项目描述")
storage_key = Column(String(36), nullable=False, unique=True, comment="磁盘存储UUID")
owner_id = Column(BigInteger, nullable=False, index=True, comment="项目所有者ID")
is_public = Column(SmallInteger, default=0, comment="是否公开0-私有 1-公开")
is_template = Column(SmallInteger, default=0, comment="是否模板项目0-否 1-是")
status = Column(SmallInteger, default=1, index=True, comment="状态0-归档 1-活跃")
cover_image = Column(String(255), comment="封面图")
sort_order = Column(Integer, default=0, comment="排序号")
visit_count = Column(Integer, default=0, comment="访问次数")
access_pass = Column(String(100), comment="访问密码(用于分享链接)")
created_at = Column(DateTime, server_default=func.now(), index=True, comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
def __repr__(self):
return f"<Project(id={self.id}, name='{self.name}')>"
class ProjectMember(Base):
"""项目成员表模型"""
__tablename__ = "project_members"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="成员ID")
project_id = Column(BigInteger, nullable=False, index=True, comment="项目ID")
user_id = Column(BigInteger, nullable=False, index=True, comment="用户ID")
role = Column(
String(20),
default="viewer",
index=True,
comment="项目角色: admin/editor/viewer"
)
invited_by = Column(BigInteger, comment="邀请人ID")
joined_at = Column(DateTime, server_default=func.now(), comment="加入时间")
def __repr__(self):
return f"<ProjectMember(project_id={self.project_id}, user_id={self.user_id}, role='{self.role}')>"

View File

@ -0,0 +1,37 @@
"""
项目向量化任务模型
"""
from sqlalchemy import BigInteger, Column, DateTime, Index, Integer, String, Text
from sqlalchemy.sql import func
from app.core.database import Base
class ProjectVectorizationTask(Base):
"""项目向量化后台任务表"""
__tablename__ = "project_vectorization_task"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="任务ID")
task_id = Column(String(64), nullable=False, unique=True, index=True, comment="任务唯一标识")
project_id = Column(BigInteger, nullable=False, index=True, comment="项目ID")
user_id = Column(BigInteger, nullable=False, index=True, comment="触发用户ID")
task_type = Column(String(32), nullable=False, comment="任务类型incremental/full")
status = Column(String(32), nullable=False, default="pending", index=True, comment="任务状态pending/running/success/failed")
total = Column(Integer, nullable=False, default=0, comment="文件总数")
processed = Column(Integer, nullable=False, default=0, comment="处理成功数")
skipped = Column(Integer, nullable=False, default=0, comment="跳过数")
failed = Column(Integer, nullable=False, default=0, comment="失败数")
error_message = Column(Text, comment="错误信息")
started_at = Column(DateTime, comment="开始时间")
finished_at = Column(DateTime, comment="完成时间")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
__table_args__ = (
Index("idx_vector_task_project_status", "project_id", "status"),
Index("idx_vector_task_created_at", "created_at"),
)
def __repr__(self):
return f"<ProjectVectorizationTask(task_id='{self.task_id}', project_id={self.project_id}, status='{self.status}')>"

View File

@ -0,0 +1,38 @@
"""
角色模型
"""
from sqlalchemy import Column, BigInteger, String, DateTime, SmallInteger
from sqlalchemy.sql import func
from app.core.database import Base
class Role(Base):
"""角色表模型"""
__tablename__ = "roles"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="角色ID")
role_name = Column(String(50), nullable=False, unique=True, comment="角色名称")
role_code = Column(String(50), nullable=False, unique=True, index=True, comment="角色编码")
description = Column(String(255), comment="角色描述")
status = Column(SmallInteger, default=1, index=True, comment="状态0-禁用 1-启用")
is_system = Column(SmallInteger, default=0, comment="是否系统角色0-否 1-是")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
def __repr__(self):
return f"<Role(id={self.id}, role_code='{self.role_code}')>"
class UserRole(Base):
"""用户角色关联表模型"""
__tablename__ = "user_roles"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="关联ID")
user_id = Column(BigInteger, nullable=False, index=True, comment="用户ID")
role_id = Column(BigInteger, nullable=False, index=True, comment="角色ID")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
def __repr__(self):
return f"<UserRole(user_id={self.user_id}, role_id={self.role_id})>"

View File

@ -0,0 +1,27 @@
"""
分享链接模型
"""
from sqlalchemy import Column, BigInteger, String, DateTime, SmallInteger
from sqlalchemy.sql import func
from app.core.database import Base
class ShareLink(Base):
"""项目分享/文件分享链接"""
__tablename__ = "share_links"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="分享ID")
project_id = Column(BigInteger, nullable=False, index=True, comment="项目ID")
share_type = Column(String(20), nullable=False, index=True, comment="分享类型: project/file")
share_code = Column(String(64), nullable=False, unique=True, index=True, comment="公开分享码")
file_path = Column(String(500), comment="文件路径,仅文件分享使用")
access_pass = Column(String(100), comment="访问密码")
created_by = Column(BigInteger, index=True, comment="创建人ID")
status = Column(SmallInteger, default=1, index=True, comment="状态0-禁用 1-启用")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
def __repr__(self):
return f"<ShareLink(id={self.id}, type='{self.share_type}', code='{self.share_code}')>"

View File

@ -0,0 +1,29 @@
"""
用户模型
"""
from sqlalchemy import Column, BigInteger, String, Integer, DateTime, SmallInteger
from sqlalchemy.sql import func
from app.core.database import Base
class User(Base):
"""用户表模型"""
__tablename__ = "users"
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="用户ID")
username = Column(String(50), nullable=False, unique=True, index=True, comment="用户名")
password_hash = Column(String(255), nullable=False, comment="密码哈希")
nickname = Column(String(50), comment="昵称")
email = Column(String(100), index=True, comment="邮箱")
phone = Column(String(20), comment="手机号")
avatar = Column(String(255), comment="头像URL")
status = Column(SmallInteger, default=1, index=True, comment="状态0-禁用 1-启用")
is_superuser = Column(SmallInteger, default=0, comment="是否超级管理员0-否 1-是")
last_login_at = Column(DateTime, comment="最后登录时间")
last_login_ip = Column(String(50), comment="最后登录IP")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
def __repr__(self):
return f"<User(id={self.id}, username='{self.username}')>"

View File

@ -49,3 +49,4 @@ sentence-transformers>=3.0,<6
weasyprint==61.2 weasyprint==61.2
pydyf<0.11.0 pydyf<0.11.0
mcp==1.26.0 mcp==1.26.0
zvec==0.6.0