diff --git a/scripts/cleanup_models.py b/scripts/cleanup_models.py new file mode 100644 index 000000000..0b09c4015 --- /dev/null +++ b/scripts/cleanup_models.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""清理 core/models.py,只保留模型定义""" + +import os + +# 文件路径 +models_file = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "src", + "common", + "database", + "core", + "models.py" +) + +print(f"正在清理文件: {models_file}") + +# 读取文件 +with open(models_file, "r", encoding="utf-8") as f: + lines = f.readlines() + +# 找到最后一个模型类的结束位置(MonthlyPlan的 __table_args__ 结束) +# 我们要保留到第593行(包含) +keep_lines = [] +found_end = False + +for i, line in enumerate(lines, 1): + keep_lines.append(line) + + # 检查是否到达 MonthlyPlan 的 __table_args__ 结束 + if i > 580 and line.strip() == ")": + # 再检查前一行是否有 Index 相关内容 + if "idx_monthlyplan" in "".join(lines[max(0, i-5):i]): + print(f"找到模型定义结束位置: 第 {i} 行") + found_end = True + break + +if not found_end: + print("❌ 未找到模型定义结束标记") + exit(1) + +# 写回文件 +with open(models_file, "w", encoding="utf-8") as f: + f.writelines(keep_lines) + +print(f"✅ 文件清理完成") +print(f"保留行数: {len(keep_lines)}") +print(f"原始行数: {len(lines)}") +print(f"删除行数: {len(lines) - len(keep_lines)}") diff --git a/scripts/extract_models.py b/scripts/extract_models.py new file mode 100644 index 000000000..2eba4adaf --- /dev/null +++ b/scripts/extract_models.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""提取models.py中的模型定义""" + +import re + +# 读取原始文件 +with open('src/common/database/sqlalchemy_models.py', 'r', encoding='utf-8') as f: + content = f.read() + +# 找到get_string_field函数的开始和结束 +get_string_field_start = content.find('# MySQL兼容的字段类型辅助函数') +get_string_field_end = content.find('\n\nclass ChatStreams(Base):') +get_string_field = content[get_string_field_start:get_string_field_end] + +# 找到第一个class定义开始 +first_class_pos = content.find('class ChatStreams(Base):') + +# 找到所有class定义,直到遇到非class的def +# 简单策略:找到所有以"class "开头且继承Base的类 +classes_pattern = r'class \w+\(Base\):.*?(?=\nclass \w+\(Base\):|$)' +matches = list(re.finditer(classes_pattern, content[first_class_pos:], re.DOTALL)) + +if matches: + # 取最后一个匹配的结束位置 + models_content = content[first_class_pos:first_class_pos + matches[-1].end()] +else: + # 备用方案:从第一个class到文件的85%位置 + models_end = int(len(content) * 0.85) + models_content = content[first_class_pos:models_end] + +# 创建新文件内容 +header = '''"""SQLAlchemy数据库模型定义 + +本文件只包含纯模型定义,使用SQLAlchemy 2.0的Mapped类型注解风格。 +引擎和会话管理已移至core/engine.py和core/session.py。 + +所有模型使用统一的类型注解风格: + field_name: Mapped[PyType] = mapped_column(Type, ...) + +这样IDE/Pylance能正确推断实例属性类型。 +""" + +import datetime +import time + +from sqlalchemy import Boolean, DateTime, Float, Index, Integer, String, Text +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Mapped, mapped_column + +# 创建基类 +Base = declarative_base() + + +''' + +new_content = header + get_string_field + '\n\n' + models_content + +# 写入新文件 +with open('src/common/database/core/models.py', 'w', encoding='utf-8') as f: + f.write(new_content) + +print('✅ Models file rewritten successfully') +print(f'File size: {len(new_content)} characters') +pattern = r"^class \w+\(Base\):" +model_count = len(re.findall(pattern, models_content, re.MULTILINE)) +print(f'Number of model classes: {model_count}') diff --git a/src/common/database/core/__init__.py b/src/common/database/core/__init__.py index e56500bd3..ca896467f 100644 --- a/src/common/database/core/__init__.py +++ b/src/common/database/core/__init__.py @@ -8,14 +8,79 @@ """ from .engine import close_engine, get_engine, get_engine_info +from .migration import check_and_migrate_database, create_all_tables, drop_all_tables +from .models import ( + ActionRecords, + AntiInjectionStats, + BanUser, + Base, + BotPersonalityInterests, + CacheEntries, + ChatStreams, + Emoji, + Expression, + get_string_field, + GraphEdges, + GraphNodes, + ImageDescriptions, + Images, + LLMUsage, + MaiZoneScheduleStatus, + Memory, + Messages, + MonthlyPlan, + OnlineTime, + PermissionNodes, + PersonInfo, + Schedule, + ThinkingLog, + UserPermissions, + UserRelationships, + Videos, +) from .session import get_db_session, get_db_session_direct, get_session_factory, reset_session_factory __all__ = [ + # Engine "get_engine", "close_engine", "get_engine_info", + # Session "get_db_session", "get_db_session_direct", "get_session_factory", "reset_session_factory", + # Migration + "check_and_migrate_database", + "create_all_tables", + "drop_all_tables", + # Models - Base + "Base", + "get_string_field", + # Models - Tables (按字母顺序) + "ActionRecords", + "AntiInjectionStats", + "BanUser", + "BotPersonalityInterests", + "CacheEntries", + "ChatStreams", + "Emoji", + "Expression", + "GraphEdges", + "GraphNodes", + "ImageDescriptions", + "Images", + "LLMUsage", + "MaiZoneScheduleStatus", + "Memory", + "Messages", + "MonthlyPlan", + "OnlineTime", + "PermissionNodes", + "PersonInfo", + "Schedule", + "ThinkingLog", + "UserPermissions", + "UserRelationships", + "Videos", ] diff --git a/src/common/database/core/migration.py b/src/common/database/core/migration.py new file mode 100644 index 000000000..eac6d0cde --- /dev/null +++ b/src/common/database/core/migration.py @@ -0,0 +1,230 @@ +"""数据库迁移模块 + +此模块负责数据库结构的自动检查和迁移: +- 自动创建不存在的表 +- 自动为现有表添加缺失的列 +- 自动为现有表创建缺失的索引 + +使用新架构的 engine 和 models +""" + +from sqlalchemy import inspect +from sqlalchemy.sql import text + +from src.common.database.core.engine import get_engine +from src.common.database.core.models import Base +from src.common.logger import get_logger + +logger = get_logger("db_migration") + + +async def check_and_migrate_database(existing_engine=None): + """异步检查数据库结构并自动迁移 + + 自动执行以下操作: + - 创建不存在的表 + - 为现有表添加缺失的列 + - 为现有表创建缺失的索引 + + Args: + existing_engine: 可选的已存在的数据库引擎。如果提供,将使用该引擎;否则获取全局引擎 + + Note: + 此函数是幂等的,可以安全地多次调用 + """ + logger.info("正在检查数据库结构并执行自动迁移...") + engine = existing_engine if existing_engine is not None else await get_engine() + + async with engine.connect() as connection: + # 在同步上下文中运行inspector操作 + def get_inspector(sync_conn): + return inspect(sync_conn) + + inspector = await connection.run_sync(get_inspector) + + # 获取数据库中已存在的表名 + db_table_names = await connection.run_sync( + lambda conn: set(inspector.get_table_names()) + ) + + # 1. 首先处理表的创建 + tables_to_create = [] + for table_name, table in Base.metadata.tables.items(): + if table_name not in db_table_names: + tables_to_create.append(table) + + if tables_to_create: + logger.info(f"发现 {len(tables_to_create)} 个不存在的表,正在创建...") + try: + # 一次性创建所有缺失的表 + await connection.run_sync( + lambda sync_conn: Base.metadata.create_all( + sync_conn, tables=tables_to_create + ) + ) + for table in tables_to_create: + logger.info(f"表 '{table.name}' 创建成功。") + db_table_names.add(table.name) # 将新创建的表添加到集合中 + + # 提交表创建事务 + await connection.commit() + except Exception as e: + logger.error(f"创建表时失败: {e}", exc_info=True) + await connection.rollback() + + # 2. 然后处理现有表的列和索引的添加 + for table_name, table in Base.metadata.tables.items(): + if table_name not in db_table_names: + logger.warning( + f"跳过检查表 '{table_name}',因为它在创建步骤中可能已失败。" + ) + continue + + logger.debug(f"正在检查表 '{table_name}' 的列和索引...") + + try: + # 检查并添加缺失的列 + db_columns = await connection.run_sync( + lambda conn: { + col["name"] for col in inspector.get_columns(table_name) + } + ) + model_columns = {col.name for col in table.c} + missing_columns = model_columns - db_columns + + if missing_columns: + logger.info( + f"在表 '{table_name}' 中发现缺失的列: {', '.join(missing_columns)}" + ) + + def add_columns_sync(conn): + dialect = conn.dialect + compiler = dialect.ddl_compiler(dialect, None) + + for column_name in missing_columns: + column = table.c[column_name] + column_type = compiler.get_column_specification(column) + sql = f"ALTER TABLE {table.name} ADD COLUMN {column.name} {column_type}" + + if column.default: + # 手动处理不同方言的默认值 + default_arg = column.default.arg + if dialect.name == "sqlite" and isinstance( + default_arg, bool + ): + # SQLite 将布尔值存储为 0 或 1 + default_value = "1" if default_arg else "0" + elif hasattr(compiler, "render_literal_value"): + try: + # 尝试使用 render_literal_value + default_value = compiler.render_literal_value( + default_arg, column.type + ) + except AttributeError: + # 如果失败,则回退到简单的字符串转换 + default_value = ( + f"'{default_arg}'" + if isinstance(default_arg, str) + else str(default_arg) + ) + else: + # 对于没有 render_literal_value 的旧版或特定方言 + default_value = ( + f"'{default_arg}'" + if isinstance(default_arg, str) + else str(default_arg) + ) + + sql += f" DEFAULT {default_value}" + + if not column.nullable: + sql += " NOT NULL" + + conn.execute(text(sql)) + logger.info(f"成功向表 '{table_name}' 添加列 '{column_name}'。") + + await connection.run_sync(add_columns_sync) + # 提交列添加事务 + await connection.commit() + else: + logger.info(f"表 '{table_name}' 的列结构一致。") + + # 检查并创建缺失的索引 + db_indexes = await connection.run_sync( + lambda conn: { + idx["name"] for idx in inspector.get_indexes(table_name) + } + ) + model_indexes = {idx.name for idx in table.indexes} + missing_indexes = model_indexes - db_indexes + + if missing_indexes: + logger.info( + f"在表 '{table_name}' 中发现缺失的索引: {', '.join(missing_indexes)}" + ) + + def add_indexes_sync(conn): + for index_name in missing_indexes: + index_obj = next( + (idx for idx in table.indexes if idx.name == index_name), + None, + ) + if index_obj is not None: + index_obj.create(conn) + logger.info( + f"成功为表 '{table_name}' 创建索引 '{index_name}'。" + ) + + await connection.run_sync(add_indexes_sync) + # 提交索引创建事务 + await connection.commit() + else: + logger.debug(f"表 '{table_name}' 的索引一致。") + + except Exception as e: + logger.error(f"在处理表 '{table_name}' 时发生意外错误: {e}", exc_info=True) + await connection.rollback() + continue + + logger.info("数据库结构检查与自动迁移完成。") + + +async def create_all_tables(existing_engine=None): + """创建所有表(不进行迁移检查) + + 直接创建所有在 Base.metadata 中定义的表。 + 如果表已存在,将被跳过。 + + Args: + existing_engine: 可选的已存在的数据库引擎 + + Note: + 生产环境建议使用 check_and_migrate_database() + """ + logger.info("正在创建所有数据库表...") + engine = existing_engine if existing_engine is not None else await get_engine() + + async with engine.begin() as connection: + await connection.run_sync(Base.metadata.create_all) + + logger.info("数据库表创建完成。") + + +async def drop_all_tables(existing_engine=None): + """删除所有表(危险操作!) + + 删除所有在 Base.metadata 中定义的表。 + + Args: + existing_engine: 可选的已存在的数据库引擎 + + Warning: + 此操作将删除所有数据,不可恢复!仅用于测试环境! + """ + logger.warning("⚠️ 正在删除所有数据库表...") + engine = existing_engine if existing_engine is not None else await get_engine() + + async with engine.begin() as connection: + await connection.run_sync(Base.metadata.drop_all) + + logger.warning("所有数据库表已删除。") diff --git a/src/common/database/core/models.py b/src/common/database/core/models.py new file mode 100644 index 000000000..202eb9dbb --- /dev/null +++ b/src/common/database/core/models.py @@ -0,0 +1,652 @@ +"""SQLAlchemy数据库模型定义 + +本文件只包含纯模型定义,使用SQLAlchemy 2.0的Mapped类型注解风格。 +引擎和会话管理已移至core/engine.py和core/session.py。 + +所有模型使用统一的类型注解风格: + field_name: Mapped[PyType] = mapped_column(Type, ...) + +这样IDE/Pylance能正确推断实例属性类型。 +""" + +import datetime +import time + +from sqlalchemy import Boolean, DateTime, Float, Index, Integer, String, Text +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Mapped, mapped_column + +# 创建基类 +Base = declarative_base() + + +# MySQL兼容的字段类型辅助函数 +def get_string_field(max_length=255, **kwargs): + """ + 根据数据库类型返回合适的字符串字段 + MySQL需要指定长度的VARCHAR用于索引,SQLite可以使用Text + """ + from src.config.config import global_config + + if global_config.database.database_type == "mysql": + return String(max_length, **kwargs) + else: + return Text(**kwargs) + + +class ChatStreams(Base): + """聊天流模型""" + + __tablename__ = "chat_streams" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + stream_id: Mapped[str] = mapped_column(get_string_field(64), nullable=False, unique=True, index=True) + create_time: Mapped[float] = mapped_column(Float, nullable=False) + group_platform: Mapped[str | None] = mapped_column(Text, nullable=True) + group_id: Mapped[str | None] = mapped_column(get_string_field(100), nullable=True, index=True) + group_name: Mapped[str | None] = mapped_column(Text, nullable=True) + last_active_time: Mapped[float] = mapped_column(Float, nullable=False) + platform: Mapped[str] = mapped_column(Text, nullable=False) + user_platform: Mapped[str] = mapped_column(Text, nullable=False) + user_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, index=True) + user_nickname: Mapped[str] = mapped_column(Text, nullable=False) + user_cardname: Mapped[str | None] = mapped_column(Text, nullable=True) + energy_value: Mapped[float | None] = mapped_column(Float, nullable=True, default=5.0) + sleep_pressure: Mapped[float | None] = mapped_column(Float, nullable=True, default=0.0) + focus_energy: Mapped[float | None] = mapped_column(Float, nullable=True, default=0.5) + # 动态兴趣度系统字段 + base_interest_energy: Mapped[float | None] = mapped_column(Float, nullable=True, default=0.5) + message_interest_total: Mapped[float | None] = mapped_column(Float, nullable=True, default=0.0) + message_count: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0) + action_count: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0) + reply_count: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0) + last_interaction_time: Mapped[float | None] = mapped_column(Float, nullable=True, default=None) + consecutive_no_reply: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0) + # 消息打断系统字段 + interruption_count: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0) + # 聊天流印象字段 + stream_impression_text: Mapped[str | None] = mapped_column(Text, nullable=True) # 对聊天流的主观印象描述 + stream_chat_style: Mapped[str | None] = mapped_column(Text, nullable=True) # 聊天流的总体风格 + stream_topic_keywords: Mapped[str | None] = mapped_column(Text, nullable=True) # 话题关键词,逗号分隔 + stream_interest_score: Mapped[float | None] = mapped_column(Float, nullable=True, default=0.5) # 对聊天流的兴趣程度(0-1) + + __table_args__ = ( + Index("idx_chatstreams_stream_id", "stream_id"), + Index("idx_chatstreams_user_id", "user_id"), + Index("idx_chatstreams_group_id", "group_id"), + ) + + +class LLMUsage(Base): + """LLM使用记录模型""" + + __tablename__ = "llm_usage" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + model_name: Mapped[str] = mapped_column(get_string_field(100), nullable=False, index=True) + model_assign_name: Mapped[str] = mapped_column(get_string_field(100), index=True) + model_api_provider: Mapped[str] = mapped_column(get_string_field(100), index=True) + user_id: Mapped[str] = mapped_column(get_string_field(50), nullable=False, index=True) + request_type: Mapped[str] = mapped_column(get_string_field(50), nullable=False, index=True) + endpoint: Mapped[str] = mapped_column(Text, nullable=False) + prompt_tokens: Mapped[int] = mapped_column(Integer, nullable=False) + completion_tokens: Mapped[int] = mapped_column(Integer, nullable=False) + time_cost: Mapped[float | None] = mapped_column(Float, nullable=True) + total_tokens: Mapped[int] = mapped_column(Integer, nullable=False) + cost: Mapped[float] = mapped_column(Float, nullable=False) + status: Mapped[str] = mapped_column(Text, nullable=False) + timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, index=True, default=datetime.datetime.now) + + __table_args__ = ( + Index("idx_llmusage_model_name", "model_name"), + Index("idx_llmusage_model_assign_name", "model_assign_name"), + Index("idx_llmusage_model_api_provider", "model_api_provider"), + Index("idx_llmusage_time_cost", "time_cost"), + Index("idx_llmusage_user_id", "user_id"), + Index("idx_llmusage_request_type", "request_type"), + Index("idx_llmusage_timestamp", "timestamp"), + ) + + +class Emoji(Base): + """表情包模型""" + + __tablename__ = "emoji" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + full_path: Mapped[str] = mapped_column(get_string_field(500), nullable=False, unique=True, index=True) + format: Mapped[str] = mapped_column(Text, nullable=False) + emoji_hash: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True) + description: Mapped[str] = mapped_column(Text, nullable=False) + query_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + is_registered: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + is_banned: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + emotion: Mapped[str | None] = mapped_column(Text, nullable=True) + record_time: Mapped[float] = mapped_column(Float, nullable=False) + register_time: Mapped[float | None] = mapped_column(Float, nullable=True) + usage_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + last_used_time: Mapped[float | None] = mapped_column(Float, nullable=True) + + __table_args__ = ( + Index("idx_emoji_full_path", "full_path"), + Index("idx_emoji_hash", "emoji_hash"), + ) + + +class Messages(Base): + """消息模型""" + + __tablename__ = "messages" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + message_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, index=True) + time: Mapped[float] = mapped_column(Float, nullable=False) + chat_id: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True) + reply_to: Mapped[str | None] = mapped_column(Text, nullable=True) + interest_value: Mapped[float | None] = mapped_column(Float, nullable=True) + key_words: Mapped[str | None] = mapped_column(Text, nullable=True) + key_words_lite: Mapped[str | None] = mapped_column(Text, nullable=True) + is_mentioned: Mapped[bool | None] = mapped_column(Boolean, nullable=True) + + # 从 chat_info 扁平化而来的字段 + chat_info_stream_id: Mapped[str] = mapped_column(Text, nullable=False) + chat_info_platform: Mapped[str] = mapped_column(Text, nullable=False) + chat_info_user_platform: Mapped[str] = mapped_column(Text, nullable=False) + chat_info_user_id: Mapped[str] = mapped_column(Text, nullable=False) + chat_info_user_nickname: Mapped[str] = mapped_column(Text, nullable=False) + chat_info_user_cardname: Mapped[str | None] = mapped_column(Text, nullable=True) + chat_info_group_platform: Mapped[str | None] = mapped_column(Text, nullable=True) + chat_info_group_id: Mapped[str | None] = mapped_column(Text, nullable=True) + chat_info_group_name: Mapped[str | None] = mapped_column(Text, nullable=True) + chat_info_create_time: Mapped[float] = mapped_column(Float, nullable=False) + chat_info_last_active_time: Mapped[float] = mapped_column(Float, nullable=False) + + # 从顶层 user_info 扁平化而来的字段 + user_platform: Mapped[str | None] = mapped_column(Text, nullable=True) + user_id: Mapped[str | None] = mapped_column(get_string_field(100), nullable=True, index=True) + user_nickname: Mapped[str | None] = mapped_column(Text, nullable=True) + user_cardname: Mapped[str | None] = mapped_column(Text, nullable=True) + + processed_plain_text: Mapped[str | None] = mapped_column(Text, nullable=True) + display_message: Mapped[str | None] = mapped_column(Text, nullable=True) + memorized_times: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + priority_mode: Mapped[str | None] = mapped_column(Text, nullable=True) + priority_info: Mapped[str | None] = mapped_column(Text, nullable=True) + additional_config: Mapped[str | None] = mapped_column(Text, nullable=True) + is_emoji: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + is_picid: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + is_command: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + is_notify: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + is_public_notice: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + notice_type: Mapped[str | None] = mapped_column(String(50), nullable=True) + + # 兴趣度系统字段 + actions: Mapped[str | None] = mapped_column(Text, nullable=True) + should_reply: Mapped[bool | None] = mapped_column(Boolean, nullable=True, default=False) + should_act: Mapped[bool | None] = mapped_column(Boolean, nullable=True, default=False) + + __table_args__ = ( + Index("idx_messages_message_id", "message_id"), + Index("idx_messages_chat_id", "chat_id"), + Index("idx_messages_time", "time"), + Index("idx_messages_user_id", "user_id"), + Index("idx_messages_should_reply", "should_reply"), + Index("idx_messages_should_act", "should_act"), + ) + + +class ActionRecords(Base): + """动作记录模型""" + + __tablename__ = "action_records" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + action_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, index=True) + time: Mapped[float] = mapped_column(Float, nullable=False) + action_name: Mapped[str] = mapped_column(Text, nullable=False) + action_data: Mapped[str] = mapped_column(Text, nullable=False) + action_done: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + action_build_into_prompt: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + action_prompt_display: Mapped[str] = mapped_column(Text, nullable=False) + chat_id: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True) + chat_info_stream_id: Mapped[str] = mapped_column(Text, nullable=False) + chat_info_platform: Mapped[str] = mapped_column(Text, nullable=False) + + __table_args__ = ( + Index("idx_actionrecords_action_id", "action_id"), + Index("idx_actionrecords_chat_id", "chat_id"), + Index("idx_actionrecords_time", "time"), + ) + + +class Images(Base): + """图像信息模型""" + + __tablename__ = "images" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + image_id: Mapped[str] = mapped_column(Text, nullable=False, default="") + emoji_hash: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + path: Mapped[str] = mapped_column(get_string_field(500), nullable=False, unique=True) + count: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + timestamp: Mapped[float] = mapped_column(Float, nullable=False) + type: Mapped[str] = mapped_column(Text, nullable=False) + vlm_processed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + + __table_args__ = ( + Index("idx_images_emoji_hash", "emoji_hash"), + Index("idx_images_path", "path"), + ) + + +class ImageDescriptions(Base): + """图像描述信息模型""" + + __tablename__ = "image_descriptions" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + type: Mapped[str] = mapped_column(Text, nullable=False) + image_description_hash: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True) + description: Mapped[str] = mapped_column(Text, nullable=False) + timestamp: Mapped[float] = mapped_column(Float, nullable=False) + + __table_args__ = (Index("idx_imagedesc_hash", "image_description_hash"),) + + +class Videos(Base): + """视频信息模型""" + + __tablename__ = "videos" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + video_id: Mapped[str] = mapped_column(Text, nullable=False, default="") + video_hash: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True, unique=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + count: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + timestamp: Mapped[float] = mapped_column(Float, nullable=False) + vlm_processed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + + # 视频特有属性 + duration: Mapped[float | None] = mapped_column(Float, nullable=True) + frame_count: Mapped[int | None] = mapped_column(Integer, nullable=True) + fps: Mapped[float | None] = mapped_column(Float, nullable=True) + resolution: Mapped[str | None] = mapped_column(Text, nullable=True) + file_size: Mapped[int | None] = mapped_column(Integer, nullable=True) + + __table_args__ = ( + Index("idx_videos_video_hash", "video_hash"), + Index("idx_videos_timestamp", "timestamp"), + ) + + +class OnlineTime(Base): + """在线时长记录模型""" + + __tablename__ = "online_time" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + timestamp: Mapped[str] = mapped_column(Text, nullable=False, default=str(datetime.datetime.now)) + duration: Mapped[int] = mapped_column(Integer, nullable=False) + start_timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now) + end_timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, index=True) + + __table_args__ = (Index("idx_onlinetime_end_timestamp", "end_timestamp"),) + + +class PersonInfo(Base): + """人物信息模型""" + + __tablename__ = "person_info" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + person_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, unique=True, index=True) + person_name: Mapped[str | None] = mapped_column(Text, nullable=True) + name_reason: Mapped[str | None] = mapped_column(Text, nullable=True) + platform: Mapped[str] = mapped_column(Text, nullable=False) + user_id: Mapped[str] = mapped_column(get_string_field(50), nullable=False, index=True) + nickname: Mapped[str | None] = mapped_column(Text, nullable=True) + impression: Mapped[str | None] = mapped_column(Text, nullable=True) + short_impression: Mapped[str | None] = mapped_column(Text, nullable=True) + points: Mapped[str | None] = mapped_column(Text, nullable=True) + forgotten_points: Mapped[str | None] = mapped_column(Text, nullable=True) + info_list: Mapped[str | None] = mapped_column(Text, nullable=True) + know_times: Mapped[float | None] = mapped_column(Float, nullable=True) + know_since: Mapped[float | None] = mapped_column(Float, nullable=True) + last_know: Mapped[float | None] = mapped_column(Float, nullable=True) + attitude: Mapped[int | None] = mapped_column(Integer, nullable=True, default=50) + + __table_args__ = ( + Index("idx_personinfo_person_id", "person_id"), + Index("idx_personinfo_user_id", "user_id"), + ) + + +class BotPersonalityInterests(Base): + """机器人人格兴趣标签模型""" + + __tablename__ = "bot_personality_interests" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + personality_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, index=True) + personality_description: Mapped[str] = mapped_column(Text, nullable=False) + interest_tags: Mapped[str] = mapped_column(Text, nullable=False) + embedding_model: Mapped[str] = mapped_column(get_string_field(100), nullable=False, default="text-embedding-ada-002") + version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + last_updated: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now, index=True) + + __table_args__ = ( + Index("idx_botpersonality_personality_id", "personality_id"), + Index("idx_botpersonality_version", "version"), + Index("idx_botpersonality_last_updated", "last_updated"), + ) + + +class Memory(Base): + """记忆模型""" + + __tablename__ = "memory" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + memory_id: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True) + chat_id: Mapped[str | None] = mapped_column(Text, nullable=True) + memory_text: Mapped[str | None] = mapped_column(Text, nullable=True) + keywords: Mapped[str | None] = mapped_column(Text, nullable=True) + create_time: Mapped[float | None] = mapped_column(Float, nullable=True) + last_view_time: Mapped[float | None] = mapped_column(Float, nullable=True) + + __table_args__ = (Index("idx_memory_memory_id", "memory_id"),) + + +class Expression(Base): + """表达风格模型""" + + __tablename__ = "expression" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + situation: Mapped[str] = mapped_column(Text, nullable=False) + style: Mapped[str] = mapped_column(Text, nullable=False) + count: Mapped[float] = mapped_column(Float, nullable=False) + last_active_time: Mapped[float] = mapped_column(Float, nullable=False) + chat_id: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True) + type: Mapped[str] = mapped_column(Text, nullable=False) + create_date: Mapped[float | None] = mapped_column(Float, nullable=True) + + __table_args__ = (Index("idx_expression_chat_id", "chat_id"),) + + +class ThinkingLog(Base): + """思考日志模型""" + + __tablename__ = "thinking_logs" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + chat_id: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True) + trigger_text: Mapped[str | None] = mapped_column(Text, nullable=True) + response_text: Mapped[str | None] = mapped_column(Text, nullable=True) + trigger_info_json: Mapped[str | None] = mapped_column(Text, nullable=True) + response_info_json: Mapped[str | None] = mapped_column(Text, nullable=True) + timing_results_json: Mapped[str | None] = mapped_column(Text, nullable=True) + chat_history_json: Mapped[str | None] = mapped_column(Text, nullable=True) + chat_history_in_thinking_json: Mapped[str | None] = mapped_column(Text, nullable=True) + chat_history_after_response_json: Mapped[str | None] = mapped_column(Text, nullable=True) + heartflow_data_json: Mapped[str | None] = mapped_column(Text, nullable=True) + reasoning_data_json: Mapped[str | None] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now) + + __table_args__ = (Index("idx_thinkinglog_chat_id", "chat_id"),) + + +class GraphNodes(Base): + """记忆图节点模型""" + + __tablename__ = "graph_nodes" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + concept: Mapped[str] = mapped_column(get_string_field(255), nullable=False, unique=True, index=True) + memory_items: Mapped[str] = mapped_column(Text, nullable=False) + hash: Mapped[str] = mapped_column(Text, nullable=False) + weight: Mapped[float] = mapped_column(Float, nullable=False, default=1.0) + created_time: Mapped[float] = mapped_column(Float, nullable=False) + last_modified: Mapped[float] = mapped_column(Float, nullable=False) + + __table_args__ = (Index("idx_graphnodes_concept", "concept"),) + + +class GraphEdges(Base): + """记忆图边模型""" + + __tablename__ = "graph_edges" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + source: Mapped[str] = mapped_column(get_string_field(255), nullable=False, index=True) + target: Mapped[str] = mapped_column(get_string_field(255), nullable=False, index=True) + strength: Mapped[int] = mapped_column(Integer, nullable=False) + hash: Mapped[str] = mapped_column(Text, nullable=False) + created_time: Mapped[float] = mapped_column(Float, nullable=False) + last_modified: Mapped[float] = mapped_column(Float, nullable=False) + + __table_args__ = ( + Index("idx_graphedges_source", "source"), + Index("idx_graphedges_target", "target"), + ) + + +class Schedule(Base): + """日程模型""" + + __tablename__ = "schedule" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + date: Mapped[str] = mapped_column(get_string_field(10), nullable=False, unique=True, index=True) + schedule_data: Mapped[str] = mapped_column(Text, nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now) + updated_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now, onupdate=datetime.datetime.now) + + __table_args__ = (Index("idx_schedule_date", "date"),) + + +class MaiZoneScheduleStatus(Base): + """麦麦空间日程处理状态模型""" + + __tablename__ = "maizone_schedule_status" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + datetime_hour: Mapped[str] = mapped_column(get_string_field(13), nullable=False, unique=True, index=True) + activity: Mapped[str] = mapped_column(Text, nullable=False) + is_processed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + processed_at: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True) + story_content: Mapped[str | None] = mapped_column(Text, nullable=True) + send_success: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + created_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now) + updated_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now, onupdate=datetime.datetime.now) + + __table_args__ = ( + Index("idx_maizone_datetime_hour", "datetime_hour"), + Index("idx_maizone_is_processed", "is_processed"), + ) + + +class BanUser(Base): + """被禁用用户模型 + + 使用 SQLAlchemy 2.0 类型标注写法,方便静态类型检查器识别实际字段类型, + 避免在业务代码中对属性赋值时报 `Column[...]` 不可赋值的告警。 + """ + + __tablename__ = "ban_users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + platform: Mapped[str] = mapped_column(Text, nullable=False) + user_id: Mapped[str] = mapped_column(get_string_field(50), nullable=False, index=True) + violation_num: Mapped[int] = mapped_column(Integer, nullable=False, default=0, index=True) + reason: Mapped[str] = mapped_column(Text, nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now) + + __table_args__ = ( + Index("idx_violation_num", "violation_num"), + Index("idx_banuser_user_id", "user_id"), + Index("idx_banuser_platform", "platform"), + Index("idx_banuser_platform_user_id", "platform", "user_id"), + ) + + +class AntiInjectionStats(Base): + """反注入系统统计模型""" + + __tablename__ = "anti_injection_stats" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + total_messages: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + """总处理消息数""" + + detected_injections: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + """检测到的注入攻击数""" + + blocked_messages: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + """被阻止的消息数""" + + shielded_messages: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + """被加盾的消息数""" + + processing_time_total: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) + """总处理时间""" + + total_process_time: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) + """累计总处理时间""" + + last_process_time: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) + """最近一次处理时间""" + + error_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + """错误计数""" + + start_time: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now) + """统计开始时间""" + + created_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now) + """记录创建时间""" + + updated_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now, onupdate=datetime.datetime.now) + """记录更新时间""" + + __table_args__ = ( + Index("idx_anti_injection_stats_created_at", "created_at"), + Index("idx_anti_injection_stats_updated_at", "updated_at"), + ) + + +class CacheEntries(Base): + """工具缓存条目模型""" + + __tablename__ = "cache_entries" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + cache_key: Mapped[str] = mapped_column(get_string_field(500), nullable=False, unique=True, index=True) + """缓存键,包含工具名、参数和代码哈希""" + + cache_value: Mapped[str] = mapped_column(Text, nullable=False) + """缓存的数据,JSON格式""" + + expires_at: Mapped[float] = mapped_column(Float, nullable=False, index=True) + """过期时间戳""" + + tool_name: Mapped[str] = mapped_column(get_string_field(100), nullable=False, index=True) + """工具名称""" + + created_at: Mapped[float] = mapped_column(Float, nullable=False, default=lambda: time.time()) + """创建时间戳""" + + last_accessed: Mapped[float] = mapped_column(Float, nullable=False, default=lambda: time.time()) + """最后访问时间戳""" + + access_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + """访问次数""" + + __table_args__ = ( + Index("idx_cache_entries_key", "cache_key"), + Index("idx_cache_entries_expires_at", "expires_at"), + Index("idx_cache_entries_tool_name", "tool_name"), + Index("idx_cache_entries_created_at", "created_at"), + ) + + +class MonthlyPlan(Base): + """月度计划模型""" + + __tablename__ = "monthly_plans" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + plan_text: Mapped[str] = mapped_column(Text, nullable=False) + target_month: Mapped[str] = mapped_column(String(7), nullable=False, index=True) + status: Mapped[str] = mapped_column(get_string_field(20), nullable=False, default="active", index=True) + usage_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + last_used_date: Mapped[str | None] = mapped_column(String(10), nullable=True, index=True) + created_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now) + is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True) + + __table_args__ = ( + Index("idx_monthlyplan_target_month_status", "target_month", "status"), + Index("idx_monthlyplan_last_used_date", "last_used_date"), + Index("idx_monthlyplan_usage_count", "usage_count"), + ) + + +class PermissionNodes(Base): + """权限节点模型""" + + __tablename__ = "permission_nodes" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + node_name: Mapped[str] = mapped_column(get_string_field(255), nullable=False, unique=True, index=True) + description: Mapped[str] = mapped_column(Text, nullable=False) + plugin_name: Mapped[str] = mapped_column(get_string_field(100), nullable=False, index=True) + default_granted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, nullable=False) + + __table_args__ = ( + Index("idx_permission_plugin", "plugin_name"), + Index("idx_permission_node", "node_name"), + ) + + +class UserPermissions(Base): + """用户权限模型""" + + __tablename__ = "user_permissions" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + platform: Mapped[str] = mapped_column(get_string_field(50), nullable=False, index=True) + user_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, index=True) + permission_node: Mapped[str] = mapped_column(get_string_field(255), nullable=False, index=True) + granted: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + granted_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, nullable=False) + granted_by: Mapped[str | None] = mapped_column(get_string_field(100), nullable=True) + + __table_args__ = ( + Index("idx_user_platform_id", "platform", "user_id"), + Index("idx_user_permission", "platform", "user_id", "permission_node"), + Index("idx_permission_granted", "permission_node", "granted"), + ) + + +class UserRelationships(Base): + """用户关系模型 - 存储用户与bot的关系数据""" + + __tablename__ = "user_relationships" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, unique=True, index=True) + user_name: Mapped[str | None] = mapped_column(get_string_field(100), nullable=True) + user_aliases: Mapped[str | None] = mapped_column(Text, nullable=True) # 用户别名,逗号分隔 + relationship_text: Mapped[str | None] = mapped_column(Text, nullable=True) + preference_keywords: Mapped[str | None] = mapped_column(Text, nullable=True) # 用户偏好关键词,逗号分隔 + relationship_score: Mapped[float] = mapped_column(Float, nullable=False, default=0.3) # 关系分数(0-1) + last_updated: Mapped[float] = mapped_column(Float, nullable=False, default=time.time) + created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, nullable=False) + + __table_args__ = ( + Index("idx_user_relationship_id", "user_id"), + Index("idx_relationship_score", "relationship_score"), + Index("idx_relationship_updated", "last_updated"), + )