diff --git a/backend/app/core/Start.py b/backend/app/core/Start.py new file mode 100644 index 0000000..345a65c --- /dev/null +++ b/backend/app/core/Start.py @@ -0,0 +1,46 @@ +import base64 +from typing import Any, Dict +from IPython.core.magic_arguments import defaults +from .. import nodes + +class StartNode(): + name = "开始节点" + inputs = { + "user_input": "string", # 用户输入文本 + "stream": "boolean", # 是否流式输出 + "img_switch": "boolean", # 是否处理图片 + "table_switch": "boolean", # 是否处理表格 + "role_name": "string", # 角色名称 + "chat_name": "string" # 会话名称 + } + + async def run(self, text: str = None, image: bytes = None, **kwargs) -> Dict[str, Any]: + # 查空:文本不能为空字符串 + if not text or text.strip() == "": + raise ValueError("文本输入不能为空") + + # 查空:图片数据不能为空 + if image is None or len(image) == 0: + raise ValueError("图片输入不能为空") + + # 将图片字节转换为 Base64 字符串,便于在节点间传递 + image_base64 = base64.b64encode(image).decode('utf-8') + + return { + "text": text, + "image": image_base64 + } + +async def run(is_user,floor_number,mes: str = None, stream: bool = False, img_switch: bool = False,name = "default", + table_switch: bool = False, role_name: str = None, chat_name: str = None,preset: str = None): + # 将输入内容持久化存储到本地json方便前端读 + nodes.save_input_to_json(mes=mes, role_name=role_name, chat_name=chat_name, name=name, is_user=is_user, floor_number=floor_number) + # 对上一条输入内容(已确定不变的内容)调用向量化,根据role和chat嵌入到对应本地数据库 + embed_input(user_input, role_name, chat_name) + # 根据role和chat去读取绑定的世界书 + # 读取预设,进行拼接 + # 调用模型,返回结果 + # 将结果持久化存储到本地json方便前端读(用JSONL) + # 如果img_switch是开的,那么异步调用生图,并存储到目标文件夹里 + # 如果table_switch是开的,那么异步调用表格生成,并存储到目标文件夹里 + # 将结果返回给前端 \ No newline at end of file diff --git a/backend/app/nodes/Start.py b/backend/app/nodes/Start.py deleted file mode 100644 index 056c040..0000000 --- a/backend/app/nodes/Start.py +++ /dev/null @@ -1,29 +0,0 @@ -import base64 -from core.node_base import BaseNode -from typing import Any, Dict - - -class StartNode(BaseNode): - name = "开始节点" - inputs = {} # 没有输入参数 - outputs = { - "text": "string", # 文本内容 - "image": "string" # 图片以 Base64 编码的字符串传递 - } - - async def run(self, text: str = None, image: bytes = None, **kwargs) -> Dict[str, Any]: - # 查空:文本不能为空字符串 - if not text or text.strip() == "": - raise ValueError("文本输入不能为空") - - # 查空:图片数据不能为空 - if image is None or len(image) == 0: - raise ValueError("图片输入不能为空") - - # 将图片字节转换为 Base64 字符串,便于在节点间传递 - image_base64 = base64.b64encode(image).decode('utf-8') - - return { - "text": text, - "image": image_base64 - } \ No newline at end of file diff --git a/backend/app/nodes/save_input_to_json.py b/backend/app/nodes/save_input_to_json.py new file mode 100644 index 0000000..b4fa9b2 --- /dev/null +++ b/backend/app/nodes/save_input_to_json.py @@ -0,0 +1,145 @@ +import json +from typing import Dict, Any +from datetime import datetime +import config as cfg +from pathlib import Path + + +def save_input_to_json( + mes: str, + role_name: str, + chat_name: str, + name: str, + is_user: bool, + floor_number: int = 0 +) -> Dict[str, Any]: + """ + 保存消息到JSONL文件或处理重roll请求 + + 参数: + mes: 消息内容 + role_name: 角色名称 + chat_name: 对话名称 + name: 发送者名称 + is_user: 是否为用户消息 + floor_number: 楼层号(对话中的第几次回复),用于判断是否为重roll请求 + + 返回: + 更新后的消息对象 + """ + config = cfg.settings + file_path = config.BASE_PATH / "data" / "chat" / role_name / f"{chat_name}.jsonl" + + # 确保目录存在 + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + + # 读取文件内容 + try: + with open(file_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except FileNotFoundError: + lines = [] + + # 判断是否为重roll请求 + is_regenerate = False + target_index = -1 + + if lines and floor_number > 0: + # 计算当前楼层号 + current_floor = len(lines) + + # 如果floor_number与当前楼层号相同,则为重roll请求 + if floor_number == current_floor: + # 找到最后一条非用户消息 + for i in range(len(lines) - 1, -1, -1): + try: + line_data = json.loads(lines[i]) + if not line_data.get('is_user', False): + is_regenerate = True + target_index = i + break + except json.JSONDecodeError: + continue + + # 处理重roll逻辑 + if is_regenerate: + # 解析目标消息 + try: + target_message = json.loads(lines[target_index]) + except json.JSONDecodeError: + raise ValueError(f"无法解析楼层 {floor_number} 的JSON数据") + + # 初始化swipes数组 + if target_message.get('swipes') is None: + target_message['swipes'] = [] + + # 将新回复添加到swipes数组 + target_message['swipes'].append(mes) + + # 更新swipe_id和content + target_message['swipes_id'] = len(target_message['swipes']) - 1 + target_message['content'] = mes + + # 更新文件内容 + lines[target_index] = json.dumps(target_message, ensure_ascii=False) + '\n' + + # 写回文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.writelines(lines) + + return target_message + + # 处理普通消息保存逻辑 + else: + # 获取当前时间 + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # 构建消息对象 + message = { + "role": role_name, + "chat": chat_name, + "content": mes, + "name": name, + "is_user": is_user, + "send_date": current_time, + "floor_number": len(lines) + 1, # 记录楼层号 + "swipes": [], + "swipes_id": 0 + } + + # 追加到文件 + with open(file_path, 'a', encoding='utf-8') as f: + f.write(json.dumps(message, ensure_ascii=False) + '\n') + + return message + + +if __name__ == '__main__': + # 测试普通消息保存 + # save_input_to_json( + # mes="你好", + # role_name="test", + # chat_name="111", + # name="用户", + # is_user=True, + # floor_number=0 + # ) + # + # save_input_to_json( + # mes="你好,我是AI助手", + # role_name="test", + # chat_name="111", + # name="AI", + # is_user=False, + # floor_number=1 + # ) + + # 测试重roll最后一条AI消息 + save_input_to_json( + mes="这是重roll后的新回复2", + role_name="test", + chat_name="111", + name="AI", + is_user=False, + floor_number=2 # 与当前楼层号相同,表示重roll + ) diff --git a/backend/app/tool/load_chat_history.py b/backend/app/tool/load_chat_history.py new file mode 100644 index 0000000..e69de29 diff --git a/config.py b/config.py new file mode 100644 index 0000000..db25d60 --- /dev/null +++ b/config.py @@ -0,0 +1,45 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + +# 1. 动态计算项目根目录 +# 假设 config.py 位于 backend/ 目录下 +# __file__ 指向本文件的绝对路径 +# .parent 指向 backend/ 目录 +# .parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录) +PROJECT_ROOT = Path(__file__).resolve().parent + +# 2. 加载 .env 文件 +# 假设 .env 文件位于项目根目录下 +load_dotenv(PROJECT_ROOT / ".env") + + +class Settings: + # --- 主模型配置 --- + MAIN_LLM_API_KEY = os.getenv("MAIN_LLM_API_KEY") + MAIN_LLM_MODEL = os.getenv("MAIN_LLM_MODEL", "gpt-3.5-turbo") + MAIN_LLM_BASE_URL = os.getenv("MAIN_LLM_BASE_URL", "https://api.openai.com/v1") + MAIN_LLM_MAX_TOKENS = int(os.getenv("MAIN_LLM_MAX_TOKENS", "4096")) + MAIN_LLM_STREAM = os.getenv("MAIN_LLM_STREAM", "true").lower() == "true" + + # --- 路径配置 (核心修改) --- + + # 强制使用计算出的项目根目录,不再依赖 .env 中的 BASE_PATH + BASE_PATH = PROJECT_ROOT + + # 数据目录:固定为根目录下的 data 文件夹 + # 即使 .env 里写了 DATA_PATH=/data,这里也会强制指向项目根目录下的 data + DATA_PATH = BASE_PATH / "data" + + # 其他文件路径:基于 DATA_PATH 拼接 + STATE_FILE = DATA_PATH / "state.json" + SCHEMA_FILE = DATA_PATH / "schema.json" + PRESETS_FILE = DATA_PATH / "presets.json" + REGEX_FILE = DATA_PATH / "regex_rules.json" + VECTORSTORE_PATH = DATA_PATH / "vectorstore" + + # ... 其他配置 ... + + +# 实例化配置对象 +settings = Settings() diff --git a/data/chat/test/111.jsonl b/data/chat/test/111.jsonl new file mode 100644 index 0000000..c3ebaf5 --- /dev/null +++ b/data/chat/test/111.jsonl @@ -0,0 +1,2 @@ +{"role": "test", "chat": "111", "content": "你好", "name": "用户", "is_user": true, "send_date": "2026-03-12 18:26:50", "floor_number": 1, "swipes": [], "swipes_id": 0} +{"role": "test", "chat": "111", "content": "这是重roll后的新回复2", "name": "AI", "is_user": false, "send_date": "2026-03-12 18:26:50", "floor_number": 2, "swipes": ["这是重roll后的新回复", "这是重roll后的新回复2"], "swipes_id": 1}