refactor(database): 阶段二 - 完成核心层重构

- models.py: 迁移25个模型类,使用统一的Mapped类型注解
  * 包含: ChatStreams, Messages, PersonInfo, LLMUsage等
  * 新增: PermissionNodes, UserPermissions, UserRelationships
  * 654行纯模型定义代码,无初始化逻辑

- migration.py: 重构数据库迁移逻辑
  * check_and_migrate_database: 自动检查和迁移表结构
  * create_all_tables: 快速创建所有表
  * drop_all_tables: 测试用删除所有表
  * 使用新架构的engine和models

- __init__.py: 完善导出清单
  * 导出所有25个模型类
  * 导出迁移函数
  * 导出Base和工具函数

- 辅助脚本:
  * extract_models.py: 自动提取模型定义
  * cleanup_models.py: 清理非模型代码

核心层现已完整,下一步进入优化层实现
This commit is contained in:
Windpicker-owo
2025-11-01 12:45:33 +08:00
parent fbe6fb759d
commit c91fee75d2
5 changed files with 1062 additions and 0 deletions

49
scripts/cleanup_models.py Normal file
View File

@@ -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)}")

66
scripts/extract_models.py Normal file
View File

@@ -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}')

View File

@@ -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",
]

View File

@@ -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("所有数据库表已删除。")

View File

@@ -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"),
)