diff --git a/changelogs/mai_next_todo.md b/changelogs/mai_next_todo.md index 32243caf..ae5783f0 100644 --- a/changelogs/mai_next_todo.md +++ b/changelogs/mai_next_todo.md @@ -33,4 +33,72 @@ version 0.3.0 - 2026-01-11 ### ModelConfig - [x] 迁移了原来在`ModelConfig`中的方法到一个单独的临时类`TempMethodsLLMUtils`中 - [x] get_model_info - - [x] get_provider \ No newline at end of file + - [x] get_provider + +## 数据库模型设计 +仅保留要点说明 +### General Modifications +- [x] 所有项目增加自增编号主键`id` +- [x] 统一使用了SQLModel作为基类 +- [x] 复杂类型使用JSON格式存储 +- [x] 所有时间戳字段统一命名为`timestamp` +### 消息模型 MaiMessage +- [x] 自增编号主键`id` +- [x] 消息元数据 + - [x] 消息id`message_id` + - [x] 消息时间戳`time` + - [x] 平台名`platform` + - [x] 用户元数据 + - [x] 用户id`user_id` + - [x] 用户昵称`user_nickname` + - [x] 用户备注名`user_cardname` + - [x] 用户平台`user_platform` + - [x] 群组元数据 + - [x] 群组id`group_id` + - [x] 群组名称`group_name` + - [x] 群组平台`group_platform` + - [x] 被提及/at字段 + - [x] 是否被提及`is_mentioned` + - [x] 是否被at`is_at` +- [x] 消息内容 + - [x] 原始消息内容`raw_content`(base64编码存储) + - [x] 处理后的纯文本内容`processed_plain_text` + - [x] 真正放入Prompt的消息内容`display_message` +- [x] 消息内部元数据 + - [x] 聊天会话id`session_id` + - [x] 回复的消息id`reply_to` + - [x] 是否为表情包消息`is_emoji` + - [x] 是否为图片消息`is_picture` + - [x] 是否为命令消息`is_command` + - [x] 是否为通知消息`is_notify` +- [x] 其他配置`additional_config`(JSON格式存储) + +### 模型使用情况 ModelUsage +- [x] 模型相关信息 +- [x] 请求相关信息 +- [x] Token使用情况 + +### 图片数据模型 +- [x] 图片元信息 + - [x] 图片哈希值`image_hash`,使用`sha256`,同时作为图片唯一ID +- [x] 表情包的情感标签`emotion` +- [x] 是否已经被注册`is_registered` +- [x] 是否被手动禁用`is_banned` +### 动作记录模型 ActionRecord +### 命令执行记录模型 CommandRecord +新增此记录 +### 在线时间记录模型 OnlineTime +### 表达方式模型 +### 黑话模型 +- [x] 重命名`inference_content_only`为`inference_with_content_only` +### 聊天记录模型 +- [x] 重命名`original_text`为`original_message` +- [x] 重命名`forget_times`为`query_forget_count` +### 细枝末节 +- [ ] 统一所有的`stream_id`和`chat_id`命名为`session_id` +- [ ] 更换Hash方式为`sha256` + +## 一些细枝末节的东西 +- [ ] 将`stream_id`和`chat_id`统一命名为`session_id` +- [ ] 映射表 + - [ ] `platform_group_user_session_id_map` `平台_群组_用户`-`会话ID` 映射表 \ No newline at end of file diff --git a/code_scripts/generate_database_datamodel_py.py b/code_scripts/generate_database_datamodel_py.py new file mode 100644 index 00000000..607c06f2 --- /dev/null +++ b/code_scripts/generate_database_datamodel_py.py @@ -0,0 +1,127 @@ +from pathlib import Path +import ast +import subprocess +import sys + +base_file_path = Path(__file__).parent.parent.absolute().resolve() / "src" / "common" / "database" / "database_model.py" +target_file_path = ( + Path(__file__).parent.parent.absolute().resolve() / "src" / "common" / "database" / "database_datamodel.py" +) + +with open(base_file_path, "r", encoding="utf-8") as f: + source_text = f.read() +source_lines = source_text.splitlines() + +try: + tree = ast.parse(source_text) +except SyntaxError as e: + raise e + +code_lines = [ + "from typing import Optional", + "from pydantic import BaseModel", + "from datetime import datetime", + "from .database_model import ModelUser, ImageType", +] + + +def src(node): + seg = ast.get_source_segment(source_text, node) + return seg if seg is not None else ast.unparse(node) + + +for node in tree.body: + if not isinstance(node, ast.ClassDef): + continue + # 判断是否 SQLModel 且 table=True + has_sqlmodel = any( + (isinstance(b, ast.Name) and b.id == "SQLModel") or (isinstance(b, ast.Attribute) and b.attr == "SQLModel") + for b in node.bases + ) + has_table_kw = any( + (kw.arg == "table" and isinstance(kw.value, ast.Constant) and kw.value.value is True) for kw in node.keywords + ) + if not (has_sqlmodel and has_table_kw): + continue + + class_name = node.name + code_lines.append("") + code_lines.append(f"class {class_name}(BaseModel):") + + fields_added = 0 + for item in node.body: + # 跳过 __tablename__ 等 + if isinstance(item, ast.Assign): + if len(item.targets) != 1 or not isinstance(item.targets[0], ast.Name): + continue + name = item.targets[0].id + if name == "__tablename__": + continue + value_src = src(item.value) + line = f" {name} = {value_src}" + fields_added += 1 + lineno = getattr(item, "lineno", None) + elif isinstance(item, ast.AnnAssign): + # 注解赋值 + if not isinstance(item.target, ast.Name): + continue + name = item.target.id + ann = src(item.annotation) if item.annotation is not None else None + if item.value is None: + line = f" {name}: {ann}" if ann else f" {name}" + elif isinstance(item.value, ast.Call) and ( + (isinstance(item.value.func, ast.Name) and item.value.func.id == "Field") + or (isinstance(item.value.func, ast.Attribute) and item.value.func.attr == "Field") + ): + default_kw = next((kw for kw in item.value.keywords if kw.arg == "default"), None) + if default_kw is None: + # 没有 default,保留类型但不赋值 + line = f" {name}: {ann}" if ann else f" {name}" + else: + default_src = src(default_kw.value) + line = f" {name}: {ann} = {default_src}" + else: + value_src = src(item.value) + line = f" {name}: {ann} = {value_src}" if ann else f" {name} = {value_src}" + fields_added += 1 + lineno = getattr(item, "lineno", None) + else: + continue + + # 提取同一行的行内注释作为字段说明(如果存在) + comment = None + if lineno is not None: + src_line = source_lines[lineno - 1] + if "#" in src_line: + # 取第一个 # + comment = src_line.split("#", 1)[1].strip() + # 避免三引号冲突 + comment = comment.replace('"""', '\\"""') + + code_lines.append(line) + if comment: + code_lines.append(f' """{comment}"""') + else: + print(f"Warning: No comment found for field '{name}' in class '{class_name}'.") + + if fields_added == 0: + code_lines.append(" pass") + +with open(target_file_path, "w", encoding="utf-8") as f: + f.write("\n".join(code_lines) + "\n") + +try: + result = subprocess.run(["ruff", "format", str(target_file_path)], capture_output=True, text=True) +except FileNotFoundError: + print("ruff 未找到,请安装 ruff 并确保其在 PATH 中(例如:pip install ruff)", file=sys.stderr) + sys.exit(127) + +# 输出 ruff 的 stdout/stderr +if result.stdout: + print(result.stdout, end="") +if result.stderr: + print(result.stderr, file=sys.stderr, end="") + +if result.returncode != 0: + print(f"ruff 检查失败,退出码:{result.returncode}", file=sys.stderr) + sys.exit(result.returncode) diff --git a/src/common/database/database_datamodel.py b/src/common/database/database_datamodel.py new file mode 100644 index 00000000..66264742 --- /dev/null +++ b/src/common/database/database_datamodel.py @@ -0,0 +1,246 @@ +from typing import Optional +from pydantic import BaseModel +from datetime import datetime +from .database_model import ModelUser, ImageType + + +class MaiMessage(BaseModel): + id: Optional[int] = None + """自增主键""" + message_id: str + """消息id""" + time: float + """消息时间,单位为秒""" + platform: str + """顶层平台字段""" + user_id: str + """发送者用户id""" + user_nickname: str + """发送者昵称""" + user_cardname: Optional[str] = None + """发送者备注名""" + user_platform: Optional[str] = None + """发送者平台""" + group_id: Optional[str] = None + """群组id""" + group_name: Optional[str] = None + """群组名称""" + group_platform: Optional[str] = None + """群组平台""" + is_mentioned: bool = False + """被提及""" + is_at: bool = False + """被at""" + session_id: str + """聊天会话id""" + reply_to: Optional[str] = None + """回复的消息id""" + is_emoji: bool = False + """是否为表情包消息""" + is_picture: bool = False + """是否为图片消息""" + is_command: bool = False + """是否为命令""" + is_notify: bool = False + """是否为通知消息""" + raw_content: str + """base64编码的原始消息内容""" + processed_plain_text: str + """平面化处理后的纯文本消息""" + display_message: str + """显示的消息内容(被放入Prompt)""" + additional_config: Optional[str] = None + """额外配置,JSON格式存储""" + + +class ModelUsage(BaseModel): + id: Optional[int] = None + """自增主键""" + model_name: str + """模型实际名称(供应商名称)""" + model_assign_name: Optional[str] = None + """模型分配名称(用户自定义名称)""" + model_api_provider_name: str + """模型API供应商名称""" + endpoint: Optional[str] = None + """模型API的具体endpoint""" + user_type: ModelUser = ModelUser.SYSTEM + """模型使用者类型""" + request_type: str + """内部请求类型,记录哪种模块使用了此模型""" + time_cost: float + """本次请求耗时,单位秒""" + timestamp: datetime + """请求时间戳""" + prompt_tokens: int + """提示词令牌数""" + completion_tokens: int + """完成词令牌数""" + total_tokens: int + """总令牌数""" + cost: float + """本次请求的费用,单位元""" + + +class Images(BaseModel): + id: Optional[int] = None + """自增主键""" + image_hash: str = "" + """图片哈希,使用sha256哈希值,亦作为图片唯一ID""" + description: str + """图片的描述""" + full_path: str + """文件的完整路径 (包括文件名)""" + image_type: ImageType = ImageType.EMOJI + emotion: Optional[str] = None + """表情包的情感标签,逗号分隔""" + query_count: int = 0 + """被查询次数""" + is_registered: bool = False + """是否已经注册""" + is_banned: bool = False + """被手动禁用""" + record_time: datetime + """记录时间(被创建的时间)""" + register_time: Optional[datetime] = None + """注册时间(被注册为可用表情包的时间)""" + vlm_processed: bool = False + """是否已经过VLM处理""" + + +class ActionRecord(BaseModel): + id: Optional[int] = None + """自增主键""" + action_id: str + """动作ID""" + timestamp: datetime + """记录时间戳""" + session_id: str + """对应的 ChatSession session_id""" + action_name: str + """动作名称""" + action_reasoning: Optional[str] = None + """动作推理过程""" + action_data: Optional[str] = None + """动作数据,JSON格式存储""" + action_builtin_prompt: Optional[str] = None + """内置动作提示""" + action_display_prompt: Optional[str] = None + """最终输入到Prompt的内容""" + + +class CommandRecord(BaseModel): + id: Optional[int] = None + """自增主键""" + timestamp: datetime + """记录时间戳""" + session_id: str + """对应的 ChatSession session_id""" + command_name: str + """命令名称""" + command_data: Optional[str] = None + """命令数据,JSON格式存储""" + command_result: Optional[str] = None + """命令执行结果""" + + +class OnlineTime(BaseModel): + id: Optional[int] = None + """自增主键""" + timestamp: datetime + """时间戳""" + duration_minutes: int + """时长,单位秒""" + start_timestamp: datetime + """上线时间""" + end_timestamp: datetime + """下线时间""" + + +class Expression(BaseModel): + id: Optional[int] = None + """自增主键""" + situation: str + """情景""" + style: str + """风格""" + context: str + """上下文""" + up_content: str + content_list: str + """内容列表,JSON格式存储""" + count: int = 0 + """使用次数""" + last_active_time: datetime + """上次使用时间""" + create_time: datetime + """创建时间""" + session_id: Optional[str] = None + """会话ID,区分是否为全局表达方式""" + + +class Jargon(BaseModel): + id: Optional[int] = None + """自增主键""" + content: str + """黑话内容""" + raw_content: Optional[str] = None + """原始内容,未处理的黑话内容""" + meaning: str + """黑话含义""" + session_id: Optional[str] = None + """会话ID,区分是否为全局黑话""" + count: int = 0 + """使用次数""" + is_jargon: Optional[bool] = True + """是否为黑话,False表示为白话""" + is_complete: bool = False + """是否为已经完成全部推断(count > 100后不再推断)""" + inference_with_context: Optional[str] = None + """带上下文的推断结果,JSON格式""" + inference_with_content_only: Optional[str] = None + """只基于词条的推断结果,JSON格式""" + + +class ChatHistory(BaseModel): + id: Optional[int] = None + """自增主键""" + session_id: str + """聊天会话ID""" + start_timestamp: datetime + """聊天开始时间""" + end_timestamp: datetime + """聊天结束时间""" + query_count: int = 0 + """被检索次数""" + query_forget_count: int = 0 + """被遗忘检查的次数""" + original_messages: str + """对话原文""" + participants: str + """参与者列表,JSON格式存储""" + theme: str + """对话主题:这段对话的主要内容,一个简短的标题""" + keywords: str + """关键词:这段对话的关键词,JSON格式存储""" + summary: str + """概括:对这段话的平文本概括""" + + +class ThinkingQuestion(BaseModel): + id: Optional[int] = None + """自增主键""" + question: str + """问题内容""" + context: Optional[str] = None + """上下文""" + found_answer: bool = False + """是否找到答案""" + answer: Optional[str] = None + """问题答案""" + thinking_steps: Optional[str] = None + """思考步骤,JSON格式存储""" + created_timestamp: datetime + """创建时间""" + updated_timestamp: datetime + """最后更新时间""" diff --git a/src/common/database/database_model.py b/src/common/database/database_model.py index f4543737..9e64da85 100644 --- a/src/common/database/database_model.py +++ b/src/common/database/database_model.py @@ -1,778 +1,235 @@ -from peewee import Model, DoubleField, IntegerField, BooleanField, TextField, FloatField, DateTimeField -from .database import db -import datetime -from src.common.logger import get_logger - -logger = get_logger("database_model") -# 请在此处定义您的数据库实例。 -# 您需要取消注释并配置适合您的数据库的部分。 -# 例如,对于 SQLite: -# db = SqliteDatabase('MaiBot.db') -# -# 对于 PostgreSQL: -# db = PostgresqlDatabase('your_db_name', user='your_user', password='your_password', -# host='localhost', port=5432) -# -# 对于 MySQL: -# db = MySQLDatabase('your_db_name', user='your_user', password='your_password', -# host='localhost', port=3306) +from typing import Optional +from sqlalchemy import Column, Float, Enum as SQLEnum +from sqlmodel import SQLModel, Field +from enum import Enum +from datetime import datetime -# 定义一个基础模型是一个好习惯,所有其他模型都应继承自它。 -# 这允许您在一个地方为所有模型指定数据库。 +class ModelUser(str, Enum): + SYSTEM = "system" + PLUGIN = "plugin" -class BaseModel(Model): - class Meta: - # 将下面的 'db' 替换为您实际的数据库实例变量名。 - database = db # 例如: database = my_actual_db_instance - pass # 在用户定义数据库实例之前,此处为占位符 +class ImageType(str, Enum): + EMOJI = "emoji" + IMAGE = "image" -class ChatStreams(BaseModel): - """ - 用于存储流式记录数据的模型,类似于提供的 MongoDB 结构。 - """ +class MaiMessage(SQLModel, table=True): + __tablename__ = "mai_messages" # type: ignore + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 - # stream_id: "a544edeb1a9b73e3e1d77dff36e41264" - # 假设 stream_id 是唯一的,并为其创建索引以提高查询性能。 - stream_id = TextField(unique=True, index=True) + # 消息元数据 + message_id: str = Field(index=True, max_length=255) # 消息id + time: float = Field(sa_column=Column(Float)) # 消息时间,单位为秒 + platform: str = Field(index=True, max_length=100) # 顶层平台字段 + # 消息发送者信息 + user_id: str = Field(index=True, max_length=255) # 发送者用户id + user_nickname: str = Field(index=True, max_length=255) # 发送者昵称 + user_cardname: Optional[str] = Field(default=None, max_length=255, nullable=True) # 发送者备注名 + user_platform: Optional[str] = Field(default=None, max_length=100, nullable=True) # 发送者平台 + # 群聊信息(如果有) + group_id: Optional[str] = Field(index=True, default=None, max_length=255, nullable=True) # 群组id + group_name: Optional[str] = Field(default=None, max_length=255, nullable=True) # 群组名称 + group_platform: Optional[str] = Field(default=None, max_length=100, nullable=True) # 群组平台 + # 被提及/at字段 + is_mentioned: bool = Field(default=False) # 被提及 + is_at: bool = Field(default=False) # 被at - # create_time: 1746096761.4490178 (时间戳,精确到小数点后7位) - # DoubleField 用于存储浮点数,适合此类时间戳。 - create_time = DoubleField() + # 消息内部元数据 + session_id: str = Field(index=True, max_length=255) # 聊天会话id + reply_to: Optional[str] = Field(default=None, max_length=255, nullable=True) # 回复的消息id + is_emoji: bool = Field(default=False) # 是否为表情包消息 + is_picture: bool = Field(default=False) # 是否为图片消息 + is_command: bool = Field(default=False) # 是否为命令 + is_notify: bool = Field(default=False) # 是否为通知消息 - # group_info 字段: - # platform: "qq" - # group_id: "941657197" - # group_name: "测试" - group_platform = TextField(null=True) # 群聊信息可能不存在 - group_id = TextField(null=True) - group_name = TextField(null=True) + # 消息内容 + raw_content: str # base64编码的原始消息内容 + processed_plain_text: str = Field(index=True) # 平面化处理后的纯文本消息 + display_message: str # 显示的消息内容(被放入Prompt) - # last_active_time: 1746623771.4825106 (时间戳,精确到小数点后7位) - last_active_time = DoubleField() - - # platform: "qq" (顶层平台字段) - platform = TextField() - - # user_info 字段: - # platform: "qq" - # user_id: "1787882683" - # user_nickname: "墨梓柒(IceSakurary)" - # user_cardname: "" - user_platform = TextField() - user_id = TextField() - user_nickname = TextField() - # user_cardname 可能为空字符串或不存在,设置 null=True 更具灵活性。 - user_cardname = TextField(null=True) - - class Meta: - # 如果 BaseModel.Meta.database 已设置,则此模型将继承该数据库配置。 - # 如果不使用带有数据库实例的 BaseModel,或者想覆盖它, - # 请取消注释并在下面设置数据库实例: - # database = db - table_name = "chat_streams" # 可选:明确指定数据库中的表名 + # 其他配置 + additional_config: Optional[str] = Field(default=None) # 额外配置,JSON格式存储 -class LLMUsage(BaseModel): - """ - 用于存储 API 使用日志数据的模型。 - """ +class ModelUsage(SQLModel, table=True): + __tablename__ = "llm_usage" # type: ignore - model_name = TextField(index=True) # 添加索引 - model_assign_name = TextField(null=True) # 添加索引 - model_api_provider = TextField(null=True) # 添加索引 - user_id = TextField(index=True) # 添加索引 - request_type = TextField(index=True) # 添加索引 - endpoint = TextField() - prompt_tokens = IntegerField() - completion_tokens = IntegerField() - total_tokens = IntegerField() - cost = DoubleField() - time_cost = DoubleField(null=True) - status = TextField() - timestamp = DateTimeField(index=True) # 更改为 DateTimeField 并添加索引 + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 - class Meta: - # 如果 BaseModel.Meta.database 已设置,则此模型将继承该数据库配置。 - # database = db - table_name = "llm_usage" + # 模型相关信息 + model_name: str = Field(index=True, max_length=255) # 模型实际名称(供应商名称) + model_assign_name: Optional[str] = Field(index=True, default=None, max_length=255) # 模型分配名称(用户自定义名称) + model_api_provider_name: str = Field(index=True, max_length=255) # 模型API供应商名称 + + # 请求相关信息 + endpoint: Optional[str] = Field(default=None, max_length=255, nullable=True) # 模型API的具体endpoint + user_type: ModelUser = Field(sa_column=Column(SQLEnum(ModelUser)), default=ModelUser.SYSTEM) # 模型使用者类型 + request_type: str = Field(max_length=50) # 内部请求类型,记录哪种模块使用了此模型 + time_cost: float = Field(sa_column=Column(Float)) # 本次请求耗时,单位秒 + timestamp: datetime = Field(default_factory=datetime.now, index=True) # 请求时间戳 + + # Token使用情况 + prompt_tokens: int # 提示词令牌数 + completion_tokens: int # 完成词令牌数 + total_tokens: int # 总令牌数 + cost: float # 本次请求的费用,单位元 -class Emoji(BaseModel): - """表情包""" +class Images(SQLModel, table=True): + """用于同时存储表情包和图片的数据库模型。""" - full_path = TextField(unique=True, index=True) # 文件的完整路径 (包括文件名) - format = TextField() # 图片格式 - emoji_hash = TextField(index=True) # 表情包的哈希值 - description = TextField() # 表情包的描述 - query_count = IntegerField(default=0) # 查询次数(用于统计表情包被查询描述的次数) - is_registered = BooleanField(default=False) # 是否已注册 - is_banned = BooleanField(default=False) # 是否被禁止注册 - # emotion: list[str] # 表情包的情感标签 - 存储为文本,应用层处理序列化/反序列化 - emotion = TextField(null=True) - record_time = FloatField() # 记录时间(被创建的时间) - register_time = FloatField(null=True) # 注册时间(被注册为可用表情包的时间) - usage_count = IntegerField(default=0) # 使用次数(被使用的次数) - last_used_time = FloatField(null=True) # 上次使用时间 + __tablename__ = "images" # type: ignore - class Meta: - # database = db # 继承自 BaseModel - table_name = "emoji" + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 + + # 元信息 + image_hash: str = Field(default="", max_length=255) # 图片哈希,使用sha256哈希值,亦作为图片唯一ID + description: str # 图片的描述 + full_path: str = Field(index=True, max_length=1024) # 文件的完整路径 (包括文件名) + image_type: ImageType = Field(sa_column=Column(SQLEnum(ImageType)), default=ImageType.EMOJI) + """图片类型,例如 'emoji' 或 'image'""" + emotion: Optional[str] = Field(default=None, nullable=True) # 表情包的情感标签,逗号分隔 + + query_count: int = Field(default=0) # 被查询次数 + is_registered: bool = Field(default=False) # 是否已经注册 + is_banned: bool = Field(default=False) # 被手动禁用 + + record_time: datetime = Field(default_factory=datetime.now, index=True) # 记录时间(被创建的时间) + register_time: Optional[datetime] = Field(default=None, nullable=True) # 注册时间(被注册为可用表情包的时间) + + vlm_processed: bool = Field(default=False) # 是否已经过VLM处理 -class Messages(BaseModel): - """ - 用于存储消息数据的模型。 - """ +class ActionRecord(SQLModel, table=True): + """存储动作记录""" - message_id = TextField(index=True) # 消息 ID (更改自 IntegerField) - time = DoubleField() # 消息时间戳 + __tablename__ = "action_records" # type: ignore - chat_id = TextField(index=True) # 对应的 ChatStreams stream_id + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 - reply_to = TextField(null=True) + # 元信息 + action_id: str = Field(index=True, max_length=255) # 动作ID + timestamp: datetime = Field(default_factory=datetime.now, index=True) # 记录时间戳 + session_id: str = Field(index=True, max_length=255) # 对应的 ChatSession session_id - interest_value = DoubleField(null=True) - key_words = TextField(null=True) - key_words_lite = TextField(null=True) + # 调用信息 + action_name: str = Field(max_length=255) # 动作名称 + action_reasoning: Optional[str] = Field(default=None) # 动作推理过程 + action_data: Optional[str] = Field(default=None) # 动作数据,JSON格式存储 - is_mentioned = BooleanField(null=True) - is_at = BooleanField(null=True) - reply_probability_boost = DoubleField(null=True) - # 从 chat_info 扁平化而来的字段 - chat_info_stream_id = TextField() - chat_info_platform = TextField() - chat_info_user_platform = TextField() - chat_info_user_id = TextField() - chat_info_user_nickname = TextField() - chat_info_user_cardname = TextField(null=True) - chat_info_group_platform = TextField(null=True) # 群聊信息可能不存在 - chat_info_group_id = TextField(null=True) - chat_info_group_name = TextField(null=True) - chat_info_create_time = DoubleField() - chat_info_last_active_time = DoubleField() - - # 从顶层 user_info 扁平化而来的字段 (消息发送者信息) - user_platform = TextField(null=True) - user_id = TextField(null=True) - user_nickname = TextField(null=True) - user_cardname = TextField(null=True) - - processed_plain_text = TextField(null=True) # 处理后的纯文本消息 - display_message = TextField(null=True) # 显示的消息 - - priority_mode = TextField(null=True) - priority_info = TextField(null=True) - - additional_config = TextField(null=True) - is_emoji = BooleanField(default=False) - is_picid = BooleanField(default=False) - is_command = BooleanField(default=False) - intercept_message_level = IntegerField(default=0) - is_notify = BooleanField(default=False) - - selected_expressions = TextField(null=True) - - class Meta: - # database = db # 继承自 BaseModel - table_name = "messages" + action_builtin_prompt: Optional[str] = Field(default=None) # 内置动作提示 + action_display_prompt: Optional[str] = Field(default=None) # 最终输入到Prompt的内容 -class ActionRecords(BaseModel): - """ - 用于存储动作记录数据的模型。 - """ +class CommandRecord(SQLModel, table=True): + """记录命令执行情况""" - action_id = TextField(index=True) # 消息 ID (更改自 IntegerField) - time = DoubleField() # 消息时间戳 + __tablename__ = "command_records" # type: ignore - action_reasoning = TextField(null=True) + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 - action_name = TextField() - action_data = TextField() - action_done = BooleanField(default=False) + # 元信息 + timestamp: datetime = Field(default_factory=datetime.now, index=True) # 记录时间戳 + session_id: str = Field(index=True, max_length=255) # 对应的 ChatSession session_id - action_build_into_prompt = BooleanField(default=False) - action_prompt_display = TextField() - - chat_id = TextField(index=True) # 对应的 ChatStreams stream_id - chat_info_stream_id = TextField() - chat_info_platform = TextField() - - class Meta: - # database = db # 继承自 BaseModel - table_name = "action_records" + # 调用信息 + command_name: str = Field(index=True, max_length=255) # 命令名称 + command_data: Optional[str] = Field(default=None) # 命令数据,JSON格式存储 + command_result: Optional[str] = Field(default=None) # 命令执行结果 -class Images(BaseModel): - """ - 用于存储图像信息的模型。 - """ - - image_id = TextField(default="") # 图片唯一ID - emoji_hash = TextField(index=True) # 图像的哈希值 - description = TextField(null=True) # 图像的描述 - path = TextField(unique=True) # 图像文件的路径 - # base64 = TextField() # 图片的base64编码 - count = IntegerField(default=1) # 图片被引用的次数 - timestamp = FloatField() # 时间戳 - type = TextField() # 图像类型,例如 "emoji" - vlm_processed = BooleanField(default=False) # 是否已经过VLM处理 - - class Meta: - table_name = "images" - - -class ImageDescriptions(BaseModel): - """ - 用于存储图像描述信息的模型。 - """ - - type = TextField() # 类型,例如 "emoji" - image_description_hash = TextField(index=True) # 图像的哈希值 - description = TextField() # 图像的描述 - timestamp = FloatField() # 时间戳 - - class Meta: - # database = db # 继承自 BaseModel - table_name = "image_descriptions" - - -class EmojiDescriptionCache(BaseModel): - """ - 存储表情包的详细描述和情感标签缓存 - """ - - emoji_hash = TextField(unique=True, index=True) - description = TextField() # 详细描述 - emotion_tags = TextField(null=True) # 情感标签,逗号分隔 - timestamp = FloatField() - - class Meta: - table_name = "emoji_description_cache" - - -class OnlineTime(BaseModel): +class OnlineTime(SQLModel, table=True): """ 用于存储在线时长记录的模型。 """ - # timestamp: "$date": "2025-05-01T18:52:18.191Z" (存储为字符串) - timestamp = TextField(default=datetime.datetime.now) # 时间戳 - duration = IntegerField() # 时长,单位分钟 - start_timestamp = DateTimeField(default=datetime.datetime.now) - end_timestamp = DateTimeField(index=True) + __tablename__ = "online_time" # type: ignore - class Meta: - # database = db # 继承自 BaseModel - table_name = "online_time" + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 + timestamp: datetime = Field(default_factory=datetime.now, index=True) # 时间戳 + duration_minutes: int = Field() # 时长,单位秒 + start_timestamp: datetime = Field(default_factory=datetime.now) # 上线时间 + end_timestamp: datetime = Field(index=True) # 下线时间 -class PersonInfo(BaseModel): - """ - 用于存储个人信息数据的模型。 - """ - is_known = BooleanField(default=False) # 是否已认识 - person_id = TextField(unique=True, index=True) # 个人唯一ID - person_name = TextField(null=True) # 个人名称 (允许为空) - name_reason = TextField(null=True) # 名称设定的原因 - platform = TextField() # 平台 - user_id = TextField(index=True) # 用户ID - nickname = TextField(null=True) # 用户昵称 - group_nick_name = TextField(null=True) # 群昵称列表 (JSON格式,存储 [{"group_id": str, "group_nick_name": str}]) - memory_points = TextField(null=True) # 个人印象的点 - know_times = FloatField(null=True) # 认识时间 (时间戳) - know_since = FloatField(null=True) # 首次印象总结时间 - last_know = FloatField(null=True) # 最后一次印象总结时间 +class Expression(SQLModel, table=True): + """用于存储表达方式的模型""" - class Meta: - # database = db # 继承自 BaseModel - table_name = "person_info" + __tablename__ = "expressions" # type: ignore + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 -class GroupInfo(BaseModel): - """ - 用于存储群组信息数据的模型。 - """ + situation: str = Field(index=True, max_length=255, primary_key=True) # 情景 + style: str = Field(index=True, max_length=255, primary_key=True) # 风格 - group_id = TextField(unique=True, index=True) # 群组唯一ID - group_name = TextField(null=True) # 群组名称 (允许为空) - platform = TextField() # 平台 - group_impression = TextField(null=True) # 群组印象 - member_list = TextField(null=True) # 群成员列表 (JSON格式) - topic = TextField(null=True) # 群组基本信息 + context: str # 上下文 + up_content: str - create_time = FloatField(null=True) # 创建时间 (时间戳) - last_active = FloatField(null=True) # 最后活跃时间 - member_count = IntegerField(null=True, default=0) # 成员数量 + content_list: str # 内容列表,JSON格式存储 + count: int = Field(default=0) # 使用次数 + last_active_time: datetime = Field(default_factory=datetime.now, index=True) # 上次使用时间 + create_time: datetime = Field(default_factory=datetime.now) # 创建时间 + session_id: Optional[str] = Field(default=None, max_length=255, nullable=True) # 会话ID,区分是否为全局表达方式 - class Meta: - # database = db # 继承自 BaseModel - table_name = "group_info" +class Jargon(SQLModel, table=True): + """存黑话的模型""" -class Expression(BaseModel): - """ - 用于存储表达风格的模型。 - """ + __tablename__ = "jargons" # type: ignore - situation = TextField() - style = TextField() - content_list = TextField(null=True) - count = IntegerField(default=1) - last_active_time = FloatField() - chat_id = TextField(index=True) - create_date = FloatField(null=True) # 创建日期,允许为空以兼容老数据 - checked = BooleanField(default=False) # 是否已检查 - rejected = BooleanField(default=False) # 是否被拒绝但未更新 - modified_by = TextField(null=True) # 最后修改来源:'ai' 或 'user',为空表示未检查 + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 - class Meta: - table_name = "expression" + content: str = Field(index=True, max_length=255, primary_key=True) # 黑话内容 + raw_content: Optional[str] = Field(default=None, nullable=True) # 原始内容,未处理的黑话内容 + meaning: str # 黑话含义 + session_id: Optional[str] = Field(default=None, max_length=255, nullable=True) # 会话ID,区分是否为全局黑话 -class Jargon(BaseModel): - """ - 用于存储俚语的模型 - """ + count: int = Field(default=0) # 使用次数 + is_jargon: Optional[bool] = Field(default=True) # 是否为黑话,False表示为白话 + is_complete: bool = Field(default=False) # 是否为已经完成全部推断(count > 100后不再推断) + inference_with_context: Optional[str] = Field(default=None, nullable=True) # 带上下文的推断结果,JSON格式 + inference_with_content_only: Optional[str] = Field(default=None, nullable=True) # 只基于词条的推断结果,JSON格式 - content = TextField() - raw_content = TextField(null=True) - meaning = TextField(null=True) - chat_id = TextField(index=True) - is_global = BooleanField(default=False) - count = IntegerField(default=0) - is_jargon = BooleanField(null=True) # None表示未判定,True表示是黑话,False表示不是黑话 - last_inference_count = IntegerField(null=True) # 最后一次判定的count值,用于避免重启后重复判定 - is_complete = BooleanField(default=False) # 是否已完成所有推断(count>=100后不再推断) - inference_with_context = TextField(null=True) # 基于上下文的推断结果(JSON格式) - inference_content_only = TextField(null=True) # 仅基于词条的推断结果(JSON格式) - class Meta: - table_name = "jargon" +class ChatHistory(SQLModel, table=True): + """存储聊天历史记录的模型""" + __tablename__ = "chat_history" # type: ignore -class ChatHistory(BaseModel): - """ - 用于存储聊天历史概括的模型 - """ + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 - chat_id = TextField(index=True) # 聊天ID - start_time = DoubleField() # 起始时间 - end_time = DoubleField() # 结束时间 - original_text = TextField() # 对话原文 - participants = TextField() # 参与的所有人的昵称,JSON格式存储 - theme = TextField() # 主题:这段对话的主要内容,一个简短的标题 - keywords = TextField() # 关键词:这段对话的关键词,JSON格式存储 - summary = TextField() # 概括:对这段话的平文本概括 - # key_point = TextField(null=True) # 关键信息:话题中的关键信息点,JSON格式存储 - count = IntegerField(default=0) # 被检索次数 - forget_times = IntegerField(default=0) # 被遗忘检查的次数 + # 元信息 + session_id: str = Field(index=True, max_length=255) # 聊天会话ID + start_timestamp: datetime = Field(index=True) # 聊天开始时间 + end_timestamp: datetime = Field(index=True) # 聊天结束时间 + query_count: int = Field(default=0) # 被检索次数 + query_forget_count: int = Field(default=0) # 被遗忘检查的次数 - class Meta: - table_name = "chat_history" + # 历史消息内容 + original_messages: str # 对话原文 + participants: str # 参与者列表,JSON格式存储 + theme: str # 对话主题:这段对话的主要内容,一个简短的标题 + keywords: str # 关键词:这段对话的关键词,JSON格式存储 + summary: str # 概括:对这段话的平文本概括 -class ThinkingBack(BaseModel): - """ - 用于存储记忆检索思考过程的模型 - """ +class ThinkingQuestion(SQLModel, table=True): + """存储思考型问题的模型""" - chat_id = TextField(index=True) # 聊天ID - question = TextField() # 提出的问题 - context = TextField(null=True) # 上下文信息 - found_answer = BooleanField(default=False) # 是否找到答案 - answer = TextField(null=True) # 答案内容 - thinking_steps = TextField(null=True) # 思考步骤(JSON格式) - create_time = DoubleField() # 创建时间 - update_time = DoubleField() # 更新时间 + __tablename__ = "thinking_questions" # type: ignore - class Meta: - table_name = "thinking_back" + id: Optional[int] = Field(default=None, primary_key=True) # 自增主键 - -MODELS = [ - ChatStreams, - LLMUsage, - Emoji, - Messages, - Images, - ImageDescriptions, - EmojiDescriptionCache, - OnlineTime, - PersonInfo, - Expression, - ActionRecords, - Jargon, - ChatHistory, - ThinkingBack, -] - - -def create_tables(): - """ - 创建所有在模型中定义的数据库表。 - """ - with db: - db.create_tables(MODELS) - - -def initialize_database(sync_constraints=False): - """ - 检查所有定义的表是否存在,如果不存在则创建它们。 - 检查所有表的所有字段是否存在,如果缺失则自动添加。 - - Args: - sync_constraints (bool): 是否同步字段约束。默认为 False。 - 如果为 True,会检查并修复字段的 NULL 约束不一致问题。 - """ - - try: - with db: # 管理 table_exists 检查的连接 - for model in MODELS: - table_name = model._meta.table_name - if not db.table_exists(model): - logger.warning(f"表 '{table_name}' 未找到,正在创建...") - db.create_tables([model]) - logger.info(f"表 '{table_name}' 创建成功") - continue - - # 检查字段 - cursor = db.execute_sql(f"PRAGMA table_info('{table_name}')") - existing_columns = {row[1] for row in cursor.fetchall()} - model_fields = set(model._meta.fields.keys()) - - if missing_fields := model_fields - existing_columns: - logger.warning(f"表 '{table_name}' 缺失字段: {missing_fields}") - - for field_name, field_obj in model._meta.fields.items(): - if field_name not in existing_columns: - logger.info(f"表 '{table_name}' 缺失字段 '{field_name}',正在添加...") - field_type = field_obj.__class__.__name__ - sql_type = { - "TextField": "TEXT", - "IntegerField": "INTEGER", - "FloatField": "FLOAT", - "DoubleField": "DOUBLE", - "BooleanField": "INTEGER", - "DateTimeField": "DATETIME", - }.get(field_type, "TEXT") - alter_sql = f"ALTER TABLE {table_name} ADD COLUMN {field_name} {sql_type}" - alter_sql += " NULL" if field_obj.null else " NOT NULL" - if hasattr(field_obj, "default") and field_obj.default is not None: - # 正确处理不同类型的默认值,跳过lambda函数 - default_value = field_obj.default - if callable(default_value): - # 跳过lambda函数或其他可调用对象,这些无法在SQL中表示 - pass - elif isinstance(default_value, str): - alter_sql += f" DEFAULT '{default_value}'" - elif isinstance(default_value, bool): - alter_sql += f" DEFAULT {int(default_value)}" - else: - alter_sql += f" DEFAULT {default_value}" - try: - db.execute_sql(alter_sql) - logger.info(f"字段 '{field_name}' 添加成功") - except Exception as e: - logger.error(f"添加字段 '{field_name}' 失败: {e}") - - # 检查并删除多余字段(新增逻辑) - extra_fields = existing_columns - model_fields - if extra_fields: - logger.warning(f"表 '{table_name}' 存在多余字段: {extra_fields}") - for field_name in extra_fields: - try: - logger.warning(f"表 '{table_name}' 存在多余字段 '{field_name}',正在尝试删除...") - db.execute_sql(f"ALTER TABLE {table_name} DROP COLUMN {field_name}") - logger.info(f"字段 '{field_name}' 删除成功") - except Exception as e: - logger.error(f"删除字段 '{field_name}' 失败: {e}") - - # 如果启用了约束同步,执行约束检查和修复 - if sync_constraints: - logger.debug("开始同步数据库字段约束...") - sync_field_constraints() - logger.debug("数据库字段约束同步完成") - - except Exception as e: - logger.exception(f"检查表或字段是否存在时出错: {e}") - # 如果检查失败(例如数据库不可用),则退出 - return - - logger.info("数据库初始化完成") - - -def sync_field_constraints(): - """ - 同步数据库字段约束,确保现有数据库字段的 NULL 约束与模型定义一致。 - 如果发现不一致,会自动修复字段约束。 - """ - - try: - with db: - for model in MODELS: - table_name = model._meta.table_name - if not db.table_exists(model): - logger.warning(f"表 '{table_name}' 不存在,跳过约束检查") - continue - - logger.debug(f"检查表 '{table_name}' 的字段约束...") - - # 获取当前表结构信息 - cursor = db.execute_sql(f"PRAGMA table_info('{table_name}')") - current_schema = { - row[1]: {"type": row[2], "notnull": bool(row[3]), "default": row[4]} for row in cursor.fetchall() - } - - # 检查每个模型字段的约束 - constraints_to_fix = [] - for field_name, field_obj in model._meta.fields.items(): - if field_name not in current_schema: - continue # 字段不存在,跳过 - - current_notnull = current_schema[field_name]["notnull"] - model_allows_null = field_obj.null - - # 如果模型允许 null 但数据库字段不允许 null,需要修复 - if model_allows_null and current_notnull: - constraints_to_fix.append( - { - "field_name": field_name, - "field_obj": field_obj, - "action": "allow_null", - "current_constraint": "NOT NULL", - "target_constraint": "NULL", - } - ) - logger.warning(f"字段 '{field_name}' 约束不一致: 模型允许NULL,但数据库为NOT NULL") - - # 如果模型不允许 null 但数据库字段允许 null,也需要修复(但要小心) - elif not model_allows_null and not current_notnull: - constraints_to_fix.append( - { - "field_name": field_name, - "field_obj": field_obj, - "action": "disallow_null", - "current_constraint": "NULL", - "target_constraint": "NOT NULL", - } - ) - logger.warning(f"字段 '{field_name}' 约束不一致: 模型不允许NULL,但数据库允许NULL") - - # 修复约束不一致的字段 - if constraints_to_fix: - logger.info(f"表 '{table_name}' 需要修复 {len(constraints_to_fix)} 个字段约束") - _fix_table_constraints(table_name, model, constraints_to_fix) - else: - logger.debug(f"表 '{table_name}' 的字段约束已同步") - - except Exception as e: - logger.exception(f"同步字段约束时出错: {e}") - - -def _fix_table_constraints(table_name, model, constraints_to_fix): - """ - 修复表的字段约束。 - 对于 SQLite,由于不支持直接修改列约束,需要重建表。 - """ - try: - # 备份表名 - backup_table = f"{table_name}_backup_{int(datetime.datetime.now().timestamp())}" - - logger.info(f"开始修复表 '{table_name}' 的字段约束...") - - # 1. 创建备份表 - db.execute_sql(f"CREATE TABLE {backup_table} AS SELECT * FROM {table_name}") - logger.info(f"已创建备份表 '{backup_table}'") - - # 2. 获取原始行数(在删除表之前) - original_count = db.execute_sql(f"SELECT COUNT(*) FROM {backup_table}").fetchone()[0] - logger.info(f"备份表 '{backup_table}' 包含 {original_count} 行数据") - - # 3. 删除原表 - db.execute_sql(f"DROP TABLE {table_name}") - logger.info(f"已删除原表 '{table_name}'") - - # 4. 重新创建表(使用当前模型定义) - db.create_tables([model]) - logger.info(f"已重新创建表 '{table_name}' 使用新的约束") - - # 5. 从备份表恢复数据 - # 获取字段列表,排除主键字段(让数据库自动生成新的主键) - fields = list(model._meta.fields.keys()) - # Peewee 默认使用 'id' 作为主键字段名 - # 尝试获取主键字段名,如果获取失败则默认使用 'id' - primary_key_name = "id" # 默认值 - try: - if hasattr(model._meta, "primary_key") and model._meta.primary_key: - if hasattr(model._meta.primary_key, "name"): - primary_key_name = model._meta.primary_key.name - elif isinstance(model._meta.primary_key, str): - primary_key_name = model._meta.primary_key - except Exception: - pass # 如果获取失败,使用默认值 'id' - - # 如果字段列表包含主键,则排除它 - if primary_key_name in fields: - fields_without_pk = [f for f in fields if f != primary_key_name] - logger.info(f"排除主键字段 '{primary_key_name}',让数据库自动生成新的主键") - else: - fields_without_pk = fields - - fields_str = ", ".join(fields_without_pk) - - # 检查是否有字段需要从 NULL 改为 NOT NULL - null_to_notnull_fields = [ - constraint["field_name"] for constraint in constraints_to_fix if constraint["action"] == "disallow_null" - ] - - if null_to_notnull_fields: - # 需要处理 NULL 值,为这些字段设置默认值 - logger.warning(f"字段 {null_to_notnull_fields} 将从允许NULL改为不允许NULL,需要处理现有的NULL值") - - # 构建更复杂的 SELECT 语句来处理 NULL 值 - select_fields = [] - for field_name in fields_without_pk: - if field_name in null_to_notnull_fields: - field_obj = model._meta.fields[field_name] - # 根据字段类型设置默认值 - if isinstance(field_obj, (TextField,)): - default_value = "''" - elif isinstance(field_obj, (IntegerField, FloatField, DoubleField)): - default_value = "0" - elif isinstance(field_obj, BooleanField): - default_value = "0" - elif isinstance(field_obj, DateTimeField): - default_value = f"'{datetime.datetime.now()}'" - else: - default_value = "''" - - select_fields.append(f"COALESCE({field_name}, {default_value}) as {field_name}") - else: - select_fields.append(field_name) - - select_str = ", ".join(select_fields) - insert_sql = f"INSERT INTO {table_name} ({fields_str}) SELECT {select_str} FROM {backup_table}" - else: - # 没有需要处理 NULL 的字段,直接复制数据(排除主键) - insert_sql = f"INSERT INTO {table_name} ({fields_str}) SELECT {fields_str} FROM {backup_table}" - - db.execute_sql(insert_sql) - logger.info(f"已从备份表恢复数据到 '{table_name}'") - - new_count = db.execute_sql(f"SELECT COUNT(*) FROM {table_name}").fetchone()[0] - - if original_count == new_count: - logger.info(f"数据完整性验证通过: {original_count} 行数据") - # 删除备份表 - db.execute_sql(f"DROP TABLE {backup_table}") - logger.info(f"已删除备份表 '{backup_table}'") - else: - logger.error(f"数据完整性验证失败: 原始 {original_count} 行,新表 {new_count} 行") - logger.error(f"备份表 '{backup_table}' 已保留,请手动检查") - - # 记录修复的约束 - for constraint in constraints_to_fix: - logger.info( - f"已修复字段 '{constraint['field_name']}': " - f"{constraint['current_constraint']} -> {constraint['target_constraint']}" - ) - - except Exception as e: - logger.exception(f"修复表 '{table_name}' 约束时出错: {e}") - # 尝试恢复 - try: - if db.table_exists(backup_table): - logger.info(f"尝试从备份表 '{backup_table}' 恢复...") - db.execute_sql(f"DROP TABLE IF EXISTS {table_name}") - db.execute_sql(f"ALTER TABLE {backup_table} RENAME TO {table_name}") - logger.info(f"已从备份恢复表 '{table_name}'") - except Exception as restore_error: - logger.exception(f"恢复表失败: {restore_error}") - - -def check_field_constraints(): - """ - 检查但不修复字段约束,返回不一致的字段信息。 - 用于在修复前预览需要修复的内容。 - """ - - inconsistencies = {} - - try: - with db: - for model in MODELS: - table_name = model._meta.table_name - if not db.table_exists(model): - continue - - # 获取当前表结构信息 - cursor = db.execute_sql(f"PRAGMA table_info('{table_name}')") - current_schema = { - row[1]: {"type": row[2], "notnull": bool(row[3]), "default": row[4]} for row in cursor.fetchall() - } - - table_inconsistencies = [] - - # 检查每个模型字段的约束 - for field_name, field_obj in model._meta.fields.items(): - if field_name not in current_schema: - continue - - current_notnull = current_schema[field_name]["notnull"] - model_allows_null = field_obj.null - - if model_allows_null and current_notnull: - table_inconsistencies.append( - { - "field_name": field_name, - "issue": "model_allows_null_but_db_not_null", - "model_constraint": "NULL", - "db_constraint": "NOT NULL", - "recommended_action": "allow_null", - } - ) - elif not model_allows_null and not current_notnull: - table_inconsistencies.append( - { - "field_name": field_name, - "issue": "model_not_null_but_db_allows_null", - "model_constraint": "NOT NULL", - "db_constraint": "NULL", - "recommended_action": "disallow_null", - } - ) - - if table_inconsistencies: - inconsistencies[table_name] = table_inconsistencies - - except Exception as e: - logger.exception(f"检查字段约束时出错: {e}") - - return inconsistencies - - -def fix_image_id(): - """ - 修复表情包的 image_id 字段 - """ - import uuid - - try: - with db: - for img in Images.select(): - if not img.image_id: - img.image_id = str(uuid.uuid4()) - img.save() - logger.info(f"已为表情包 {img.id} 生成新的 image_id: {img.image_id}") - except Exception as e: - logger.exception(f"修复 image_id 时出错: {e}") - - -# 模块加载时调用初始化函数 -initialize_database(sync_constraints=True) -fix_image_id() + # 问答对 + question: str # 问题内容 + context: Optional[str] = Field(default=None, nullable=True) # 上下文 + found_answer: bool = Field(default=False) # 是否找到答案 + answer: Optional[str] = Field(default=None, nullable=True) # 问题答案 + + thinking_steps: Optional[str] = Field(default=None, nullable=True) # 思考步骤,JSON格式存储 + created_timestamp: datetime = Field(default_factory=datetime.now, index=True) # 创建时间 + updated_timestamp: datetime = Field(default_factory=datetime.now, index=True) # 最后更新时间