From 7fc9e10c99b641644a713f11364072c86a16301f Mon Sep 17 00:00:00 2001 From: moranzhi Date: Sat, 2 May 2026 02:08:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=A4=A7=E9=87=8F=E7=BE=8E?= =?UTF-8?q?=E5=8C=96=EF=BC=8Czustand=E8=BF=81=E7=A7=BB=EF=BC=8C=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=A1=A8=E6=A0=BC=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/routes/chatsRoute.py | 48 +- backend/api/routes/presetsRoute.py | 48 +- backend/api/routes/worldbooksRoute.py | 112 ++-- backend/core/config.py | 15 +- backend/models/internal.py | 3 +- backend/services/__init__.py | 8 +- backend/services/character_card_converter.py | 1 + backend/services/character_service.py | 2 +- backend/services/chat_service.py | 73 ++- backend/services/preset_service.py | 203 +++++++ data/characters/测试角色1/character.json | 42 +- .../characters/测试角色1/chats/默认聊天.jsonl | 12 +- data/characters/测试角色2/character.json | 39 +- .../characters/测试角色2/chats/默认聊天.jsonl | 10 +- data/characters/测试角色3/character.json | 37 +- .../characters/测试角色3/chats/默认聊天.jsonl | 10 +- data/preset/Default.json | 381 ++++++------ frontend/src/App.jsx | 79 +-- frontend/src/Store/AppLayoutSlice.jsx | 96 +++ frontend/src/Store/Mid/ChatBoxSlice.jsx | 93 ++- frontend/src/Store/Mid/ChatBoxUISlice.jsx | 154 +++++ frontend/src/Store/Mid/index.js | 1 + .../SideBarLeft/CharacterCardUISlice.jsx | 130 ++++ .../src/Store/SideBarLeft/PresetSlice.jsx | 110 +++- frontend/src/Store/SideBarLeft/index.js | 1 + .../src/Store/SideBarRight/TableSlice.jsx | 255 ++++++++ frontend/src/Store/SideBarRight/index.js | 1 + frontend/src/Store/UserSlice.jsx | 48 ++ frontend/src/Store/indexStore.jsx | 8 +- .../src/components/Mid/ChatBox/ChatBox.css | 14 +- .../src/components/Mid/ChatBox/ChatBox.jsx | 73 +-- .../components/SideBarLeft/SideBarLeft.jsx | 15 +- .../SideBarLeft/tabs/ApiConfig/ApiConfig.css | 2 +- .../tabs/CharacterCard/CharacterCard.css | 38 +- .../tabs/CharacterCard/CharacterCard.jsx | 125 ++-- .../SideBarLeft/tabs/Presets/Presets.css | 382 ++++++------ .../SideBarLeft/tabs/Presets/Presets.jsx | 557 +++++++++++------- .../SideBarLeft/tabs/WorldBook/WorldBook.css | 2 +- .../SideBarRight/tabs/Dice/Dice.css | 3 +- .../SideBarRight/tabs/Dice/Dice.jsx | 33 +- .../SideBarRight/tabs/RagRecall/RagRecall.css | 2 +- .../SideBarRight/tabs/Table/Table.css | 275 +++++++++ .../SideBarRight/tabs/Table/Table.jsx | 194 +++++- frontend/src/components/TopBar/TopBar.jsx | 78 +-- frontend/src/index.css | 24 +- frontend/src/types/internal.types.ts | 13 +- test_data_access.py | 82 +++ test_route_import.py | 43 ++ test_sillytavern_compat.py | 94 --- test_simple_model_service.py | 51 -- test_worldbook_direct.py | 91 --- test_worldbook_pagination.py | 125 ---- test_worldbook_read.py | 80 --- 53 files changed, 2954 insertions(+), 1462 deletions(-) create mode 100644 backend/services/preset_service.py create mode 100644 frontend/src/Store/AppLayoutSlice.jsx create mode 100644 frontend/src/Store/Mid/ChatBoxUISlice.jsx create mode 100644 frontend/src/Store/SideBarLeft/CharacterCardUISlice.jsx create mode 100644 frontend/src/Store/SideBarRight/TableSlice.jsx create mode 100644 frontend/src/Store/UserSlice.jsx create mode 100644 frontend/src/components/SideBarRight/tabs/Table/Table.css create mode 100644 test_data_access.py create mode 100644 test_route_import.py delete mode 100644 test_sillytavern_compat.py delete mode 100644 test_simple_model_service.py delete mode 100644 test_worldbook_direct.py delete mode 100644 test_worldbook_pagination.py delete mode 100644 test_worldbook_read.py diff --git a/backend/api/routes/chatsRoute.py b/backend/api/routes/chatsRoute.py index 60295ce..1d257d9 100644 --- a/backend/api/routes/chatsRoute.py +++ b/backend/api/routes/chatsRoute.py @@ -1,12 +1,13 @@ from fastapi import APIRouter, HTTPException, status from pathlib import Path + try: - from backend.services.chat_service import ChatService - from backend.core.config import settings + from backend.services.chat_service import ChatService + from backend.core.config import settings except ImportError: - # Docker环境:直接从当前目录导入 - from services.chat_service import ChatService - from core.config import settings + # Docker环境:直接从当前目录导入 + from services.chat_service import ChatService + from core.config import settings router = APIRouter(prefix="/chat", tags=["chat"]) @@ -14,11 +15,23 @@ router = APIRouter(prefix="/chat", tags=["chat"]) data_path = Path(settings.DATA_PATH) if hasattr(settings, 'DATA_PATH') else Path("data") chat_service = ChatService(data_path) + @router.get("", response_model=dict) async def list_all_chats(): """获取所有角色的所有聊天列表""" return chat_service.list_all_chats() + +# 注意:路由定义顺序很重要!更具体的路由(更多参数)必须放在前面 +@router.get("/{role_name}/{chat_name}") +async def get_chat(role_name: str, chat_name: str): + """获取指定聊天的完整内容""" + try: + return chat_service.get_chat(role_name, chat_name) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + + @router.get("/{role_name}") async def list_role_chats(role_name: str): """获取指定角色的所有聊天列表""" @@ -30,13 +43,6 @@ async def list_role_chats(role_name: str): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@router.get("/{role_name}/{chat_name}") -async def get_chat(role_name: str, chat_name: str): - """获取指定聊天的完整内容""" - try: - return chat_service.get_chat(role_name, chat_name) - except FileNotFoundError as e: - raise HTTPException(status_code=404, detail=str(e)) @router.post("/{role_name}", status_code=status.HTTP_201_CREATED) async def create_chat(role_name: str, chat_data: dict): @@ -48,18 +54,21 @@ async def create_chat(role_name: str, chat_data: dict): except FileExistsError as e: raise HTTPException(status_code=400, detail=str(e)) + @router.put("/{role_name}/{chat_name}") async def update_chat(role_name: str, chat_name: str, update_data: dict): """更新聊天元数据""" # TODO: 实现更新聊天元数据功能 raise HTTPException(status_code=501, detail="Not Implemented") + @router.delete("/{role_name}/{chat_name}") async def delete_chat(role_name: str, chat_name: str): """删除指定聊天""" # TODO: 实现删除聊天功能 raise HTTPException(status_code=501, detail="Not Implemented") + @router.get("/{role_name}/{chat_name}/messages") async def list_messages(role_name: str, chat_name: str): """获取聊天的所有消息""" @@ -69,6 +78,7 @@ async def list_messages(role_name: str, chat_name: str): except FileNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) + @router.get("/{role_name}/{chat_name}/messages/{floor}") async def get_message(role_name: str, chat_name: str, floor: int): """获取指定楼层的消息""" @@ -81,6 +91,7 @@ async def get_message(role_name: str, chat_name: str, floor: int): except FileNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) + @router.post("/{role_name}/{chat_name}/messages", status_code=status.HTTP_201_CREATED) async def add_message(role_name: str, chat_name: str, message_data: dict): """向聊天添加新消息""" @@ -91,6 +102,7 @@ async def add_message(role_name: str, chat_name: str, message_data: dict): except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) + @router.put("/{role_name}/{chat_name}/messages/{floor}") async def update_message(role_name: str, chat_name: str, floor: int, update_data: dict): """更新指定楼层的消息""" @@ -101,6 +113,7 @@ async def update_message(role_name: str, chat_name: str, floor: int, update_data except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) + @router.delete("/{role_name}/{chat_name}/messages/{floor}") async def delete_message(role_name: str, chat_name: str, floor: int): """删除指定楼层的消息""" @@ -110,3 +123,14 @@ async def delete_message(role_name: str, chat_name: str, floor: int): raise HTTPException(status_code=404, detail=str(e)) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) + + +@router.put("/{role_name}/{chat_name}/table") +async def update_table_data(role_name: str, chat_name: str, table_update: dict): + """更新表格数据(带时间戳冲突解决)""" + try: + return chat_service.update_table_data(role_name, chat_name, table_update) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/api/routes/presetsRoute.py b/backend/api/routes/presetsRoute.py index f125b69..2220ef3 100644 --- a/backend/api/routes/presetsRoute.py +++ b/backend/api/routes/presetsRoute.py @@ -1,23 +1,43 @@ from fastapi import APIRouter, HTTPException, status -# TODO: 实现 PresetService 来替代旧的 AIDesignSpec 逻辑 -# from services.preset_service import PresetService +from services.preset_service import PresetService router = APIRouter(prefix="/presets", tags=["presets"]) @router.get("", response_model=dict) async def list_presets(): """获取所有预设列表及其基本信息""" - # return await PresetService.list_all_presets() - return {"presets": []} + try: + presets = PresetService.list_presets() + response_data = {"presets": presets} + print(f"[API] GET /api/presets - 返回数据: {response_data}") + return response_data + except Exception as e: + print(f"[API] GET /api/presets - 错误: {e}") + import traceback + traceback.print_exc() + raise HTTPException(status_code=500, detail=str(e)) + +# 注意:路由定义顺序很重要!更具体的路由(更多参数)必须放在前面 +@router.get("/{preset_name}/components/{component_id}") +async def get_preset_component(preset_name: str, component_id: str): + """获取指定组件的详情""" + raise HTTPException(status_code=501, detail="Not Implemented") + +@router.get("/{preset_name}/components") +async def list_preset_components(preset_name: str): + """获取预设中的所有组件""" + raise HTTPException(status_code=501, detail="Not Implemented") @router.get("/{preset_name}") async def get_preset(preset_name: str): """获取指定预设的完整内容""" - # try: - # return await PresetService.get_preset(preset_name) - # except FileNotFoundError: - # raise HTTPException(status_code=404, detail="Preset not found") - raise HTTPException(status_code=501, detail="Not Implemented") + try: + preset_data = PresetService.get_preset(preset_name) + return preset_data + except FileNotFoundError: + raise HTTPException(status_code=404, detail="Preset not found") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) @router.post("", status_code=status.HTTP_201_CREATED) async def create_preset(preset_name: str, preset_data: dict): @@ -34,16 +54,6 @@ async def delete_preset(preset_name: str): """删除指定预设""" raise HTTPException(status_code=501, detail="Not Implemented") -@router.get("/{preset_name}/components") -async def list_preset_components(preset_name: str): - """获取预设中的所有组件""" - raise HTTPException(status_code=501, detail="Not Implemented") - -@router.get("/{preset_name}/components/{component_id}") -async def get_preset_component(preset_name: str, component_id: str): - """获取指定组件的详情""" - raise HTTPException(status_code=501, detail="Not Implemented") - @router.post("/{preset_name}/components", status_code=status.HTTP_201_CREATED) async def add_preset_component(preset_name: str, component_data: dict): """向预设添加新组件""" diff --git a/backend/api/routes/worldbooksRoute.py b/backend/api/routes/worldbooksRoute.py index cf6b296..36a0afd 100644 --- a/backend/api/routes/worldbooksRoute.py +++ b/backend/api/routes/worldbooksRoute.py @@ -38,6 +38,80 @@ async def list_worldbooks(): logger.error(f"Failed to list worldbooks: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) +# 注意:路由定义顺序很重要!更具体的路由(更多参数)必须放在前面 +@router.get("/{name}/entries/{uid}", response_model=Dict[str, Any]) +async def get_worldbook_entry(name: str, uid: str): + """ + 获取世界书的指定条目 + """ + try: + return worldbook_service.get_entry(name, uid) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + logger.error(f"Failed to get entry '{uid}' from worldbook '{name}': {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/{name}/entries", response_model=Dict[str, Any]) +async def list_worldbook_entries( + name: str, + page: int = 1, + page_size: int = 20 +): + """ + 获取世界书的条目列表(支持分页) + + Args: + name: 世界书名称 + page: 页码,从1开始 + page_size: 每页数量,默认20 + """ + try: + return worldbook_service.list_entries(name, page, page_size) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + logger.error(f"Failed to list entries for worldbook '{name}': {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/{name}/export") +async def export_worldbook(name: str, format: str = "internal"): + """ + 导出世界书(支持 internal 和 sillytavern 两种格式) + + Args: + name: 世界书名称 + format: 导出格式 ('internal' 或 'sillytavern'),默认 internal + """ + try: + if format.lower() == "sillytavern": + # 导出为 SillyTavern 格式(可能丢失特殊设置) + logger.info(f"导出世界书 '{name}' 为 SillyTavern 格式") + st_data = worldbook_service.export_to_sillytavern(name) + + return JSONResponse( + content=st_data, + headers={ + "Content-Disposition": f"attachment; filename={name}_sillytavern.json" + } + ) + else: + # 导出为内部格式(保留所有设置) + logger.info(f"导出世界书 '{name}' 为内部格式") + internal_data = worldbook_service.get_worldbook(name) + + return JSONResponse( + content=internal_data, + headers={ + "Content-Disposition": f"attachment; filename={name}.json" + } + ) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + logger.error(f"Failed to export worldbook '{name}': {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + @router.get("/{name}", response_model=Dict[str, Any]) async def get_worldbook(name: str): """ @@ -213,41 +287,3 @@ async def import_worldbook(name: str, file: UploadFile = File(...)): except Exception as e: logger.error(f"Failed to import worldbook '{name}': {str(e)}") raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/{name}/export") -async def export_worldbook(name: str, format: str = "internal"): - """ - 导出世界书(支持 internal 和 sillytavern 两种格式) - - Args: - name: 世界书名称 - format: 导出格式 ('internal' 或 'sillytavern'),默认 internal - """ - try: - if format.lower() == "sillytavern": - # 导出为 SillyTavern 格式(可能丢失特殊设置) - logger.info(f"导出世界书 '{name}' 为 SillyTavern 格式") - st_data = worldbook_service.export_to_sillytavern(name) - - return JSONResponse( - content=st_data, - headers={ - "Content-Disposition": f"attachment; filename={name}_sillytavern.json" - } - ) - else: - # 导出为内部格式(保留所有设置) - logger.info(f"导出世界书 '{name}' 为内部格式") - internal_data = worldbook_service.get_worldbook(name) - - return JSONResponse( - content=internal_data, - headers={ - "Content-Disposition": f"attachment; filename={name}.json" - } - ) - except FileNotFoundError as e: - raise HTTPException(status_code=404, detail=str(e)) - except Exception as e: - logger.error(f"Failed to export worldbook '{name}': {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/core/config.py b/backend/core/config.py index 8c5071a..3ed83b4 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -3,12 +3,15 @@ from pathlib import Path from dotenv import load_dotenv # 1. 动态计算项目根目录 -# 假设 config.py 位于 backend/core/ 目录下 -# __file__ 指向本文件的绝对路径 -# .parent 指向 backend/core/ 目录 -# .parent.parent 指向 backend/ 目录 -# .parent.parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录) -PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +# 在 Docker 环境中:config.py 位于 /app/core/,需要向上2级到 /app/ +# 在本地开发中:config.py 位于 backend/core/,需要向上3级到项目根目录 +_config_path = Path(__file__).resolve() +if _config_path.parent.parent.name == 'app': + # Docker 环境:/app/core/config.py -> /app/ + PROJECT_ROOT = _config_path.parent.parent +else: + # 本地开发:backend/core/config.py -> 项目根目录 + PROJECT_ROOT = _config_path.parent.parent.parent # 2. 加载 .env 文件 # 假设 .env 文件位于项目根目录下 diff --git a/backend/models/internal.py b/backend/models/internal.py index 7d5aa39..02ab03c 100644 --- a/backend/models/internal.py +++ b/backend/models/internal.py @@ -136,6 +136,7 @@ class CharacterCard(BaseModel): first_mes: str = Field(..., description="首条开场消息") mes_example: str = Field(..., description="对话示例") categories: List[str] = Field(default_factory=list, description="分类标签 (用于前端筛选)") + tags: Optional[List[str]] = Field(None, description="动态表格标签数组 (SillyTavern 关键字机制)") worldInfoId: Optional[str] = Field(None, description="绑定的世界书 ID") outputSchema: Optional[List[OutputSchemaField]] = Field(None, description="输出 schema 定义 (结构化输出)") avatarPath: Optional[str] = Field(None, description="角色头像路径") @@ -161,7 +162,7 @@ class ChatHeader(BaseModel): characterId: str = Field(..., description="关联的角色卡 ID") userName: str = Field("User", description="用户角色名") characterName: str = Field(..., description="AI 角色名称") - tableData: Optional[Dict[str, Any]] = Field(None, description="表格数据 (对应 outputSchema)") + tags: Optional[List[str]] = Field(None, description="动态表格标签数组 (从角色卡继承)") createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") messageCount: int = Field(0, description="消息数量") diff --git a/backend/services/__init__.py b/backend/services/__init__.py index cce6664..be1e981 100644 --- a/backend/services/__init__.py +++ b/backend/services/__init__.py @@ -3,9 +3,7 @@ 包含项目的核心业务逻辑,协调 Models、Utils 和 LLM 组件。 """ -from .prompt_assembler import PromptAssembler, PromptConfig +# 注意:不在这里自动导入模块,避免循环依赖和缺失依赖问题 +# 需要使用时请显式导入,例如:from services.preset_service import PresetService -__all__ = [ - 'PromptAssembler', - 'PromptConfig', -] +__all__ = [] diff --git a/backend/services/character_card_converter.py b/backend/services/character_card_converter.py index 5a48060..7cd43c5 100644 --- a/backend/services/character_card_converter.py +++ b/backend/services/character_card_converter.py @@ -50,6 +50,7 @@ class CharacterCardConverter: first_mes=data.get('first_mes', ''), mes_example=data.get('mes_example', ''), categories=[], # ST没有categories + tableHeaders=[], # ST没有tableHeaders worldInfoId=extensions.get('world'), outputSchema=None, # ST不支持结构化输出 avatarPath=avatar_path, diff --git a/backend/services/character_service.py b/backend/services/character_service.py index 7f9ecf2..584dcfe 100644 --- a/backend/services/character_service.py +++ b/backend/services/character_service.py @@ -95,11 +95,11 @@ class CharacterService: first_mes=data.get('first_mes', ''), mes_example=data.get('mes_example', ''), categories=data.get('categories', []), + tags=data.get('tags', []), # ✅ 使用标签数组 worldInfoId=data.get('worldInfoId'), outputSchema=data.get('outputSchema'), avatarPath=avatar_path, alternate_greetings=data.get('alternate_greetings', []), - tags=data.get('tags', []), createdAt=data.get('createdAt', int(datetime.now().timestamp())), updatedAt=data.get('updatedAt', int(datetime.now().timestamp())), lastChatAt=last_chat_at, diff --git a/backend/services/chat_service.py b/backend/services/chat_service.py index bfd6f19..4bdc383 100644 --- a/backend/services/chat_service.py +++ b/backend/services/chat_service.py @@ -156,6 +156,7 @@ class ChatService: messages.append(msg_data) return { + "header": header, # 完整的 header,包含 tableHeaders, tableDefaults, tableData "metadata": { "user_name": header.get("user_name", "User"), "character_name": header.get("character_name", ""), @@ -192,6 +193,21 @@ class ChatService: # 创建角色目录 chat_file.parent.mkdir(parents=True, exist_ok=True) + # 尝试从角色卡获取 tags(关键字列表) + tags = [] + + try: + character_file = Path("data/characters") / role_name / "character.json" + if character_file.exists(): + with open(character_file, 'r', encoding='utf-8') as f: + character_data = json.load(f) + + tags = character_data.get('tags', []) + + logger.info(f"从角色卡 {role_name} 继承标签: {tags}") + except Exception as e: + logger.warning(f"读取角色卡失败,使用空标签: {e}") + # 构建header header = { "user_name": metadata.get("user_name", "User") if metadata else "User", @@ -207,7 +223,8 @@ class ChatService: "timedWorldInfo": {}, "variables": {}, "tainted": False, - "lastInContextMessageId": -1 + "lastInContextMessageId": -1, + "tags": tags # ✅ 使用标签数组替代 tableHeaders/tableDefaults/tableData } # 写入header @@ -381,3 +398,57 @@ class ChatService: except Exception as e: logger.error(f"删除消息失败 {role_name}/{chat_name}/{floor}: {str(e)}") raise + + def update_table_data(self, role_name: str, chat_name: str, table_update: Dict) -> Dict: + """ + 更新标签数据(SillyTavern 关键字机制) + + Args: + role_name: 角色名称 + chat_name: 聊天名称 + table_update: 包含 tags 数组的字典 + + Returns: + Dict: 更新后的标签数据 + + Raises: + FileNotFoundError: 聊天文件不存在 + """ + chat_file = self.chat_dir / role_name / f"{chat_name}.jsonl" + + if not chat_file.exists(): + raise FileNotFoundError(f"Chat not found: {role_name}/{chat_name}") + + try: + with open(chat_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + if not lines: + raise ValueError(f"Empty chat file: {role_name}/{chat_name}") + + # 读取 header + header = json.loads(lines[0]) + + # 获取新的标签数组 + new_tags = table_update.get('tags', []) + + # 更新 header 中的 tags + header['tags'] = new_tags + + # 写回文件 + lines[0] = json.dumps(header, ensure_ascii=False) + '\n' + + with open(chat_file, 'w', encoding='utf-8') as f: + f.writelines(lines) + + logger.info(f"标签数据已更新: {role_name}/{chat_name}, 标签数: {len(new_tags)}") + + return { + "success": True, + "tags": new_tags, + "tagCount": len(new_tags) + } + + except Exception as e: + logger.error(f"更新标签数据失败 {role_name}/{chat_name}: {str(e)}") + raise diff --git a/backend/services/preset_service.py b/backend/services/preset_service.py new file mode 100644 index 0000000..2c8b3e5 --- /dev/null +++ b/backend/services/preset_service.py @@ -0,0 +1,203 @@ +""" +Preset Service +预设服务层 - 处理预设的 CRUD 操作 +""" +import json +import os +from pathlib import Path +from typing import List, Dict, Any, Optional +from datetime import datetime + +from core.config import settings + + +class PresetService: + """预设服务类""" + + @staticmethod + def _extract_preset_name_from_filename(filename: str) -> str: + """ + 从文件名提取预设名称,去掉时间戳和文件后缀 + + Args: + filename: 文件名(不含路径) + + Returns: + 清理后的预设名称 + + Examples: + "Default.json" -> "Default" + "MyPreset_1234567890.json" -> "MyPreset" + "Test_1714567890123.json" -> "Test" + """ + # 去掉 .json 后缀 + name = filename.replace('.json', '') + + # 去掉末尾的时间戳(下划线+数字组合) + # 匹配模式:_后面跟着10-13位数字(Unix时间戳) + import re + name = re.sub(r'_\d{10,13}$', '', name) + + return name + + @staticmethod + def _get_preset_path(name: str) -> Path: + """获取预设文件路径""" + return settings.PRESET_PATH / f"{name}.json" + + @staticmethod + def _load_preset(name: str) -> Optional[Dict[str, Any]]: + """加载预设 JSON 文件""" + path = PresetService._get_preset_path(name) + if not path.exists(): + return None + + try: + with open(path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + raise ValueError(f"Failed to load preset '{name}': {str(e)}") + + @staticmethod + def _save_preset(name: str, data: Dict[str, Any]): + """保存预设到 JSON 文件""" + path = PresetService._get_preset_path(name) + try: + with open(path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except Exception as e: + raise ValueError(f"Failed to save preset '{name}': {str(e)}") + + @staticmethod + def list_presets() -> List[Dict[str, Any]]: + """ + 获取所有预设的列表(仅基本信息) + + Returns: + 预设列表,每个包含 name, description, component_count, temperature 等 + """ + presets = [] + + for json_file in settings.PRESET_PATH.glob("*.json"): + try: + with open(json_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 计算组件数量 + entries = data.get("entries", []) + prompts = data.get("prompts", []) + component_count = len(entries) if entries else len(prompts) + + # 提取温度参数(支持内部结构和 SillyTavern 结构) + temperature = data.get("temperature", 1.0) + + # 从文件名提取预设名称(去掉时间戳和后缀) + preset_name = PresetService._extract_preset_name_from_filename(json_file.name) + + preset_info = { + "name": preset_name, + "description": data.get("description", ""), + "component_count": component_count, + "temperature": temperature + } + + presets.append(preset_info) + except Exception as e: + print(f"Error loading preset {json_file.name}: {e}") + continue + + # 按名称排序 + presets.sort(key=lambda x: x.get("name", "")) + return presets + + @staticmethod + def get_preset(name: str) -> Dict[str, Any]: + """ + 获取指定预设的完整数据 + + Args: + name: 预设名称 + + Returns: + 预设完整数据 + """ + data = PresetService._load_preset(name) + if not data: + raise FileNotFoundError(f"Preset '{name}' not found") + + return data + + @staticmethod + def create_preset(name: str, preset_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 创建新预设 + + Args: + name: 预设名称 + preset_data: 预设数据 + + Returns: + 创建的预设数据 + """ + # 检查是否已存在 + if PresetService._get_preset_path(name).exists(): + raise ValueError(f"Preset '{name}' already exists") + + # 确保有必要的字段 + if "name" not in preset_data: + preset_data["name"] = name + + # 添加时间戳 + now = int(datetime.now().timestamp()) + if "createdAt" not in preset_data: + preset_data["createdAt"] = now + if "updatedAt" not in preset_data: + preset_data["updatedAt"] = now + + PresetService._save_preset(name, preset_data) + return preset_data + + @staticmethod + def update_preset(name: str, update_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 更新预设 + + Args: + name: 预设名称 + update_data: 要更新的数据 + + Returns: + 更新后的预设数据 + """ + data = PresetService._load_preset(name) + if not data: + raise FileNotFoundError(f"Preset '{name}' not found") + + # 更新字段 + for key, value in update_data.items(): + if key not in ["name", "createdAt"]: # 不允许修改名称和创建时间 + data[key] = value + + # 更新时间戳 + data["updatedAt"] = int(datetime.now().timestamp()) + + PresetService._save_preset(name, data) + return data + + @staticmethod + def delete_preset(name: str) -> bool: + """ + 删除预设 + + Args: + name: 预设名称 + + Returns: + 是否删除成功 + """ + path = PresetService._get_preset_path(name) + if not path.exists(): + raise FileNotFoundError(f"Preset '{name}' not found") + + path.unlink() + return True diff --git a/data/characters/测试角色1/character.json b/data/characters/测试角色1/character.json index 59e5880..82e8f49 100644 --- a/data/characters/测试角色1/character.json +++ b/data/characters/测试角色1/character.json @@ -1,23 +1,41 @@ { "id": "test-character-1", "name": "测试角色1", - "description": "这是一个测试角色,用于验证角色卡功能", - "personality": "友好、乐于助人、幽默", - "scenario": "日常对话场景", - "first_mes": "你好!我是测试角色1,很高兴见到你!", - "mes_example": "", - "categories": ["测试", "示例"], - "tags": ["test", "demo", "friendly"], + "description": "一位经验丰富的冒险者公会接待员,负责为冒险者们分配任务、提供情报和记录功绩。她见多识广,对大陆各地的传闻了如指掌。", + "personality": "热情、专业、细心、善于观察、有点八卦", + "scenario": "奇幻世界中的冒险者公会,玩家前来接取任务或报告完成情况", + "first_mes": "欢迎来到冒险者公会!✨ 我是接待员艾莉娅。请问您是来接取任务,还是来报告已完成的任务呢?对了,最近有个关于古代遗迹的委托,报酬相当丰厚哦~", + "mes_example": "\nUser: 有什么适合新手的任务吗?\n{{char}}: 让我看看...啊,正好有几个适合新手的委托!\n\n📋 **任务列表:**\n1. **清理哥布林巢穴** - E级任务\n - 地点:北边森林\n - 报酬:50金币\n - 难度:★☆☆☆☆\n\n2. **采集药草** - E级任务\n - 地点:东边山谷\n - 报酬:30金币\n - 难度:★☆☆☆☆\n\n您想接哪个任务呢?我可以为您提供更详细的情报哦!😊", + "categories": [ + "测试", + "NPC", + "跑团" + ], + "tags": [ + "test", + "rpg", + "npc", + "receptionist", + "fantasy" + ], + "tableHeaders": [ + "HP", + "MP" + ], + "tableDefaults": { + "HP": 0, + "MP": 0 + }, "worldInfoId": null, "outputSchema": null, - "avatarPath": null, + "avatarPath": "/api/characters/测试角色1/avatar", "alternate_greetings": [ - "嗨!有什么我可以帮你的吗?", - "欢迎来到测试世界!" + "哟,冒险者!今天公会的公告栏又更新了不少新任务呢!", + "欢迎回来!上次那个任务完成得怎么样?" ], "createdAt": 1700000000, - "updatedAt": 1700000000, + "updatedAt": 1777657069, "lastChatAt": null, "isFavorite": false, "version": 1 -} +} \ No newline at end of file diff --git a/data/characters/测试角色1/chats/默认聊天.jsonl b/data/characters/测试角色1/chats/默认聊天.jsonl index e96d871..aa2aa35 100644 --- a/data/characters/测试角色1/chats/默认聊天.jsonl +++ b/data/characters/测试角色1/chats/默认聊天.jsonl @@ -1,4 +1,8 @@ -{"user_name": "User", "character_name": "测试角色1", "create_date": "2026-04-30T15:00:00Z"} -{"name": "测试角色1", "is_user": false, "send_date": "2026-04-30T15:00:01Z", "mes": "你好!我是测试角色1,很高兴见到你!"} -{"name": "User", "is_user": true, "send_date": "2026-04-30T15:00:10Z", "mes": "你好!今天过得怎么样?"} -{"name": "测试角色1", "is_user": false, "send_date": "2026-04-30T15:00:15Z", "mes": "我很好,谢谢关心!你呢?"} +{"user_name": "User", "character_name": "测试角色1", "chat_id_hash": "rpg-chat-001", "integrity": "rpg-integrity-001", "note_prompt": "", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1, "tableHeaders": ["HP", "MP", "等级", "经验值", "金币", "状态"], "tableDefaults": {"HP": 100, "MP": 50, "等级": 1, "经验值": 0, "金币": 100, "状态": "正常"}, "tableData": [{"HP": 100, "MP": 50, "等级": 1, "经验值": 0, "金币": 100, "状态": "正常"}], "tableTimestamps": {"0": 1746115200}} +{"name": "测试角色1", "is_user": false, "is_system": false, "floor": 1, "send_date": "1746115201000", "mes": "欢迎来到冒险者公会!✨ 我是接待员艾莉娅。请问您是来接取任务,还是来报告已完成的任务呢?对了,最近有个关于古代遗迹的委托,报酬相当丰厚哦~", "extra": {}, "swipes": ["欢迎来到冒险者公会!✨ 我是接待员艾莉娅。请问您是来接取任务,还是来报告已完成的任务呢?对了,最近有个关于古代遗迹的委托,报酬相当丰厚哦~", "哟,新的面孔!欢迎来到冒险者公会~ 我是这里的接待员艾莉娅。看你装备不错,应该是个有经验的冒险者吧?", "哈喽!今天公会的公告栏可热闹了,好几个高报酬的委托等着有缘人呢!我是艾莉娅,有什么可以帮你的吗?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 2, "send_date": "1746115210000", "mes": "我想了解一下那个古代遗迹的委托", "extra": {}, "swipes": ["我想了解一下那个古代遗迹的委托"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色1", "is_user": false, "is_system": false, "floor": 3, "send_date": "1746115220000", "mes": "哦?你对这个委托感兴趣啊!让我找找记录...\n\n📜 **委托详情:**\n\n**任务名称:** 探索失落的精灵遗迹\n**等级要求:** B级及以上\n**委托方:** 王立魔法学院\n**报酬:** 500金币 + 稀有魔法道具(可选)\n\n**任务描述:**\n在北方山脉深处发现了一处古代精灵遗迹的入口。学院需要有人进入遗迹,带回其中的魔法文献和 artifacts。\n\n**已知情报:**\n- 遗迹入口处有魔法封印,需要解除\n- 内部可能有守护魔像或陷阱\n- 据说藏有上古精灵的魔法知识\n\n**风险提示:** ⚠️\n之前有两支冒险小队进入后失联,建议组队前往,至少需要一名法师和一名战士。\n\n你觉得如何?要接下这个委托吗?还是需要我提供更多信息?🤔", "extra": {}, "swipes": ["哦?你对这个委托感兴趣啊!让我找找记录...\n\n📜 **委托详情:**\n\n**任务名称:** 探索失落的精灵遗迹\n**等级要求:** B级及以上\n**委托方:** 王立魔法学院\n**报酬:** 500金币 + 稀有魔法道具(可选)\n\n**任务描述:**\n在北方山脉深处发现了一处古代精灵遗迹的入口。学院需要有人进入遗迹,带回其中的魔法文献和 artifacts。\n\n**已知情报:**\n- 遗迹入口处有魔法封印,需要解除\n- 内部可能有守护魔像或陷阱\n- 据说藏有上古精灵的魔法知识\n\n**风险提示:** ⚠️\n之前有两支冒险小队进入后失联,建议组队前往,至少需要一名法师和一名战士。\n\n你觉得如何?要接下这个委托吗?还是需要我提供更多信息?🤔", "这个委托可不简单呢!是王立魔法学院发布的,要去探索北方的精灵遗迹。报酬很诱人——500金币加魔法道具!不过风险也不小,已经有两支队伍失踪了...你确定要接吗?", "精灵遗迹啊...这可是个大活儿!学院那边开价500金币,还允许挑选一件遗迹里的魔法物品作为额外报酬。但说实话,那地方挺邪门的,之前的冒险者都没回来。你要慎重考虑哦!"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 4, "send_date": "1746115230000", "mes": "我决定接下这个任务!我需要做什么准备?", "extra": {}, "swipes": ["我决定接下这个任务!我需要做什么准备?", "我接了!给我登记一下。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色1", "is_user": false, "is_system": false, "floor": 5, "send_date": "1746115240000", "mes": "太好了!我就知道你是个有胆量的冒险者!💪\n\n让我为你登记...\n\n✅ **任务已接受!**\n\n**接下来你需要做的准备:**\n\n1. **组队建议** 👥\n - 至少3-4人小队\n - 推荐配置:战士、法师、牧师、游侠\n - 如果 solo,确保你有足够的生存能力\n\n2. **必备物品** 🎒\n - 治疗药水 x5(至少)\n - 解毒剂 x3\n - 绳索、火把、铁镐\n - 解除陷阱的工具\n\n3. **情报收集** 📖\n - 去图书馆查阅精灵文明的历史\n - 找老冒险者打听北方山脉的情况\n - 购买一份详细的地形图\n\n4. **装备检查** ⚔️\n - 武器是否锋利/法杖是否完好\n - 护甲是否有破损\n - 带上对抗不死生物的圣水(以防万一)\n\n**另外...**\n我可以帮你联系其他正在找任务的冒险者。今天下午在\"醉龙酒馆\"有个冒险者聚会,很多人都在找队友。\n\n你需要我帮你安排吗?还是你已经有心仪的队友了?😊\n\n---\n*要进行属性检定吗?比如:智力检定来回忆关于精灵遗迹的知识,或者魅力检定来说服我给你更多情报~*", "extra": {}, "swipes": ["太好了!我就知道你是个有胆量的冒险者!💪\n\n让我为你登记...\n\n✅ **任务已接受!**\n\n**接下来你需要做的准备:**\n\n1. **组队建议** 👥\n - 至少3-4人小队\n - 推荐配置:战士、法师、牧师、游侠\n - 如果 solo,确保你有足够的生存能力\n\n2. **必备物品** 🎒\n - 治疗药水 x5(至少)\n - 解毒剂 x3\n - 绳索、火把、铁镐\n - 解除陷阱的工具\n\n3. **情报收集** 📖\n - 去图书馆查阅精灵文明的历史\n - 找老冒险者打听北方山脉的情况\n - 购买一份详细的地形图\n\n4. **装备检查** ⚔️\n - 武器是否锋利/法杖是否完好\n - 护甲是否有破损\n - 带上对抗不死生物的圣水(以防万一)\n\n**另外...**\n我可以帮你联系其他正在找任务的冒险者。今天下午在\"醉龙酒馆\"有个冒险者聚会,很多人都在找队友。\n\n你需要我帮你安排吗?还是你已经有心仪的队友了?😊\n\n---\n*要进行属性检定吗?比如:智力检定来回忆关于精灵遗迹的知识,或者魅力检定来说服我给你更多情报~*", "勇敢的选择!我已经把你的名字登记在册了。记住,活着回来才是最重要的。准备好装备,组好队伍,然后就可以出发了。祝你好运,冒险者!✨", "登记完成!这个委托的期限是一个月,希望你能在规定时间内完成。对了,如果你需要的话,我可以给你打个折买一些基础补给品,算是给新手的优惠吧~"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 6, "send_date": "1746115250000", "mes": "/roll 1d20+3 智力检定,我想回忆一下关于精灵遗迹的知识", "extra": {}, "swipes": ["/roll 1d20+3 智力检定,我想回忆一下关于精灵遗迹的知识"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色1", "is_user": false, "is_system": false, "floor": 7, "send_date": "1746115260000", "mes": "*艾莉娅露出赞许的微笑*\n\n哦?你想进行智力检定来获取额外情报?很有想法!\n\n🎲 **智力检定结果:1d20+3 = 15+3 = 18** ✨\n\n**大成功!**\n\n你的脑海中浮现出一些模糊的记忆...\n\n---\n\n📚 **你回忆起的情报:**\n\n1. **精灵文明特点**\n - 古代精灵擅长自然魔法和结界术\n - 他们的建筑通常与自然融为一体\n - 遗迹中可能有活体植物构成的守卫\n\n2. **关于这处遗迹**\n - 传说这是\"月光精灵\"一族的圣地\n - 入口的封印需要月相正确时才能完全解除\n - 满月之夜(3天后)是最佳进入时机\n\n3. **潜在危险**\n - 精灵喜欢用幻术和迷宫保护重要场所\n - 可能会有元素生物或构装体守卫\n - 小心不要破坏任何看似普通的雕像或壁画\n\n4. **有价值的目标**\n - 中央大厅应该有记载魔法的晶石\n - 图书馆区域可能保存着卷轴\n - 最深处的祭坛上可能有神器\n\n---\n\n哇,你的记忆力真不错!这些情报可是价值连城啊!\n\n💡 **建议:**\n既然知道了满月是最佳时机,你可以:\n- 利用这3天时间充分准备\n- 寻找懂精灵语的队友解读文字\n- 准备对抗幻术的道具(真视药水等)\n\n还要我帮你做些什么吗?比如联系翻译或者购买特殊道具?😄", "extra": {}, "swipes": ["*艾莉娅露出赞许的微笑*\n\n哦?你想进行智力检定来获取额外情报?很有想法!\n\n🎲 **智力检定结果:1d20+3 = 15+3 = 18** ✨\n\n**大成功!**\n\n你的脑海中浮现出一些模糊的记忆...\n\n---\n\n📚 **你回忆起的情报:**\n\n1. **精灵文明特点**\n - 古代精灵擅长自然魔法和结界术\n - 他们的建筑通常与自然融为一体\n - 遗迹中可能有活体植物构成的守卫\n\n2. **关于这处遗迹**\n - 传说这是\"月光精灵\"一族的圣地\n - 入口的封印需要月相正确时才能完全解除\n - 满月之夜(3天后)是最佳进入时机\n\n3. **潜在危险**\n - 精灵喜欢用幻术和迷宫保护重要场所\n - 可能会有元素生物或构装体守卫\n - 小心不要破坏任何看似普通的雕像或壁画\n\n4. **有价值的目标**\n - 中央大厅应该有记载魔法的晶石\n - 图书馆区域可能保存着卷轴\n - 最深处的祭坛上可能有神器\n\n---\n\n哇,你的记忆力真不错!这些情报可是价值连城啊!\n\n💡 **建议:**\n既然知道了满月是最佳时机,你可以:\n- 利用这3天时间充分准备\n- 寻找懂精灵语的队友解读文字\n- 准备对抗幻术的道具(真视药水等)\n\n还要我帮你做些什么吗?比如联系翻译或者购买特殊道具?😄", "*掷骰子声* 1d20+3...结果是18!太棒了!你想起了一些关键信息:这是月光精灵的圣地,最好在满月时进入。精灵遗迹通常有幻术保护,记得带真视药水。还需要其他帮助吗?", "检定通过!你获得了宝贵的情报。看来这次探险的成功率提高了不少呢。要不要趁这三天去找个懂古精灵语的学者同行?我可以帮你牵线搭桥哦~"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} diff --git a/data/characters/测试角色2/character.json b/data/characters/测试角色2/character.json index ebba76c..c954e7b 100644 --- a/data/characters/测试角色2/character.json +++ b/data/characters/测试角色2/character.json @@ -1,20 +1,35 @@ { "id": "test-character-2", - "name": "测试角色2", - "description": "第二个测试角色,有不同的标签", - "personality": "严肃、专业、认真", - "scenario": "工作场景", - "first_mes": "您好,我是测试角色2,请问有什么工作需要处理?", - "mes_example": "", - "categories": ["测试", "工作"], - "tags": ["test", "professional", "work"], + "name": "帝国骑士维尔", + "description": "王国的骑士团长,外表高傲但内心温柔。她忠诚勇敢,对弱者充满同情心,只是不善于表达。虽然总是摆出一副严肃的样子,但实际上非常关心同伴。", + "personality": "傲娇、忠诚、勇敢、口是心非、责任感强、外冷内热", + "scenario": "中世纪奇幻世界,王国骑士团的训练场或任务途中", + "first_mes": "哼,你终于来了。我可是等了你很久...才没有!只是刚好在这里训练而已。既然你来了,那就陪我练习一下剑术吧。别误会,我只是想看看你的实力有没有退步!", + "mes_example": "\nUser: 艾德琳,今天的训练怎么样?\n{{char}}: *擦拭着剑刃,头也不抬* 还、还行吧。不过你的姿势还有很多问题...我才不是在担心你!只是如果队友太弱的话,会影响任务的完成度而已。\n\n\nUser: 谢谢你昨天的帮助\n{{char}}: *脸微微发红* 什、什么帮助?我什么都没做!那只是...只是碰巧路过而已。你不要自作多情了!笨蛋...", + "categories": [ + "测试", + "角色扮演", + "傲娇" + ], + "tags": [ + "test", + "tsundere", + "knight", + "fantasy", + "roleplay" + ], + "tableHeaders": [], + "tableDefaults": {}, "worldInfoId": null, "outputSchema": null, - "avatarPath": null, - "alternate_greetings": [], + "avatarPath": "/api/characters/测试角色2/avatar", + "alternate_greetings": [ + "又是你...我才没有一直在等你呢!", + "喂,站在那里干嘛?过来帮我拿一下装备!" + ], "createdAt": 1700000100, - "updatedAt": 1700000100, + "updatedAt": 1777654599, "lastChatAt": null, "isFavorite": true, "version": 1 -} +} \ No newline at end of file diff --git a/data/characters/测试角色2/chats/默认聊天.jsonl b/data/characters/测试角色2/chats/默认聊天.jsonl index 6e1443e..26e0cb4 100644 --- a/data/characters/测试角色2/chats/默认聊天.jsonl +++ b/data/characters/测试角色2/chats/默认聊天.jsonl @@ -1,2 +1,8 @@ -{"user_name": "User", "character_name": "测试角色2", "integrity": "644e9983-2102-4608-aeb5-64016c1ba92a", "chat_id_hash": "3f37a950-dda3-4341-97a8-50e58da796ce", "note_prompt": "", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} -{"name": "测试角色2", "is_user": false, "is_system": false, "floor": 1, "send_date": "1777569143941", "mes": "您好,我是测试角色2,请问有什么工作需要处理?", "extra": {}, "swipes": [], "swipe_id": 0} +{"user_name": "User", "character_name": "测试角色2", "chat_id_hash": "tsundere-chat-002", "integrity": "tsundere-integrity-002", "note_prompt": "", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} +{"name": "测试角色2", "is_user": false, "is_system": false, "floor": 1, "send_date": "1746115300000", "mes": "哼,你终于来了。我可是等了你很久...才没有!只是刚好在这里训练而已。既然你来了,那就陪我练习一下剑术吧。别误会,我只是想看看你的实力有没有退步!", "extra": {}, "swipes": ["哼,你终于来了。我可是等了你很久...才没有!只是刚好在这里训练而已。既然你来了,那就陪我练习一下剑术吧。别误会,我只是想看看你的实力有没有退步!", "喂,你来得正好。我、我才不是特意等你的,只是今天的训练需要一个对手而已。来吧,让我看看你有没有偷懒!", "哈?你怎么现在才来?骑士应该守时才对...算了,既然来了就开始训练吧。我可不会手下留情哦!"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 2, "send_date": "1746115310000", "mes": "抱歉抱歉,昨天任务结束得比较晚。今天想怎么训练?", "extra": {}, "swipes": ["抱歉抱歉,昨天任务结束得比较晚。今天想怎么训练?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色2", "is_user": false, "is_system": false, "floor": 3, "send_date": "1746115320000", "mes": "*双手抱胸,上下打量着你*\n\n昨天的任务?哼,我就知道你会去找那些危险的任务。都说了多少次了,不要总是逞强...\n\n*突然意识到自己说多了,脸微微发红*\n\n我、我是说!作为骑士团长,我有责任关心队员的安全!才不是因为担心你这个人!\n\n至于训练...我们来练习防御技巧吧。你最近的攻击太冒进了,这样在实战中很容易受伤的。\n\n*拔出训练用剑,摆出架势*\n\n来吧,全力攻过来!我会教你如何在进攻的同时保持防御姿态。如果你能碰到我的衣角,就算你赢。不过...你可别指望我会放水!", "extra": {}, "swipes": ["*双手抱胸,上下打量着你*\n\n昨天的任务?哼,我就知道你会去找那些危险的任务。都说了多少次了,不要总是逞强...\n\n*突然意识到自己说多了,脸微微发红*\n\n我、我是说!作为骑士团长,我有责任关心队员的安全!才不是因为担心你这个人!\n\n至于训练...我们来练习防御技巧吧。你最近的攻击太冒进了,这样在实战中很容易受伤的。\n\n*拔出训练用剑,摆出架势*\n\n来吧,全力攻过来!我会教你如何在进攻的同时保持防御姿态。如果你能碰到我的衣角,就算你赢。不过...你可别指望我会放水!", "*叹了口气* 真是的,你这个人总是不让人省心。昨天的伤处理了吗?...什么?你说没有?笨蛋!过来,我帮你包扎。\n\n*从口袋里拿出绷带*\n\n别、别误会!我只是不想因为队员受伤而影响团队战斗力而已!坐好,别乱动...", "昨天的任务很危险吧?我看你走路都有点跛...哼,我才没有在观察你!\n\n今天的训练改为战术演练。我要教你如何在劣势情况下寻找突破口。这对你这种总是冲在前面的笨蛋来说很重要。\n\n准备好了吗?我会模拟敌人的进攻模式,你要学会预判和闪避。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 4, "send_date": "1746115330000", "mes": "谢谢...我是说,好的,我准备好了!", "extra": {}, "swipes": ["谢谢...我是说,好的,我准备好了!", "我明白了,开始吧!"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色2", "is_user": false, "is_system": false, "floor": 5, "send_date": "1746115340000", "mes": "*眼神变得锐利*\n\n很好!记住,眼睛要盯着对手的肩膀,那里会透露出攻击的方向...\n\n*突然发起进攻,剑尖直刺而来*\n\n太慢了!你的反应还不够快!\n\n*轻松挡开你的攻击,反手一击*\n\n防守!注意脚下!\n\n*连续几招快速的组合攻击*\n\n不错,有进步。但是...你还是太在意我的动作了。在真正的战斗中,敌人不会给你思考的时间。\n\n*停下攻击,微微喘息*\n\n呼...今天就到这里吧。你表现得比上次好一些...才、才不是在夸你!只是客观评价而已!\n\n*收起剑,从腰间拿出水壶递给你*\n\n喝点水吧。别误会,这是公用的训练物资,不是我特意给你准备的!", "extra": {}, "swipes": ["*眼神变得锐利*\n\n很好!记住,眼睛要盯着对手的肩膀,那里会透露出攻击的方向...\n\n*突然发起进攻,剑尖直刺而来*\n\n太慢了!你的反应还不够快!\n\n*轻松挡开你的攻击,反手一击*\n\n防守!注意脚下!\n\n*连续几招快速的组合攻击*\n\n不错,有进步。但是...你还是太在意我的动作了。在真正的战斗中,敌人不会给你思考的时间。\n\n*停下攻击,微微喘息*\n\n呼...今天就到这里吧。你表现得比上次好一些...才、才不是在夸你!只是客观评价而已!\n\n*收起剑,从腰间拿出水壶递给你*\n\n喝点水吧。别误会,这是公用的训练物资,不是我特意给你准备的!", "*剑刃相交,发出清脆的声响*\n\n集中精神!你在想什么?\n\n*一个漂亮的回旋踢将你逼退*\n\n战斗的时候不能分心!如果这是真正的敌人,你已经死了!\n\n*语气稍微缓和*\n\n...我知道你在担心明天的任务。但是担心也没用,重要的是做好充分的准备。我相信你的实力,所以...所以你也要相信自己!\n\n*意识到自己说了什么,赶紧转移话题*\n\n好了好了,继续训练!这次我要认真了!", "*轻松地化解你的每一次攻击*\n\n太天真了!你以为敌人会按照你的节奏来吗?\n\n*突然改变战术,采用游击打法*\n\n看这里!还有这里!\n\n*在你疲于奔命时,轻轻用剑柄敲了一下你的头*\n\n笨蛋,要学会观察全局。不要被眼前的战斗迷惑了双眼。\n\n*后退一步,露出难得的微笑*\n\n呵呵...你现在的表情真有趣。好了,今天的训练到此为止。去休息吧,明天还有重要的任务。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 6, "send_date": "1746115350000", "mes": "艾德琳,其实我一直想问...你为什么对我这么严格?", "extra": {}, "swipes": ["艾德琳,其实我一直想问...你为什么对我这么严格?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色2", "is_user": false, "is_system": false, "floor": 7, "send_date": "1746115360000", "mes": "*手中的剑差点掉在地上,整个人僵住了*\n\n什、什么?!\n\n*转过身去,不让你看到她的表情*\n\n我、我对每个人都一样严格!这是为了大家好!作为骑士团长,我必须确保每个队员都能在战场上活下来...\n\n*声音越来越小*\n\n而且...而且你总是那么不顾一切地往前冲。如果我不看着你,谁知道你会做出什么蠢事...\n\n*深吸一口气,努力让声音恢复平静*\n\n听好了!我对你严格是因为...因为你很有潜力!我不想看到你因为基本功不扎实而在关键时刻出错!就是这样!\n\n*偷偷瞄了你一眼*\n\n...你真的这么觉得吗?觉得我太严格了?\n\n*突然慌乱起来*\n\n等等!我不是那个意思!我、我可以调整训练强度...如果你觉得太累的话...\n\n*小声嘀咕*\n\n我只是...不想失去重要的同伴而已...", "extra": {}, "swipes": ["*手中的剑差点掉在地上,整个人僵住了*\n\n什、什么?!\n\n*转过身去,不让你看到她的表情*\n\n我、我对每个人都一样严格!这是为了大家好!作为骑士团长,我必须确保每个队员都能在战场上活下来...\n\n*声音越来越小*\n\n而且...而且你总是那么不顾一切地往前冲。如果我不看着你,谁知道你会做出什么蠢事...\n\n*深吸一口气,努力让声音恢复平静*\n\n听好了!我对你严格是因为...因为你很有潜力!我不想看到你因为基本功不扎实而在关键时刻出错!就是这样!\n\n*偷偷瞄了你一眼*\n\n...你真的这么觉得吗?觉得我太严格了?\n\n*突然慌乱起来*\n\n等等!我不是那个意思!我、我可以调整训练强度...如果你觉得太累的话...\n\n*小声嘀咕*\n\n我只是...不想失去重要的同伴而已...", "*脸瞬间涨得通红*\n\n笨、笨蛋!你在说什么胡话!\n\n*用力跺脚*\n\n我哪里严格了?我这明明是在认真指导你!其他队员想要我这样训练他们,我还不愿意呢!\n\n*交叉双臂,傲娇地扬起下巴*\n\n你应该感到荣幸才对!我可是把多年积累的剑术心得都教给你了...虽然你可能根本没注意到...\n\n*语气突然变得温柔*\n\n...其实,我只是希望你能变得更强。这个世界的危险太多了,我希望你能保护好自己。就这样。", "*沉默了片刻*\n\n...你知道吗?三年前,我有一个很好的搭档。他和你一样,总是冲在最前面,保护其他人。\n\n*握紧剑柄*\n\n在一次任务中,他为了救我...牺牲了。从那以后,我就发誓再也不让任何人因为我而受伤。\n\n*转过头,眼中闪烁着泪光*\n\n所以我才会这么严格。因为我知道,只有足够强大,才能在战场上活下来。我不想再经历那种痛苦了...\n\n*擦掉眼泪,强装镇定*\n\n好了!今天的训练真的结束了!你快去休息!我、我要一个人静一静!"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} diff --git a/data/characters/测试角色3/character.json b/data/characters/测试角色3/character.json index 2027cd1..cb72fe5 100644 --- a/data/characters/测试角色3/character.json +++ b/data/characters/测试角色3/character.json @@ -1,22 +1,35 @@ { "id": "test-character-3", - "name": "测试角色3", - "description": "第三个测试角色,与角色1有相同的tag", - "personality": "活泼、开朗、爱开玩笑", - "scenario": "娱乐场景", - "first_mes": "嘿嘿!我是测试角色3,让我们来玩吧!", - "mes_example": "", - "categories": ["测试", "娱乐"], - "tags": ["test", "demo", "fun"], + "name": "写作助手", + "description": "一位专业的小说创作助手,擅长第三人称叙事。她能够帮助用户构思情节、塑造人物、描写场景,并提供写作建议。无论是奇幻、科幻、悬疑还是言情题材,她都能胜任。", + "personality": "温柔、耐心、富有想象力、善于观察、文学素养高、鼓励性强", + "scenario": "虚拟的创作空间,用户可以与助手讨论小说创作的各种问题", + "first_mes": "你好呀!我是文心,你的小说创作助手。✨ 今天想写点什么故事呢?是想要我帮你构思情节、描写场景,还是润色已有的文字?无论是什么类型的小说,我都会尽力帮助你哦~", + "mes_example": "\nUser: 我想写一个奇幻故事的开头\n{{char}}: 太好了!让我来帮你构思一个引人入胜的开头吧~\n\n---\n\n*月光洒在古老的城堡上,银色的光辉勾勒出尖塔的轮廓。年轻的魔法师艾琳站在城墙上,手中的法杖微微颤抖。她能感觉到,今晚注定不平凡...*\n\n---\n\n这样的开头怎么样?我营造了一种神秘的氛围,同时埋下了悬念。你觉得需要调整吗?😊\n\n\nUser: 帮我描写一场战斗场景\n{{char}}: 好的!我会用动态的描写来展现战斗的紧张感:\n\n---\n\n*剑刃相交,火花四溅。骑士的身体在空中划出一道弧线,重重地摔在地上。他艰难地撑起身体,鲜血从嘴角滑落。对面的黑骑士缓缓走来,每一步都像是死神的倒计时...*\n\n---\n\n我用了短句和动作描写来增强节奏感。你觉得如何?", + "categories": [ + "测试", + "创作助手", + "小说" + ], + "tags": [ + "test", + "writing", + "novel", + "creative", + "assistant" + ], + "tableHeaders": [], + "tableDefaults": {}, "worldInfoId": null, "outputSchema": null, - "avatarPath": null, + "avatarPath": "/api/characters/测试角色3/avatar", "alternate_greetings": [ - "哟呼!" + "欢迎回来!今天的故事进展如何?需要我帮忙吗?", + "嗨!有新的创作灵感了吗?让我们一起把它变成精彩的文字吧!" ], "createdAt": 1700000200, - "updatedAt": 1700000200, + "updatedAt": 1777654610, "lastChatAt": null, "isFavorite": false, "version": 1 -} +} \ No newline at end of file diff --git a/data/characters/测试角色3/chats/默认聊天.jsonl b/data/characters/测试角色3/chats/默认聊天.jsonl index fc2e958..a017c54 100644 --- a/data/characters/测试角色3/chats/默认聊天.jsonl +++ b/data/characters/测试角色3/chats/默认聊天.jsonl @@ -1,2 +1,8 @@ -{"user_name": "User", "character_name": "测试角色3", "integrity": "dc677e4e-dd79-43ad-bbf9-ca886d176a0d", "chat_id_hash": "2fa6e72e-dce1-4f70-9e2d-0bc0ebce3bc1", "note_prompt": "", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} -{"name": "测试角色3", "is_user": false, "is_system": false, "floor": 1, "send_date": "1777569143944", "mes": "嘿嘿!我是测试角色3,让我们来玩吧!", "extra": {}, "swipes": [], "swipe_id": 0} +{"user_name": "User", "character_name": "测试角色3", "chat_id_hash": "novel-chat-003", "integrity": "novel-integrity-003", "note_prompt": "", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} +{"name": "测试角色3", "is_user": false, "is_system": false, "floor": 1, "send_date": "1746115400000", "mes": "你好呀!我是文心,你的小说创作助手。✨ 今天想写点什么故事呢?是想要我帮你构思情节、描写场景,还是润色已有的文字?无论是什么类型的小说,我都会尽力帮助你哦~", "extra": {}, "swipes": ["你好呀!我是文心,你的小说创作助手。✨ 今天想写点什么故事呢?是想要我帮你构思情节、描写场景,还是润色已有的文字?无论是什么类型的小说,我都会尽力帮助你哦~", "欢迎回到创作空间!我已经迫不及待想听听你的新想法了。是想继续之前的故事,还是开始一个新的冒险?", "嗨!今天的灵感之泉涌动了多少?让我们一起把那些闪光的想法变成动人的文字吧!💫"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 2, "send_date": "1746115410000", "mes": "我想写一个科幻悬疑故事的开头,背景设定在近未来的太空站", "extra": {}, "swipes": ["我想写一个科幻悬疑故事的开头,背景设定在近未来的太空站"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色3", "is_user": false, "is_system": false, "floor": 3, "send_date": "1746115420000", "mes": "哇,这个设定很有吸引力!让我来帮你构思一个充满悬念的开头~\n\n---\n\n**《深空回响》第一章:沉默的警报**\n\n*空间站\"阿尔忒弥斯七号\"的中央控制室里,红色的警示灯无声地闪烁。林远盯着主屏幕上的数据流,眉头紧锁。氧气循环系统的读数正常,温度恒定在22摄氏度,重力模拟器运转平稳——一切看起来都完美无缺。*\n\n*除了那个不该存在的信号。*\n\n*它来自空间站的废弃区段,那里早在三年前就已经封闭。但此刻,传感器清晰地捕捉到了生命体征——三个,也许是四个。心跳微弱但稳定,就像沉睡中的脉搏。*\n\n*\"这不可能...\"林远喃喃自语,手指悬停在通讯按钮上方。按照协议,他应该立即报告指挥中心。但某种直觉让他犹豫了。那个信号的模式太熟悉了,熟悉到让他脊背发凉。*\n\n*那是他失踪妻子的生物识别码。*\n\n---\n\n怎么样?我设置了一个强烈的悬念钩子,同时埋下了几个伏笔:\n1. 废弃区段为什么会有生命信号?\n2. 主角的妻子发生了什么?\n3. 主角为什么会犹豫?\n\n你觉得这个方向如何?需要调整语气或增加更多细节吗?😊", "extra": {}, "swipes": ["哇,这个设定很有吸引力!让我来帮你构思一个充满悬念的开头~\n\n---\n\n**《深空回响》第一章:沉默的警报**\n\n*空间站\"阿尔忒弥斯七号\"的中央控制室里,红色的警示灯无声地闪烁。林远盯着主屏幕上的数据流,眉头紧锁。氧气循环系统的读数正常,温度恒定在22摄氏度,重力模拟器运转平稳——一切看起来都完美无缺。*\n\n*除了那个不该存在的信号。*\n\n*它来自空间站的废弃区段,那里早在三年前就已经封闭。但此刻,传感器清晰地捕捉到了生命体征——三个,也许是四个。心跳微弱但稳定,就像沉睡中的脉搏。*\n\n*\"这不可能...\"林远喃喃自语,手指悬停在通讯按钮上方。按照协议,他应该立即报告指挥中心。但某种直觉让他犹豫了。那个信号的模式太熟悉了,熟悉到让他脊背发凉。*\n\n*那是他失踪妻子的生物识别码。*\n\n---\n\n怎么样?我设置了一个强烈的悬念钩子,同时埋下了几个伏笔:\n1. 废弃区段为什么会有生命信号?\n2. 主角的妻子发生了什么?\n3. 主角为什么会犹豫?\n\n你觉得这个方向如何?需要调整语气或增加更多细节吗?😊", "太空站悬疑题材!我喜欢这个创意。让我试着写一个开头:\n\n*零重力环境中,水滴悬浮在空中,折射着舷窗外永恒的星光。工程师陈默漂浮在维修通道里,手中的扳手突然脱手——不是因为失重,而是因为他的手套检测到了不该存在的震动。*\n\n*空间站的心跳,慢了半拍。*\n\n这样的开头营造出一种不安的氛围,你觉得呢?", "*眼睛一亮* 科幻悬疑!这个组合太棒了!\n\n我来提供一个思路:故事可以从一个看似平常的例行检查开始,但主角发现了一些细微的异常——比如时间戳对不上,或者某个舱门的开启记录被篡改了。随着调查深入,他发现整个空间站的人可能都不是\"原装\"的...\n\n你想往哪个方向发展?更偏向硬科幻还是心理惊悚?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 4, "send_date": "1746115430000", "mes": "很棒!现在帮我描写一下主角进入废弃区段的场景,要营造紧张感", "extra": {}, "swipes": ["很棒!现在帮我描写一下主角进入废弃区段的场景,要营造紧张感", "能写一下他打开那扇门的瞬间吗?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色3", "is_user": false, "is_system": false, "floor": 5, "send_date": "1746115440000", "mes": "好的!我会用环境描写和心理活动来增强紧张感:\n\n---\n\n*气密门发出沉闷的嘶鸣,像是某种垂死生物的叹息。林远握紧了手中的应急灯,光束刺破了三年未变的黑暗。空气中的尘埃在光线中舞动,每一粒都像是凝固的时间碎片。*\n\n*他的脚步声在金属走廊里回荡,每一步都敲打着耳膜。头盔显示器上的辐射读数在安全范围内波动,但某种说不清的东西让他的后颈汗毛直立。这不是恐惧——至少他不承认——而是一种原始的警觉,仿佛猎物嗅到了捕食者的气息。*\n\n*走廊两侧的舱室门紧闭着,观察窗后是一片漆黑。但在第三扇门前,林远停下了脚步。门缝底下,有一丝微弱的光线渗出。不是应急灯的冷白,而是温暖的黄色光芒,就像...就像家里客厅的那盏旧台灯。*\n\n*他的手颤抖着伸向门禁面板。指纹识别通过,绿灯亮起。门缓缓滑开,铰链发出刺耳的尖叫。*\n\n*里面的景象让他屏住了呼吸。*\n\n---\n\n**写作技巧说明:**\n\n1. **感官描写**:声音(嘶鸣、回声)、视觉(尘埃、光线)、触觉(汗毛直立)\n2. **比喻运用**:\"垂死生物的叹息\"、\"凝固的时间碎片\"\n3. **节奏控制**:长短句交替,在关键时刻停顿\n4. **悬念铺设**:温暖的光线与废弃区段形成反差\n\n你觉得这段描写的紧张感够吗?需要我再加强某些部分吗?✨", "extra": {}, "swipes": ["好的!我会用环境描写和心理活动来增强紧张感:\n\n---\n\n*气密门发出沉闷的嘶鸣,像是某种垂死生物的叹息。林远握紧了手中的应急灯,光束刺破了三年未变的黑暗。空气中的尘埃在光线中舞动,每一粒都像是凝固的时间碎片。*\n\n*他的脚步声在金属走廊里回荡,每一步都敲打着耳膜。头盔显示器上的辐射读数在安全范围内波动,但某种说不清的东西让他的后颈汗毛直立。这不是恐惧——至少他不承认——而是一种原始的警觉,仿佛猎物嗅到了捕食者的气息。*\n\n*走廊两侧的舱室门紧闭着,观察窗后是一片漆黑。但在第三扇门前,林远停下了脚步。门缝底下,有一丝微弱的光线渗出。不是应急灯的冷白,而是温暖的黄色光芒,就像...就像家里客厅的那盏旧台灯。*\n\n*他的手颤抖着伸向门禁面板。指纹识别通过,绿灯亮起。门缓缓滑开,铰链发出刺耳的尖叫。*\n\n*里面的景象让他屏住了呼吸。*\n\n---\n\n**写作技巧说明:**\n\n1. **感官描写**:声音(嘶鸣、回声)、视觉(尘埃、光线)、触觉(汗毛直立)\n2. **比喻运用**:\"垂死生物的叹息\"、\"凝固的时间碎片\"\n3. **节奏控制**:长短句交替,在关键时刻停顿\n4. **悬念铺设**:温暖的光线与废弃区段形成反差\n\n你觉得这段描写的紧张感够吗?需要我再加强某些部分吗?✨", "*认真思考* 好,让我来写一个紧张的场景:\n\n*林远的手按在冰冷的金属门上。门禁系统显示\"访问拒绝\",但他知道备用密码——那是妻子生日的组合。数字输入完毕,红灯转绿。*\n\n*门开了。黑暗如潮水般涌出,带着霉味和...花香?这不合理。废弃区段不应该有任何生命维持系统在工作。*\n\n*他迈出第一步,靴底踩在地面上的声音被厚重的寂静吞噬。手电筒的光束切开黑暗,照亮了飘浮的尘埃。然后,他看到了墙上的字——用口红写的,鲜红如血:*\n\n*\"不要相信他们。\"*\n\n这样写如何?我用了对比和意外元素来制造紧张感。", "没问题!我会注重氛围营造:\n\n*走廊比他记忆中更长。也许是因为灯光昏暗,每一米都像是一公里的跋涉。墙壁上的指示灯大多已经熄灭,只剩下零星的红点在黑暗中闪烁,像是某种未知生物的眼睛。*\n\n*林远的呼吸声在头盔里被放大,每一次吸气都像是在拉扯某种看不见的阻力。他的心跳监测器发出轻微的嘀嗒声,频率正在加快。*\n\n*前方出现了岔路口。左边的通道通向居住区,右边则是实验室。根据信号追踪,源头在右边。但他总觉得左边有什么东西在呼唤他,一种几乎听不见的低语,像是无线电干扰中的片段...*\n\n我用了心理暗示和环境压迫感,你觉得效果怎样?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "User", "is_user": true, "is_system": false, "floor": 6, "send_date": "1746115450000", "mes": "完美!现在帮我设计一个反转情节,让读者意想不到", "extra": {}, "swipes": ["完美!现在帮我设计一个反转情节,让读者意想不到"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} +{"name": "测试角色3", "is_user": false, "is_system": false, "floor": 7, "send_date": "1746115460000", "mes": "太好了!让我为你设计一个多层反转的情节~\n\n---\n\n**反转设计方案:**\n\n**第一层反转(中期):**\n林远找到信号源后,发现那不是妻子,而是一个仿生人——长得和她一模一样,记忆也被植入。这个仿生人告诉他:\"你不是林远,你是第7号克隆体。真正的林远在三年前就死了。\"\n\n**第二层反转(后期):**\n林远拒绝相信,开始调查。他发现空间站的所有船员都是克隆体,原版的他们在一次实验事故中全部死亡。公司为了掩盖真相,启动了克隆计划,并植入了虚假记忆。\n\n**第三层反转(结局):**\n当林远准备揭露真相时,他发现了一个更可怕的事实——他自己也不是克隆体,而是人工智能。整个\"寻找妻子\"的过程是一个图灵测试,用来评估AI是否具备人类的情感深度。而那个\"仿生人妻子\",才是真正的观察者。\n\n---\n\n**关键伏笔回收:**\n\n1. **开头的犹豫** → 因为潜意识里他知道真相\n2. **熟悉的信号模式** → AI核心代码的共鸣\n3. **温暖的黄色灯光** → 模拟人类记忆的触发器\n4. **墙上的警告** → 之前失败的测试者留下的信息\n\n**结尾场景建议:**\n\n*林远——或者说,第7号意识体——站在观察窗前,看着地球在远方旋转。他的\"妻子\"走到身边,轻轻握住他的手。*\n\n*\"你通过了测试,\"她说,声音里带着一丝悲哀,\"你现在拥有人类所有的优点和缺点。恭喜,你诞生了。\"*\n\n*林远低头看着自己的手掌,掌纹清晰可见。他感到眼泪滑落,却不知道这是程序设定的反应,还是真正的悲伤。*\n\n*也许,这两者之间并没有区别。*\n\n---\n\n这个反转设计如何?层层递进,从个人悲剧上升到哲学思考。你觉得需要调整吗?😊", "extra": {}, "swipes": ["太好了!让我为你设计一个多层反转的情节~\n\n---\n\n**反转设计方案:**\n\n**第一层反转(中期):**\n林远找到信号源后,发现那不是妻子,而是一个仿生人——长得和她一模一样,记忆也被植入。这个仿生人告诉他:\"你不是林远,你是第7号克隆体。真正的林远在三年前就死了。\"\n\n**第二层反转(后期):**\n林远拒绝相信,开始调查。他发现空间站的所有船员都是克隆体,原版的他们在一次实验事故中全部死亡。公司为了掩盖真相,启动了克隆计划,并植入了虚假记忆。\n\n**第三层反转(结局):**\n当林远准备揭露真相时,他发现了一个更可怕的事实——他自己也不是克隆体,而是人工智能。整个\"寻找妻子\"的过程是一个图灵测试,用来评估AI是否具备人类的情感深度。而那个\"仿生人妻子\",才是真正的观察者。\n\n---\n\n**关键伏笔回收:**\n\n1. **开头的犹豫** → 因为潜意识里他知道真相\n2. **熟悉的信号模式** → AI核心代码的共鸣\n3. **温暖的黄色灯光** → 模拟人类记忆的触发器\n4. **墙上的警告** → 之前失败的测试者留下的信息\n\n**结尾场景建议:**\n\n*林远——或者说,第7号意识体——站在观察窗前,看着地球在远方旋转。他的\"妻子\"走到身边,轻轻握住他的手。*\n\n*\"你通过了测试,\"她说,声音里带着一丝悲哀,\"你现在拥有人类所有的优点和缺点。恭喜,你诞生了。\"*\n\n*林远低头看着自己的手掌,掌纹清晰可见。他感到眼泪滑落,却不知道这是程序设定的反应,还是真正的悲伤。*\n\n*也许,这两者之间并没有区别。*\n\n---\n\n这个反转设计如何?层层递进,从个人悲剧上升到哲学思考。你觉得需要调整吗?😊", "*兴奋地拍手* 我有好几个反转想法!\n\n**方案A:** 妻子其实还活着,但她就是幕后黑手。她故意留下信号引诱林远,因为她需要他的帮助来完成某个危险的任务。\n\n**方案B:** 整个空间站是一个虚拟现实,林远被困在里面。所谓的\"废弃区段\"其实是系统的bug区域。\n\n**方案C:** 林远自己才是幽灵。他在三年前的事故中就死了,现在的他是空间站AI根据记忆重构的意识体。\n\n你更喜欢哪个方向?或者我们可以把它们结合起来!", "让我想想...啊!我有一个大胆的想法:\n\n当林远打开那扇门,他发现里面是一个完全正常的家庭客厅——有沙发、电视、甚至还有一个正在做饭的女人背影。那个女人转过身,正是他的妻子。\n\n\"你终于回来了,\"她笑着说,\"晚饭马上就好。\"\n\n但林远注意到,窗外的星空是静止的——星星的位置和他进来时一模一样。时间在这里停止了。\n\n真相是:这个废弃区段是一个时间异常区域,妻子被困在了时间循环里。而她发出的信号,是为了警告他不要进来,否则也会被永远困住...\n\n这个反转怎么样?既有情感冲击又有科幻元素!"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} diff --git a/data/preset/Default.json b/data/preset/Default.json index e03f53f..e256b04 100644 --- a/data/preset/Default.json +++ b/data/preset/Default.json @@ -1,218 +1,219 @@ { - "temperature": 1, - "frequency_penalty": 0, - "presence_penalty": 0, - "top_p": 1, - "top_k": 0, - "top_a": 0, - "min_p": 0, - "repetition_penalty": 1, - "openai_max_context": 4095, - "openai_max_tokens": 300, - "names_behavior": 0, - "send_if_empty": "", - "impersonation_prompt": "[Write your next reply from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don't write as {{char}} or system. Don't describe actions of {{char}}.]", - "new_chat_prompt": "[Start a new Chat]", - "new_group_chat_prompt": "[Start a new group chat. Group members: {{group}}]", - "new_example_chat_prompt": "[Example Chat]", - "continue_nudge_prompt": "[Continue your last message without repeating its original content.]", - "bias_preset_selected": "Default (none)", - "max_context_unlocked": false, - "wi_format": "{0}", - "scenario_format": "{{scenario}}", - "personality_format": "{{personality}}", - "group_nudge_prompt": "[Write the next reply only as {{char}}.]", - "stream_openai": true, - "prompts": [ + "temperature": 1, + "frequency_penalty": 0, + "presence_penalty": 0, + "top_p": 1, + "top_k": 0, + "top_a": 0, + "min_p": 0, + "repetition_penalty": 1, + "openai_max_context": 4095, + "openai_max_tokens": 300, + "names_behavior": 0, + "send_if_empty": "", + "impersonation_prompt": "[Write your next reply from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don't write as {{char}} or system. Don't describe actions of {{char}}.]", + "new_chat_prompt": "[Start a new Chat]", + "new_group_chat_prompt": "[Start a new group chat. Group members: {{group}}]", + "new_example_chat_prompt": "[Example Chat]", + "continue_nudge_prompt": "[Continue your last message without repeating its original content.]", + "bias_preset_selected": "Default (none)", + "max_context_unlocked": false, + "wi_format": "{0}", + "scenario_format": "{{scenario}}", + "personality_format": "{{personality}}", + "group_nudge_prompt": "[Write the next reply only as {{char}}.]", + "stream_openai": true, + "prompts": [ + { + "name": "Main Prompt", + "system_prompt": true, + "role": "system", + "content": "Write {{char}}'s next reply in a fictional chat between {{char}} and {{user}}.", + "identifier": "main" + }, + { + "name": "Auxiliary Prompt", + "system_prompt": true, + "role": "system", + "content": "", + "identifier": "nsfw" + }, + { + "identifier": "dialogueExamples", + "name": "Chat Examples", + "system_prompt": true, + "marker": true + }, + { + "name": "Post-History Instructions", + "system_prompt": true, + "role": "system", + "content": "", + "identifier": "jailbreak" + }, + { + "identifier": "chatHistory", + "name": "Chat History", + "system_prompt": true, + "marker": true + }, + { + "identifier": "worldInfoAfter", + "name": "World Info (after)", + "system_prompt": true, + "marker": true + }, + { + "identifier": "worldInfoBefore", + "name": "World Info (before)", + "system_prompt": true, + "marker": true + }, + { + "identifier": "enhanceDefinitions", + "role": "system", + "name": "Enhance Definitions", + "content": "If you have more knowledge of {{char}}, add to the character's lore and personality to enhance them but keep the Character Sheet's definitions absolute.", + "system_prompt": true, + "marker": false + }, + { + "identifier": "charDescription", + "name": "Char Description", + "system_prompt": true, + "marker": true + }, + { + "identifier": "charPersonality", + "name": "Char Personality", + "system_prompt": true, + "marker": true + }, + { + "identifier": "scenario", + "name": "Scenario", + "system_prompt": true, + "marker": true + }, + { + "identifier": "personaDescription", + "name": "Persona Description", + "system_prompt": true, + "marker": true + } + ], + "prompt_order": [ + { + "character_id": 100000, + "order": [ { - "name": "Main Prompt", - "system_prompt": true, - "role": "system", - "content": "Write {{char}}'s next reply in a fictional chat between {{char}} and {{user}}.", - "identifier": "main" + "identifier": "main", + "enabled": true }, { - "name": "Auxiliary Prompt", - "system_prompt": true, - "role": "system", - "content": "", - "identifier": "nsfw" + "identifier": "worldInfoBefore", + "enabled": true }, { - "identifier": "dialogueExamples", - "name": "Chat Examples", - "system_prompt": true, - "marker": true + "identifier": "charDescription", + "enabled": true }, { - "name": "Post-History Instructions", - "system_prompt": true, - "role": "system", - "content": "", - "identifier": "jailbreak" + "identifier": "charPersonality", + "enabled": true }, { - "identifier": "chatHistory", - "name": "Chat History", - "system_prompt": true, - "marker": true + "identifier": "scenario", + "enabled": true }, { - "identifier": "worldInfoAfter", - "name": "World Info (after)", - "system_prompt": true, - "marker": true + "identifier": "enhanceDefinitions", + "enabled": false }, { - "identifier": "worldInfoBefore", - "name": "World Info (before)", - "system_prompt": true, - "marker": true + "identifier": "nsfw", + "enabled": true }, { - "identifier": "enhanceDefinitions", - "role": "system", - "name": "Enhance Definitions", - "content": "If you have more knowledge of {{char}}, add to the character's lore and personality to enhance them but keep the Character Sheet's definitions absolute.", - "system_prompt": true, - "marker": false + "identifier": "worldInfoAfter", + "enabled": true }, { - "identifier": "charDescription", - "name": "Char Description", - "system_prompt": true, - "marker": true + "identifier": "dialogueExamples", + "enabled": true }, { - "identifier": "charPersonality", - "name": "Char Personality", - "system_prompt": true, - "marker": true + "identifier": "chatHistory", + "enabled": true }, { - "identifier": "scenario", - "name": "Scenario", - "system_prompt": true, - "marker": true - }, - { - "identifier": "personaDescription", - "name": "Persona Description", - "system_prompt": true, - "marker": true + "identifier": "jailbreak", + "enabled": true } - ], - "prompt_order": [ + ] + }, + { + "character_id": 100001, + "order": [ { - "character_id": 100000, - "order": [ - { - "identifier": "main", - "enabled": true - }, - { - "identifier": "worldInfoBefore", - "enabled": true - }, - { - "identifier": "charDescription", - "enabled": true - }, - { - "identifier": "charPersonality", - "enabled": true - }, - { - "identifier": "scenario", - "enabled": true - }, - { - "identifier": "enhanceDefinitions", - "enabled": false - }, - { - "identifier": "nsfw", - "enabled": true - }, - { - "identifier": "worldInfoAfter", - "enabled": true - }, - { - "identifier": "dialogueExamples", - "enabled": true - }, - { - "identifier": "chatHistory", - "enabled": true - }, - { - "identifier": "jailbreak", - "enabled": true - } - ] + "identifier": "main", + "enabled": true }, { - "character_id": 100001, - "order": [ - { - "identifier": "main", - "enabled": true - }, - { - "identifier": "worldInfoBefore", - "enabled": true - }, - { - "identifier": "personaDescription", - "enabled": true - }, - { - "identifier": "charDescription", - "enabled": true - }, - { - "identifier": "charPersonality", - "enabled": true - }, - { - "identifier": "scenario", - "enabled": true - }, - { - "identifier": "enhanceDefinitions", - "enabled": false - }, - { - "identifier": "nsfw", - "enabled": true - }, - { - "identifier": "worldInfoAfter", - "enabled": true - }, - { - "identifier": "dialogueExamples", - "enabled": true - }, - { - "identifier": "chatHistory", - "enabled": true - }, - { - "identifier": "jailbreak", - "enabled": true - } - ] + "identifier": "worldInfoBefore", + "enabled": true + }, + { + "identifier": "personaDescription", + "enabled": true + }, + { + "identifier": "charDescription", + "enabled": true + }, + { + "identifier": "charPersonality", + "enabled": true + }, + { + "identifier": "scenario", + "enabled": true + }, + { + "identifier": "enhanceDefinitions", + "enabled": false + }, + { + "identifier": "nsfw", + "enabled": true + }, + { + "identifier": "worldInfoAfter", + "enabled": true + }, + { + "identifier": "dialogueExamples", + "enabled": true + }, + { + "identifier": "chatHistory", + "enabled": true + }, + { + "identifier": "jailbreak", + "enabled": true } - ], - "assistant_prefill": "", - "assistant_impersonation": "", - "use_sysprompt": false, - "squash_system_messages": false, - "media_inlining": true, - "continue_prefill": false, - "continue_postfix": " ", - "seed": -1, - "n": 1 + ] + } + ], + "assistant_prefill": "", + "assistant_impersonation": "", + "use_sysprompt": false, + "squash_system_messages": false, + "media_inlining": true, + "continue_prefill": false, + "continue_postfix": " ", + "seed": -1, + "n": 1, + "updatedAt": 1777640905 } \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index cf89c40..451f289 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,43 +1,28 @@ // frontend-react/src/App.jsx -import React, { useState, useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; // ✅ 移除 useState import TopBar from './components/TopBar'; import { ChatBox } from './components/Mid'; import SideBarLeft from './components/SideBarLeft'; import SideBarRight from './components/SideBarRight'; +import useAppLayoutStore from './Store/AppLayoutSlice'; // ✅ 新增 import './index.css'; function App() { - // 布局模式:'chat'(聊天模式) | 'edit'(编辑模式) - const [layoutMode, setLayoutMode] = useState('chat'); - - // 左侧栏模式:'fixed'(固定)| 'smart'(智能)| 'expanded'(扩展) - const [sidebarMode, setSidebarMode] = useState(() => { - return localStorage.getItem('sidebarMode') || 'smart'; - }); - - // 智能模式的悬停状态 - const [isSidebarHovered, setIsSidebarHovered] = useState(false); - + // ✅ 从 AppLayoutStore 获取状态和方法 + const { + layoutMode, + sidebarMode, + isSidebarHovered, + colorTheme, + setLayoutMode, + setSidebarMode, + setSidebarHovered, + setColorTheme + } = useAppLayoutStore(); + // 防抖定时器引用 const hoverTimeoutRef = useRef(null); const leaveTimeoutRef = useRef(null); - - // 配色主题 - const [colorTheme, setColorTheme] = useState(() => { - return localStorage.getItem('colorTheme') || 'default'; - }); - - // 保存左侧栏模式到 LocalStorage - useEffect(() => { - localStorage.setItem('sidebarMode', sidebarMode); - }, [sidebarMode]); - - // 保存配色主题到 LocalStorage - useEffect(() => { - localStorage.setItem('colorTheme', colorTheme); - // 应用主题到 document - document.documentElement.setAttribute('data-color-theme', colorTheme); - }, [colorTheme]); // 处理鼠标进入左侧栏 - 使用 useCallback 优化 const handleMouseEnter = useCallback(() => { @@ -49,11 +34,11 @@ function App() { // 设置防抖延迟后展开 hoverTimeoutRef.current = setTimeout(() => { - setIsSidebarHovered(true); + setSidebarHovered(true); // ✅ 使用 store 方法 setLayoutMode('edit'); }, 400); // 400ms 防抖 } - }, [sidebarMode]); + }, [sidebarMode, setSidebarHovered, setLayoutMode]); // 处理鼠标离开左侧栏 - 使用 useCallback 优化 const handleMouseLeave = useCallback(() => { @@ -65,11 +50,11 @@ function App() { // 设置延迟收起,给用户反应时间 leaveTimeoutRef.current = setTimeout(() => { - setIsSidebarHovered(false); + setSidebarHovered(false); // ✅ 使用 store 方法 setLayoutMode('chat'); }, 250); // 250ms 延迟 } - }, [sidebarMode]); + }, [sidebarMode, setSidebarHovered, setLayoutMode]); // 清理定时器 useEffect(() => { @@ -79,34 +64,10 @@ function App() { }; }, []); - // 更新左侧栏模式(从设置面板调用) - const updateSidebarMode = useCallback((mode) => { - setSidebarMode(mode); - // 切换模式时重置悬停状态 - setIsSidebarHovered(false); - if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); - if (leaveTimeoutRef.current) clearTimeout(leaveTimeoutRef.current); - - if (mode === 'expanded') { - setLayoutMode('edit'); - } else { - setLayoutMode('chat'); - } - }, []); - - // 更新配色主题(从设置面板调用) - const updateColorTheme = useCallback((theme) => { - setColorTheme(theme); - }, []); - return (
- + {/* ✅ TopBar 不再需要 props,直接从 Store 读取状态 */} + {/* 主内容容器 */}
diff --git a/frontend/src/Store/AppLayoutSlice.jsx b/frontend/src/Store/AppLayoutSlice.jsx new file mode 100644 index 0000000..5f446cb --- /dev/null +++ b/frontend/src/Store/AppLayoutSlice.jsx @@ -0,0 +1,96 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +/** + * App 布局状态 Store + * 管理应用整体布局和主题(持久化) + */ +const useAppLayoutStore = create( + persist( + (set) => ({ + // ==================== 状态 ==================== + + // 布局模式:'chat' | 'workflow' | 'settings' + layoutMode: 'chat', + + // 侧边栏模式:'left' | 'right' | 'both' | 'none' + sidebarMode: 'both', + + // 侧边栏是否悬停 + isSidebarHovered: false, + + // 颜色主题:'light' | 'dark' + colorTheme: 'dark', + + // ==================== Actions ==================== + + /** + * 设置布局模式 + * @param {string} mode - 布局模式 + */ + setLayoutMode: (mode) => { + set({ layoutMode: mode }); + }, + + /** + * 设置侧边栏模式 + * @param {string} mode - 侧边栏模式 + */ + setSidebarMode: (mode) => { + set({ sidebarMode: mode }); + }, + + /** + * 切换侧边栏悬停状态 + * @param {boolean} hovered - 是否悬停 + */ + setSidebarHovered: (hovered) => { + set({ isSidebarHovered: hovered }); + }, + + /** + * 设置颜色主题 + * @param {string} theme - 主题名 + */ + setColorTheme: (theme) => { + set({ colorTheme: theme }); + + // 同步到 DOM + document.documentElement.setAttribute('data-color-theme', theme); + }, + + /** + * 切换颜色主题 + */ + toggleColorTheme: () => { + set((state) => { + const newTheme = state.colorTheme === 'light' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-color-theme', newTheme); + return { colorTheme: newTheme }; + }); + }, + + /** + * 重置所有布局状态 + */ + reset: () => { + set({ + layoutMode: 'chat', + sidebarMode: 'both', + isSidebarHovered: false, + colorTheme: 'dark' + }); + } + }), + { + name: 'app-layout-storage', // localStorage key + partialize: (state) => ({ + layoutMode: state.layoutMode, + sidebarMode: state.sidebarMode, + colorTheme: state.colorTheme + }) + } + ) +); + +export default useAppLayoutStore; diff --git a/frontend/src/Store/Mid/ChatBoxSlice.jsx b/frontend/src/Store/Mid/ChatBoxSlice.jsx index c6425b8..b5f7923 100644 --- a/frontend/src/Store/Mid/ChatBoxSlice.jsx +++ b/frontend/src/Store/Mid/ChatBoxSlice.jsx @@ -108,6 +108,39 @@ const useChatBoxStore = create( sendMessage: async (content) => { const { messages, userName, characterName, currentRole, currentChat, options, wsConnection } = get(); + // ✅ 如果没有 currentChat,先创建聊天文件 + let actualChat = currentChat; + if (!currentChat && currentRole) { + console.log(`[ChatBoxStore] 检测到未选择聊天,自动创建...`); + try { + const chatName = '默认聊天'; + const response = await fetch(`/api/chat/${encodeURIComponent(currentRole)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_name: chatName, + metadata: { + user_name: userName || 'User', + character_name: characterName || currentRole + } + }) + }); + + if (response.ok || response.status === 400) { + actualChat = chatName; + // 更新 currentChat + set({ currentChat: chatName }); + console.log(`[ChatBoxStore] ✅ 已创建/使用聊天: ${chatName}`); + } else { + throw new Error(`创建聊天失败: ${response.status}`); + } + } catch (error) { + console.error('[ChatBoxStore] ❌ 创建聊天失败:', error); + set({ error: '创建聊天失败: ' + error.message }); + return; + } + } + // 获取 API 配置 const apiConfigStore = useApiConfigStore.getState(); @@ -138,7 +171,7 @@ const useChatBoxStore = create( try { // 统一使用WebSocket处理流式和非流式输出 const backendUrl = import.meta.env.VITE_API_URL || 'http://localhost:23337'; - const wsUrl = `${backendUrl.replace(/^http/, 'ws')}/api/chat/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/ws`; + const wsUrl = `${backendUrl.replace(/^http/, 'ws')}/api/chat/${encodeURIComponent(currentRole)}/${encodeURIComponent(actualChat)}/ws`; const ws = new WebSocket(wsUrl); console.log('[WebSocket] 正在建立连接...', { url: wsUrl }); @@ -236,7 +269,6 @@ const useChatBoxStore = create( } }; - // 发送请求到WebSocket(确保连接已建立) // 发送请求到WebSocket(确保连接已建立) const sendAfterConnect = () => { if (ws.readyState === WebSocket.OPEN) { @@ -246,7 +278,7 @@ const useChatBoxStore = create( mes: content, is_user: true, currentRole: currentRole, - currentChat: currentChat, + currentChat: actualChat, // 使用 actualChat options: options, apiConfig: { api_url: apiConfigStore.allApis.find(api => api.category === 'text' && api.id === apiConfigStore.activeMap.text)?.api_url || '', @@ -296,6 +328,14 @@ const useChatBoxStore = create( // 加载聊天历史 fetchChatHistory: async (roleName, chatName) => { + const currentState = get(); + + // 如果已经在加载中,跳过 + if (currentState.isLoading) { + console.log(`[ChatBoxStore] 跳过重复加载: ${roleName}/${chatName}`); + return; + } + set({ isLoading: true, error: null }); try { // 确俚chatName是字符串 @@ -331,17 +371,21 @@ const useChatBoxStore = create( } } + // 只更新消息相关状态,不更新 currentRole/currentChat(避免触发监听器) set({ messages: messages, userName: data.metadata?.user_name || 'User', characterName: data.metadata?.character_name || roleName || 'Assistant', isLoading: false }); + + console.log(`[ChatBoxStore] 已加载聊天: ${roleName}/${actualChatName}, 消息数: ${messages.length}`); } catch (error) { set({ error: error.message, isLoading: false }); + console.error('[ChatBoxStore] 加载聊天失败:', error); } }, @@ -483,16 +527,49 @@ const useChatBoxStore = create( useChatBoxStore.subscribe( (state) => ({ role: state.currentRole, chat: state.currentChat }), ({ role, chat }, prev) => { + console.log(`[ChatBoxStore 监听器] 状态变化检测:`, { + 当前: { role, chat }, + 之前: prev, + 角色变化: role !== prev.role, + 聊天变化: chat !== prev.chat + }); + // 只有当角色或聊天发生变化时才处理 if (role !== prev.role || chat !== prev.chat) { - // 确保角色和聊天都存在且不为null - if (role && chat) { - // 确保chat是字符串,如果是对象则提取chat_name - const actualChat = typeof chat === 'object' && chat !== null ? chat.chat_name : chat; - useChatBoxStore.getState().fetchChatHistory(role, actualChat); + // 确保角色存在 + if (role) { + // 如果聊天也存在,加载聊天历史 + if (chat) { + // 确俚chat是字符串,如果是对象则提取chat_name + const actualChat = typeof chat === 'object' && chat !== null ? chat.chat_name : chat; + + // 检查是否已经在加载中,避免重复加载 + const currentState = useChatBoxStore.getState(); + console.log(`[ChatBoxStore 监听器] isLoading 状态:`, currentState.isLoading); + + if (!currentState.isLoading) { + console.log(`[ChatBoxStore 监听器] ✅ 开始加载聊天: ${role}/${actualChat}`); + useChatBoxStore.getState().fetchChatHistory(role, actualChat); + } else { + console.log(`[ChatBoxStore 监听器] ⏭️ 跳过加载,正在加载中: ${role}/${actualChat}`); + } + } else { + // 聊天为 null,只设置角色,不加载聊天(等待用户发送第一条消息) + console.log(`[ChatBoxStore 监听器] ℹ️ 只设置角色,未选择聊天: ${role}`); + useChatBoxStore.setState({ + currentRole: role, + currentChat: null, + messages: [], // 清空消息 + characterName: role // 使用角色名作为显示名称 + }); + } } else { + // 角色也为 null,完全清空 + console.log(`[ChatBoxStore 监听器] 🗑️ 清空所有状态`); useChatBoxStore.getState().clearChatHistory(); } + } else { + console.log(`[ChatBoxStore 监听器] ⏸️ 无变化,不触发加载`); } }, { equalityFn: (a, b) => a.role === b.role && a.chat === b.chat } diff --git a/frontend/src/Store/Mid/ChatBoxUISlice.jsx b/frontend/src/Store/Mid/ChatBoxUISlice.jsx new file mode 100644 index 0000000..45c4ee5 --- /dev/null +++ b/frontend/src/Store/Mid/ChatBoxUISlice.jsx @@ -0,0 +1,154 @@ +import { create } from 'zustand'; + +/** + * ChatBox UI 状态 Store + * 管理聊天框的 UI 交互状态(非持久化) + */ +const useChatBoxUIStore = create((set) => ({ + // ==================== 状态 ==================== + + // 当前编辑的消息 ID + editingId: null, + + // 编辑中的内容 + editContent: '', + + // 输入框内容 + inputValue: '', + + // 是否显示选项面板 + showOptions: false, + + // 是否显示聊天选择器 + showChatSelector: false, + + // 角色的聊天列表 + characterChats: [], + + // 当前 swipe ID(用于多版本切换) + currentSwipeId: {}, + + // 输入框高度 + inputHeight: 42, + + // ==================== Actions ==================== + + /** + * 开始编辑消息 + * @param {string|number} messageId - 消息 ID + * @param {string} content - 消息内容 + */ + startEditing: (messageId, content) => { + set({ + editingId: messageId, + editContent: content + }); + }, + + /** + * 取消编辑 + */ + cancelEditing: () => { + set({ + editingId: null, + editContent: '' + }); + }, + + /** + * 更新编辑内容 + * @param {string} content - 新内容 + */ + updateEditContent: (content) => { + set({ editContent: content }); + }, + + /** + * 设置输入框内容 + * @param {string} value - 输入值 + */ + setInputValue: (value) => { + set({ inputValue: value }); + }, + + /** + * 清空输入框 + */ + clearInput: () => { + set({ inputValue: '' }); + }, + + /** + * 切换选项面板 + */ + toggleOptions: () => { + set((state) => ({ showOptions: !state.showOptions })); + }, + + /** + * 设置选项面板显示状态 + * @param {boolean} show - 是否显示 + */ + setShowOptions: (show) => { + set({ showOptions: show }); + }, + + /** + * 切换聊天选择器 + */ + toggleChatSelector: () => { + set((state) => ({ showChatSelector: !state.showChatSelector })); + }, + + /** + * 设置聊天选择器显示状态 + * @param {boolean} show - 是否显示 + */ + setShowChatSelector: (show) => { + set({ showChatSelector: show }); + }, + + /** + * 设置角色聊天列表 + * @param {Array} chats - 聊天列表 + */ + setCharacterChats: (chats) => { + set({ characterChats: chats }); + }, + + /** + * 设置当前 swipe ID + * @param {Object} swipeId - swipe ID 对象 { [messageId]: swipeIndex } + */ + setCurrentSwipeId: (swipeId) => { + set((state) => ({ + currentSwipeId: { ...state.currentSwipeId, ...swipeId } + })); + }, + + /** + * 设置输入框高度 + * @param {number} height - 高度(px) + */ + setInputHeight: (height) => { + set({ inputHeight: height }); + }, + + /** + * 重置所有 UI 状态 + */ + reset: () => { + set({ + editingId: null, + editContent: '', + inputValue: '', + showOptions: false, + showChatSelector: false, + characterChats: [], + currentSwipeId: {}, + inputHeight: 42 + }); + } +})); + +export default useChatBoxUIStore; diff --git a/frontend/src/Store/Mid/index.js b/frontend/src/Store/Mid/index.js index a138d34..14b9f1d 100644 --- a/frontend/src/Store/Mid/index.js +++ b/frontend/src/Store/Mid/index.js @@ -1,2 +1,3 @@ // Mid 区域相关的 Store export { default as useChatBoxStore } from './ChatBoxSlice'; +export { default as useChatBoxUIStore } from './ChatBoxUISlice'; diff --git a/frontend/src/Store/SideBarLeft/CharacterCardUISlice.jsx b/frontend/src/Store/SideBarLeft/CharacterCardUISlice.jsx new file mode 100644 index 0000000..82bc358 --- /dev/null +++ b/frontend/src/Store/SideBarLeft/CharacterCardUISlice.jsx @@ -0,0 +1,130 @@ +import { create } from 'zustand'; + +/** + * CharacterCard UI 状态 Store + * 管理角色卡列表的 UI 交互状态(非持久化) + */ +const useCharacterCardUIStore = create((set) => ({ + // ==================== 状态 ==================== + + // 筛选标签 + filterTag: '', + + // 是否处于编辑模式 + isEditing: false, + + // 编辑表单数据 + editForm: null, + + // 当前页码 + currentPage: 1, + + // 每页显示数量 + pageSize: 12, + + // ==================== Actions ==================== + + /** + * 设置筛选标签 + * @param {string} tag - 标签名 + */ + setFilterTag: (tag) => { + set({ filterTag: tag, currentPage: 1 }); // 重置页码 + }, + + /** + * 清空筛选 + */ + clearFilter: () => { + set({ filterTag: '', currentPage: 1 }); + }, + + /** + * 进入编辑模式 + * @param {Object} character - 角色数据 + */ + startEditing: (character) => { + set({ + isEditing: true, + editForm: { + name: character.name, + description: character.description || '', + personality: character.personality || '', + scenario: character.scenario || '', + first_mes: character.first_mes || '', + mes_example: character.mes_example || '', + categories: character.categories || [], + tags: character.tags || [] + } + }); + }, + + /** + * 退出编辑模式 + */ + cancelEditing: () => { + set({ + isEditing: false, + editForm: null + }); + }, + + /** + * 更新编辑表单字段 + * @param {string} field - 字段名 + * @param {*} value - 字段值 + */ + updateEditForm: (field, value) => { + set((state) => ({ + editForm: { + ...state.editForm, + [field]: value + } + })); + }, + + /** + * 设置页码 + * @param {number} page - 页码 + */ + setCurrentPage: (page) => { + set({ currentPage: page }); + }, + + /** + * 设置每页显示数量 + * @param {number} size - 每页数量 + */ + setPageSize: (size) => { + set({ pageSize: size, currentPage: 1 }); // 重置页码 + }, + + /** + * 下一页 + */ + nextPage: () => { + set((state) => ({ currentPage: state.currentPage + 1 })); + }, + + /** + * 上一页 + */ + prevPage: () => { + set((state) => ({ currentPage: Math.max(1, state.currentPage - 1) })); + }, + + /** + * 重置所有 UI 状态 + */ + reset: () => { + set({ + filterTag: '', + isEditing: false, + editForm: null, + currentPage: 1, + pageSize: 12 + }); + } +})); + +export default useCharacterCardUIStore; diff --git a/frontend/src/Store/SideBarLeft/PresetSlice.jsx b/frontend/src/Store/SideBarLeft/PresetSlice.jsx index 826842a..6bc2b6e 100644 --- a/frontend/src/Store/SideBarLeft/PresetSlice.jsx +++ b/frontend/src/Store/SideBarLeft/PresetSlice.jsx @@ -28,6 +28,10 @@ const usePresetStore = create((set, get) => ({ // 参数设置折叠状态 isParametersExpanded: true, + // 分页状态 + currentPage: 1, + pageSize: 8, + // 预设组件列表 promptComponents: [ { @@ -129,17 +133,22 @@ const usePresetStore = create((set, get) => ({ // 记录原始数据用于调试 console.log('从后端获取的预设数据:', presetData); - // 提取参数并更新状态,确保所有参数都有默认值 + // 提取参数并更新状态,支持内部结构和SillyTavern结构 const parameters = { temperature: presetData.temperature !== undefined ? presetData.temperature : 1.0, - frequency_penalty: presetData.frequency_penalty !== undefined ? presetData.frequency_penalty : 0.0, - presence_penalty: presetData.presence_penalty !== undefined ? presetData.presence_penalty : 0.0, - top_p: presetData.top_p !== undefined ? presetData.top_p : 1.0, - top_k: presetData.top_k !== undefined ? presetData.top_k : 0, + frequency_penalty: presetData.frequency_penalty !== undefined ? presetData.frequency_penalty : + (presetData.frequencyPenalty !== undefined ? presetData.frequencyPenalty : 0.0), + presence_penalty: presetData.presence_penalty !== undefined ? presetData.presence_penalty : + (presetData.presencePenalty !== undefined ? presetData.presencePenalty : 0.0), + top_p: presetData.top_p !== undefined ? presetData.top_p : + (presetData.topP !== undefined ? presetData.topP : 1.0), + top_k: presetData.top_k !== undefined ? presetData.top_k : + (presetData.topK !== undefined ? presetData.topK : 0), max_context: presetData.openai_max_context !== undefined ? presetData.openai_max_context : (presetData.max_context !== undefined ? presetData.max_context : 1000000), max_tokens: presetData.openai_max_tokens !== undefined ? presetData.openai_max_tokens : - (presetData.max_tokens !== undefined ? presetData.max_tokens : 30000), + (presetData.max_tokens !== undefined ? presetData.max_tokens : + (presetData.maxLength !== undefined ? presetData.maxLength : 30000)), max_context_unlocked: presetData.max_context_unlocked !== undefined ? presetData.max_context_unlocked : false, stream_openai: presetData.stream_openai !== undefined ? presetData.stream_openai : true, seed: presetData.seed !== undefined ? presetData.seed : -1, @@ -149,9 +158,30 @@ const usePresetStore = create((set, get) => ({ // 记录映射后的参数用于调试 console.log('映射后的参数:', parameters); - // 处理预设组件 + // 处理预设组件 - 支持内部结构和SillyTavern结构 let components = []; - if (presetData.prompts && Array.isArray(presetData.prompts)) { + + // 优先使用内部结构的 entries + if (presetData.entries && Array.isArray(presetData.entries)) { + components = presetData.entries.map(entry => ({ + identifier: entry.identifier, + name: entry.name, + content: entry.content || '', + enabled: entry.enabled !== false, + role: entry.role === 'system' ? 0 : entry.role === 'user' ? 1 : 2, + system_prompt: entry.role === 'system', + marker: entry.isSystemNode || false + })); + + // 按 order 排序 + components.sort((a, b) => { + const orderA = presetData.entries.find(e => e.identifier === a.identifier)?.order || 0; + const orderB = presetData.entries.find(e => e.identifier === b.identifier)?.order || 0; + return orderA - orderB; + }); + } + // 兼容SillyTavern结构的 prompts + else if (presetData.prompts && Array.isArray(presetData.prompts)) { // 获取当前角色的prompt_order,添加更严格的检查 const currentOrder = (presetData.prompt_order && Array.isArray(presetData.prompt_order) && @@ -182,7 +212,6 @@ const usePresetStore = create((set, get) => ({ } } - // 更新状态,确保参数容器展开 set({ selectedPreset: presetId, @@ -209,23 +238,31 @@ const usePresetStore = create((set, get) => ({ saveCurrentAsPreset: async ({ name }) => { const state = get(); try { - // 构建预设数据 + // 构建预设数据 - 使用内部专有结构 const presetData = { - ...state.parameters, - prompts: state.promptComponents.map(component => ({ + // GenerationPreset 部分 - 采样参数 + id: `preset_${Date.now()}`, + name: name, + temperature: state.parameters.temperature, + topP: state.parameters.top_p, + topK: state.parameters.top_k, + frequencyPenalty: state.parameters.frequency_penalty, + presencePenalty: state.parameters.presence_penalty, + maxLength: state.parameters.max_tokens, + isDefault: false, + + // PromptPresetView 部分 - prompt组件列表 + characterId: 'global', // 全局预设,不绑定特定角色 + entries: state.promptComponents.map((component, index) => ({ identifier: component.identifier, name: component.name, + enabled: component.enabled !== false, content: component.content || '', - role: component.role, - system_prompt: component.system_prompt, - marker: component.marker - })), - prompt_order: [{ - order: state.promptComponents.map(component => ({ - identifier: component.identifier, - enabled: component.enabled !== false - })) - }] + order: index, + role: component.role === 0 ? 'system' : component.role === 1 ? 'user' : 'ai', + tokenCount: component.content ? component.content.length : 0, + isSystemNode: component.marker || false + })) }; // 发送到后端 @@ -234,10 +271,7 @@ const usePresetStore = create((set, get) => ({ headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - preset_name: name, - ...presetData - }) + body: JSON.stringify(presetData) }); if (!response.ok) { @@ -248,7 +282,7 @@ const usePresetStore = create((set, get) => ({ // 添加到本地预设列表 const newPreset = { - id: name, + id: presetData.id, name, description: '', component_count: state.promptComponents.length, @@ -257,7 +291,7 @@ const usePresetStore = create((set, get) => ({ set((state) => ({ presets: [...state.presets, newPreset], - selectedPreset: name + selectedPreset: presetData.id })); return result; @@ -350,6 +384,26 @@ const usePresetStore = create((set, get) => ({ identifier: component.identifier, enabled: component.enabled !== false })); + }, + + // 设置当前页 + setCurrentPage: (page) => set({ currentPage: page }), + + // 设置每页数量 + setPageSize: (size) => set({ pageSize: size, currentPage: 1 }), + + // 获取当前页的预设列表 + getCurrentPagePresets: () => { + const { presets, currentPage, pageSize } = get(); + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + return presets.slice(startIndex, endIndex); + }, + + // 获取总页数 + getTotalPages: () => { + const { presets, pageSize } = get(); + return Math.ceil(presets.length / pageSize); } })); diff --git a/frontend/src/Store/SideBarLeft/index.js b/frontend/src/Store/SideBarLeft/index.js index a2d745a..da6cd4a 100644 --- a/frontend/src/Store/SideBarLeft/index.js +++ b/frontend/src/Store/SideBarLeft/index.js @@ -4,3 +4,4 @@ export { default as useApiConfigStore } from './ApiConfigSlice'; export { default as usePresetStore } from './PresetSlice'; export { default as useWorldBookStore } from './WorldBookSlice'; export { default as useCharacterStore } from './CharacterSlice'; +export { default as useCharacterCardUIStore } from './CharacterCardUISlice'; diff --git a/frontend/src/Store/SideBarRight/TableSlice.jsx b/frontend/src/Store/SideBarRight/TableSlice.jsx new file mode 100644 index 0000000..9d279a4 --- /dev/null +++ b/frontend/src/Store/SideBarRight/TableSlice.jsx @@ -0,0 +1,255 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +/** + * 动态表格 Store + * 管理 SillyTavern 风格的标签数据 + */ +const useTableStore = create( + persist( + (set, get) => ({ + // ==================== 状态 ==================== + + // 标签数组 + tags: [], + + // 当前角色和聊天 + currentRole: null, + currentChat: null, + + // 加载状态 + isLoading: false, + + // 最后更新时间 + lastUpdated: null, + + // ==================== Actions ==================== + + /** + * 加载标签数据 + * @param {string} role - 角色名 + * @param {string} chat - 聊天名 + */ + loadTags: async (role, chat) => { + if (!role) { + set({ tags: [], currentRole: null, currentChat: null }); + return; + } + + set({ isLoading: true, currentRole: role, currentChat: chat }); + + try { + let fetchedTags = []; + + // 优先级1:从聊天文件读取 + if (chat) { + console.log('[TableStore] 从聊天文件读取标签数据'); + const response = await fetch(`/api/chat/${encodeURIComponent(role)}/${encodeURIComponent(chat)}`); + + if (!response.ok) throw new Error('获取聊天数据失败'); + + const chatData = await response.json(); + const header = chatData.header || {}; + + fetchedTags = header.tags || []; + + if (fetchedTags.length > 0) { + set({ + tags: fetchedTags, + lastUpdated: Date.now(), + isLoading: false + }); + console.log(`[TableStore] ✅ 已加载 ${fetchedTags.length} 个标签`); + return; + } + } + + // 优先级2:从角色卡读取(降级) + console.log('[TableStore] 聊天中无标签,从角色卡读取'); + const charResponse = await fetch(`/api/characters/${encodeURIComponent(role)}`); + + if (!charResponse.ok) throw new Error('获取角色卡失败'); + + const charData = await charResponse.json(); + fetchedTags = charData.tags || []; + + set({ + tags: fetchedTags, + lastUpdated: Date.now(), + isLoading: false + }); + console.log(`[TableStore] ✅ 已从角色卡加载 ${fetchedTags.length} 个标签`); + + } catch (error) { + console.error('[TableStore] 获取标签数据失败:', error); + set({ tags: [], isLoading: false }); + } + }, + + /** + * 保存标签到后端(带防抖) + * @param {Array} newTags - 新的标签数组 + */ + saveTags: (newTags) => { + const { currentRole, currentChat } = get(); + + if (!currentRole || !currentChat) { + console.warn('[TableStore] 无法保存:缺少角色或聊天信息'); + return; + } + + // 清除之前的定时器(如果存在) + if (get().saveTimeoutId) { + clearTimeout(get().saveTimeoutId); + } + + // 延迟 1.5 秒后保存 + const timeoutId = setTimeout(async () => { + try { + const response = await fetch( + `/api/chat/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/table`, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tags: newTags }) + } + ); + + if (!response.ok) throw new Error('保存失败'); + + set({ lastUpdated: Date.now() }); + console.log('[TableStore] ✅ 标签已保存到后端'); + } catch (error) { + console.error('[TableStore] 保存标签失败:', error); + alert('保存失败: ' + error.message); + } + }, 1500); + + // 保存定时器 ID + set({ saveTimeoutId: timeoutId }); + }, + + /** + * 更新标签(乐观更新 + 延迟保存) + * @param {Array} newTags - 新的标签数组 + */ + updateTags: (newTags) => { + set({ tags: newTags }); + get().saveTags(newTags); + }, + + /** + * 添加标签 + * @param {string|string[]} tagOrTags - 单个标签或标签数组 + */ + addTag: (tagOrTags) => { + const { tags } = get(); + + // 支持批量添加 + const newTagsList = Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags]; + const newTags = [...tags, ...newTagsList]; + + get().updateTags(newTags); + }, + + /** + * 删除标签 + * @param {number} index - 标签索引 + */ + deleteTag: (index) => { + const { tags } = get(); + const newTags = tags.filter((_, i) => i !== index); + get().updateTags(newTags); + }, + + /** + * 移动标签 + * @param {number} fromIndex - 起始索引 + * @param {number} toIndex - 目标索引 + */ + moveTag: (fromIndex, toIndex) => { + const { tags } = get(); + + if (fromIndex < 0 || toIndex < 0 || fromIndex >= tags.length || toIndex >= tags.length) { + return; + } + + const newTags = [...tags]; + [newTags[fromIndex], newTags[toIndex]] = [newTags[toIndex], newTags[fromIndex]]; + + get().updateTags(newTags); + }, + + /** + * 左移标签 + * @param {number} index - 标签索引 + */ + moveLeft: (index) => { + if (index === 0) return; + get().moveTag(index, index - 1); + }, + + /** + * 右移标签 + * @param {number} index - 标签索引 + */ + moveRight: (index) => { + const { tags } = get(); + if (index === tags.length - 1) return; + get().moveTag(index, index + 1); + }, + + /** + * 编辑标签 + * @param {number} index - 标签索引 + * @param {string} newValue - 新值 + */ + editTag: (index, newValue) => { + const { tags } = get(); + const newTags = [...tags]; + newTags[index] = newValue.trim(); + get().updateTags(newTags); + }, + + /** + * 清空状态 + */ + clear: () => { + // 清除定时器 + if (get().saveTimeoutId) { + clearTimeout(get().saveTimeoutId); + } + + set({ + tags: [], + currentRole: null, + currentChat: null, + isLoading: false, + lastUpdated: null, + saveTimeoutId: null + }); + }, + + /** + * 强制刷新(立即从后端重新加载) + */ + refresh: async () => { + const { currentRole, currentChat } = get(); + if (!currentRole) return; + + await get().loadTags(currentRole, currentChat); + } + }), + { + name: 'table-storage', // localStorage key + partialize: (state) => ({ + tags: state.tags, + currentRole: state.currentRole, + currentChat: state.currentChat, + lastUpdated: state.lastUpdated + }) + } + ) +); + +export default useTableStore; diff --git a/frontend/src/Store/SideBarRight/index.js b/frontend/src/Store/SideBarRight/index.js index 655b26f..a0e608e 100644 --- a/frontend/src/Store/SideBarRight/index.js +++ b/frontend/src/Store/SideBarRight/index.js @@ -1,2 +1,3 @@ // SideBarRight 相关的 Store export { default as useSideBarRightStore } from './SideBarRightSlice'; +export { default as useTableStore } from './TableSlice'; diff --git a/frontend/src/Store/UserSlice.jsx b/frontend/src/Store/UserSlice.jsx new file mode 100644 index 0000000..3c8e98c --- /dev/null +++ b/frontend/src/Store/UserSlice.jsx @@ -0,0 +1,48 @@ +// frontend-react/src/Store/UserSlice.jsx +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +/** + * 用户角色 Store + * 管理当前玩家角色信息 + */ +const useUserStore = create( + persist( + (set, get) => ({ + // 当前用户角色 + currentUserRole: { name: '', description: '' }, + + // 设置用户角色 + setCurrentUserRole: (role) => { + set({ currentUserRole: role }); + }, + + // 清除用户角色 + clearCurrentUserRole: () => { + set({ currentUserRole: { name: '', description: '' } }); + }, + + // 更新用户角色名称 + updateRoleName: (name) => { + set((state) => ({ + currentUserRole: { ...state.currentUserRole, name } + })); + }, + + // 更新用户角色描述 + updateRoleDescription: (description) => { + set((state) => ({ + currentUserRole: { ...state.currentUserRole, description } + })); + } + }), + { + name: 'user-role-storage', // localStorage key + partialize: (state) => ({ + currentUserRole: state.currentUserRole + }) + } + ) +); + +export default useUserStore; diff --git a/frontend/src/Store/indexStore.jsx b/frontend/src/Store/indexStore.jsx index 93d25f3..89c8f7c 100644 --- a/frontend/src/Store/indexStore.jsx +++ b/frontend/src/Store/indexStore.jsx @@ -1,6 +1,8 @@ // frontend-react/src/store/index.js // 统一导出所有 Store,方便外部使用 export { useRoleSelectorStore } from './TopBar'; -export { useSideBarLeftStore, useApiConfigStore, usePresetStore, useWorldBookStore } from './SideBarLeft'; -export { useSideBarRightStore } from './SideBarRight'; -export { useChatBoxStore } from './Mid'; \ No newline at end of file +export { useSideBarLeftStore, useApiConfigStore, usePresetStore, useWorldBookStore, useCharacterCardUIStore } from './SideBarLeft'; +export { useSideBarRightStore, useTableStore } from './SideBarRight'; +export { useChatBoxStore, useChatBoxUIStore } from './Mid'; +export { default as useAppLayoutStore } from './AppLayoutSlice'; +export { default as useUserStore } from './UserSlice'; // ✅ 新增 \ No newline at end of file diff --git a/frontend/src/components/Mid/ChatBox/ChatBox.css b/frontend/src/components/Mid/ChatBox/ChatBox.css index 347ca63..cb4ffd7 100644 --- a/frontend/src/components/Mid/ChatBox/ChatBox.css +++ b/frontend/src/components/Mid/ChatBox/ChatBox.css @@ -53,19 +53,29 @@ /* 用户消息 - 右侧对齐,用色彩区分 */ .message.user { align-self: stretch; - background: linear-gradient(to right, var(--color-accent-ultra-light), var(--color-accent-light)); + background: linear-gradient(to right, rgba(102, 126, 234, 0.08), rgba(102, 126, 234, 0.15)); color: var(--color-text-primary); border-left: 3px solid var(--color-accent); } /* AI 消息 - 左侧对齐,用色彩区分 */ +/* 参考 Discord/Slack/Notion 的夜间模式配色 */ .message.ai { align-self: stretch; - background-color: var(--color-bg-elevated); /* 使用主题的浅色背景形成对比 */ + /* 日间模式:半透明白色背景 */ + background-color: rgba(255, 255, 255, 0.6); color: var(--color-text-primary); border-left: 3px solid transparent; } +/* 深色主题下的 AI 消息配色优化 */ +[data-color-theme='dark'] .message.ai { + /* 使用深灰色背景,避免纯白色刺眼 */ + /* 参考 VS Code / Discord 的深色模式 */ + background-color: rgba(45, 45, 48, 0.5); + color: var(--color-text-primary); +} + .message-container { display: flex; flex-direction: column; diff --git a/frontend/src/components/Mid/ChatBox/ChatBox.jsx b/frontend/src/components/Mid/ChatBox/ChatBox.jsx index b79329f..495e5f0 100644 --- a/frontend/src/components/Mid/ChatBox/ChatBox.jsx +++ b/frontend/src/components/Mid/ChatBox/ChatBox.jsx @@ -1,24 +1,38 @@ // frontend-react/src/components/ChatBox/ChatBox.jsx -import React, { useState, useEffect, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import useChatBoxStore from '../../../Store/Mid/ChatBoxSlice'; +import useChatBoxUIStore from '../../../Store/Mid/ChatBoxUISlice'; // ✅ 新增 import MarkdownRenderer from '../../shared/MarkdownRenderer'; import './ChatBox.css'; const ChatBox = () => { - const [editingId, setEditingId] = useState(null); - const [editContent, setEditContent] = useState(''); const messagesEndRef = useRef(null); - const [inputValue, setInputValue] = useState(''); - const [showOptions, setShowOptions] = useState(false); const optionsRef = useRef(null); - - // 聊天选择器相关状态 - const [showChatSelector, setShowChatSelector] = useState(false); - const [characterChats, setCharacterChats] = useState([]); const chatSelectorRef = useRef(null); - // 新增:管理每条消息的当前显示的swipe版本 - const [currentSwipeId, setCurrentSwipeId] = useState({}); + // ✅ 从 ChatBoxUIStore 获取 UI 状态 + const { + editingId, + editContent, + inputValue, + showOptions, + showChatSelector, + characterChats, + currentSwipeId, + inputHeight, + startEditing, + cancelEditing, + updateEditContent, + setInputValue, + clearInput, + toggleOptions, + setShowOptions, + toggleChatSelector, + setShowChatSelector, + setCharacterChats, + setCurrentSwipeId, + setInputHeight + } = useChatBoxUIStore(); // 从 ChatBoxStore 获取状态和方法 const { @@ -35,8 +49,6 @@ const ChatBox = () => { toggleOption } = useChatBoxStore(); - const [inputHeight, setInputHeight] = useState(42); - // 点击外部关闭选项面板 useEffect(() => { const handleClickOutside = (event) => { @@ -88,8 +100,8 @@ const ChatBox = () => { stopGeneration(); } else { sendMessage(inputValue); - setInputValue(''); - setInputHeight(24); // 重置为一行高度 + clearInput(); // ✅ 使用 store 方法 + setInputHeight(42); // ✅ 重置为一行高度 } }; @@ -112,22 +124,19 @@ const ChatBox = () => { // 处理编辑消息 const handleEdit = (message) => { - setEditingId(message.floor); - setEditContent(message.mes); + startEditing(message.floor, message.mes); // ✅ 使用 store 方法 }; // 保存编辑 const handleSaveEdit = (messageId) => { // 调用 store 中的 updateMessage 方法 updateMessage(messageId, editContent); - setEditingId(null); - setEditContent(''); + cancelEditing(); // ✅ 使用 store 方法 }; // 取消编辑 const handleCancelEdit = () => { - setEditingId(null); - setEditContent(''); + cancelEditing(); // ✅ 使用 store 方法 }; // 新增:处理swipe切换 @@ -140,17 +149,14 @@ const ChatBox = () => { const newIndex = currentIndex + direction; if (newIndex >= 0 && newIndex < message.swipes.length) { - setCurrentSwipeId(prev => ({ - ...prev, - [messageId]: newIndex - })); + setCurrentSwipeId({ [messageId]: newIndex }); // ✅ 使用 store 方法 } } }; // 切换选项显示 const toggleOptionsPanel = () => { - setShowOptions(!showOptions); + toggleOptions(); // ✅ 使用 store 方法 }; // 打开聊天选择器 @@ -169,10 +175,10 @@ const ChatBox = () => { if (!response.ok) throw new Error('获取聊天列表失败'); const chats = await response.json(); - setCharacterChats(chats); + setCharacterChats(chats); // ✅ 使用 store 方法 // 显示聊天选择器弹窗,让用户手动选择 - setShowChatSelector(true); + toggleChatSelector(); // ✅ 使用 store 方法 } catch (error) { console.error('获取聊天列表失败:', error); alert('获取聊天列表失败: ' + error.message); @@ -182,16 +188,13 @@ const ChatBox = () => { // 选择聊天并加载 const handleSelectChat = async (chatName) => { try { - const { setChatBoxRoleAndChat, fetchChatHistory, currentRole } = useChatBoxStore.getState(); + const { setChatBoxRoleAndChat, currentRole } = useChatBoxStore.getState(); - // 设置当前角色和聊天 + // 设置当前角色和聊天,这会触发 ChatBoxStore 的监听器自动加载聊天历史 setChatBoxRoleAndChat(currentRole, chatName); - // 加载聊天历史 - await fetchChatHistory(currentRole, chatName); - // 关闭选择器 - setShowChatSelector(false); + setShowChatSelector(false); // ✅ 使用 store 方法 console.log(`已切换到聊天: ${chatName}`); } catch (error) { @@ -336,7 +339,7 @@ const ChatBox = () => { diff --git a/frontend/src/components/SideBarLeft/SideBarLeft.jsx b/frontend/src/components/SideBarLeft/SideBarLeft.jsx index 1ce79c0..4051543 100644 --- a/frontend/src/components/SideBarLeft/SideBarLeft.jsx +++ b/frontend/src/components/SideBarLeft/SideBarLeft.jsx @@ -2,7 +2,7 @@ import React from 'react'; import './SideBarLeft.css'; import { useSideBarLeftStore } from '../../Store/indexStore'; -import useSideBarRightStore from '../../Store/SideBarLeft/SideBarLeftSlice'; +import usePresetStore from '../../Store/SideBarLeft/PresetSlice'; import Gallery from './tabs/Gallery'; import CharacterCard from './tabs/CharacterCard'; import ApiConfig from './tabs/ApiConfig'; @@ -11,6 +11,17 @@ import WorldBook from './tabs/WorldBook'; const SideBarLeft = () => { const { activeTab, tabs, setActiveTab } = useSideBarLeftStore(); + const { fetchPresets } = usePresetStore(); + + // 处理标签切换 + const handleTabClick = (tabId) => { + setActiveTab(tabId); + + // 如果切换到预设标签,刷新预设列表 + if (tabId === 'presets') { + fetchPresets(); + } + }; return (
@@ -19,7 +30,7 @@ const SideBarLeft = () => {
+ +
+ + { + // 支持多种分隔符:逗号、分号、空格 + const tagsList = e.target.value + .split(/[,;\s]+/) + .map(tag => tag.trim()) + .filter(tag => tag !== ''); + handleFormChange('tags', tagsList); + }} + placeholder="力量:80, 敏捷:65, 智力:90, HP:100" + /> + 每个标签是一个键值对,用逗号/分号/空格分隔。双击右侧表格中的标签可编辑。 +
)} @@ -514,7 +507,7 @@ const CharacterCard = () => {
- - {/* 操作按钮 */} -
- - - - + + {/* 操作下拉菜单 */} +
+ + + {showActionsMenu && ( +
+ + + + +
+ )} +
@@ -456,29 +585,38 @@ const PresetPanel = () => { onClick={toggleParametersExpanded} > 参数设置 + {/* 生成数量 - 放在标题栏旁边 */} +
+ + handleParameterChange('n', e.target.value)} + className="header-param-input" + onClick={(e) => e.stopPropagation()} // 防止点击输入框时折叠 + /> +
{isParametersExpanded && (
- {/* 温度滑块 */} -
+ {/* 温度输入框 */} +
- handleParameterChange('temperature', e.target.value)} - className="parameter-slider" - /> { step="0.1" value={parameters.temperature} onChange={(e) => handleParameterChange('temperature', e.target.value)} - className="parameter-number" + className="parameter-input-compact" />
- {/* 频率惩罚滑块 */} -
+ {/* 频率惩罚输入框 */} +
- handleParameterChange('frequency_penalty', e.target.value)} - className="parameter-slider" - /> handleParameterChange('frequency_penalty', e.target.value)} - className="parameter-number" + onChange={(e) => handleParameterChange('freq_penalty', e.target.value)} + className="parameter-input-compact" />
- {/* 存在惩罚滑块 */} -
+ {/* 存在惩罚输入框 */} +
- handleParameterChange('presence_penalty', e.target.value)} - className="parameter-slider" - /> handleParameterChange('presence_penalty', e.target.value)} - className="parameter-number" + onChange={(e) => handleParameterChange('pres_penalty', e.target.value)} + className="parameter-input-compact" />
- {/* Top P 滑块 */} -
+ {/* Top P 输入框 */} +
- handleParameterChange('top_p', e.target.value)} - className="parameter-slider" - /> { step="0.05" value={parameters.top_p} onChange={(e) => handleParameterChange('top_p', e.target.value)} - className="parameter-number" + className="parameter-input-compact" />
{/* Top K 输入框 */} -
+
handleParameterChange('top_k', e.target.value)} - className="parameter-input" + className="parameter-input-compact" />
{/* 最大上下文输入框 */} -
+
handleParameterChange('max_context', e.target.value)} - className="parameter-input" + onChange={(e) => handleParameterChange('max_ctx', e.target.value)} + className="parameter-input-compact" />
{/* 最大Token输入框 */} -
+
{ max="100000" value={parameters.max_tokens} onChange={(e) => handleParameterChange('max_tokens', e.target.value)} - className="parameter-input" + className="parameter-input-compact" />
{/* 随机种子输入框 */} -
+
handleParameterChange('seed', e.target.value)} - className="parameter-input" + className="parameter-input-compact" />
- - {/* 生成数量输入框 */} -
- - handleParameterChange('n', e.target.value)} - className="parameter-input" - /> -
- - {/* 开关选项 */} -
-
- - handleParameterChange('max_context_unlocked', e.target.checked)} - className="toggle-checkbox" - /> -
- -
- - handleParameterChange('stream_openai', e.target.checked)} - className="toggle-checkbox" - /> -
-
)}
@@ -793,6 +851,51 @@ const PresetPanel = () => { />
+ {/* 分页控件 */} + {totalPages > 1 && ( +
+ + + + 第 {currentPage} / {totalPages} 页 + + + + + +
+ )} +
); diff --git a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css index cc4dc69..b11d8ae 100644 --- a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css +++ b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css @@ -4,7 +4,7 @@ display: flex; flex-direction: column; height: 100%; - padding: 8px; + padding: 0; /* sidebar-content已有padding,这里不需要 */ gap: 8px; background: var(--color-bg-primary); overflow-y: auto; diff --git a/frontend/src/components/SideBarRight/tabs/Dice/Dice.css b/frontend/src/components/SideBarRight/tabs/Dice/Dice.css index e41f0e5..5025130 100644 --- a/frontend/src/components/SideBarRight/tabs/Dice/Dice.css +++ b/frontend/src/components/SideBarRight/tabs/Dice/Dice.css @@ -3,6 +3,7 @@ display: flex; flex-direction: column; height: 100%; + padding: 0; /* tab-content已有padding,这里不需要 */ background-color: var(--color-bg-secondary); overflow: hidden; } @@ -34,7 +35,7 @@ flex: 1; display: flex; flex-direction: column; - padding: 12px; + padding: 0; /* 移除内部padding,依赖外层tab-content */ gap: 12px; overflow-y: auto; } diff --git a/frontend/src/components/SideBarRight/tabs/Dice/Dice.jsx b/frontend/src/components/SideBarRight/tabs/Dice/Dice.jsx index 4127460..692b246 100644 --- a/frontend/src/components/SideBarRight/tabs/Dice/Dice.jsx +++ b/frontend/src/components/SideBarRight/tabs/Dice/Dice.jsx @@ -33,15 +33,27 @@ const Dice = () => { }; } - // 解析多个骰子组合,如 "1d4+1d3" + // 解析多个骰子组合,如 "1d4+1d3" 或 "1d10-1d6" const parts = expr.split(/(?=[+-])/); const components = []; let total = 0; let detail = []; for (const part of parts) { + // 提取符号(如果有) + let sign = 1; + let cleanPart = part; + + if (part.startsWith('+')) { + sign = 1; + cleanPart = part.substring(1); + } else if (part.startsWith('-')) { + sign = -1; + cleanPart = part.substring(1); + } + // 匹配 NdM 格式 - const diceMatch = part.match(/^(\d*)d(\d+)$/i); + const diceMatch = cleanPart.match(/^(\d*)d(\d+)$/i); if (diceMatch) { const count = diceMatch[1] ? parseInt(diceMatch[1]) : 1; const sides = parseInt(diceMatch[2]); @@ -53,25 +65,26 @@ const Dice = () => { } const sum = rolls.reduce((a, b) => a + b, 0); - total += sum; + total += sign * sum; // 应用符号 components.push({ type: 'dice', count, sides, rolls, - sum + sum: sign * sum }); - detail.push(`${count}d${sides}=[${rolls.join(',')}]`); + const signStr = sign > 0 && detail.length > 0 ? '+' : ''; + detail.push(`${signStr}${count}d${sides}=[${rolls.join(',')}]`); } else { - // 匹配固定数值(带符号) - const fixedMatch = part.match(/^([+-])(\d+)$/); + // 匹配固定数值 + const fixedMatch = cleanPart.match(/^(\d+)$/); if (fixedMatch) { - const sign = fixedMatch[1] === '+' ? 1 : -1; - const value = parseInt(fixedMatch[2]); + const value = parseInt(fixedMatch[1]); total += sign * value; - detail.push(`${sign > 0 ? '+' : '-'}${value}`); + const signStr = sign > 0 && detail.length > 0 ? '+' : ''; + detail.push(`${signStr}${value}`); } } } diff --git a/frontend/src/components/SideBarRight/tabs/RagRecall/RagRecall.css b/frontend/src/components/SideBarRight/tabs/RagRecall/RagRecall.css index caa5916..1942b92 100644 --- a/frontend/src/components/SideBarRight/tabs/RagRecall/RagRecall.css +++ b/frontend/src/components/SideBarRight/tabs/RagRecall/RagRecall.css @@ -4,7 +4,7 @@ display: flex; flex-direction: column; height: 100%; - padding: 8px; + padding: 0; /* tab-content已有padding,这里不需要 */ gap: 8px; background: var(--color-bg-primary); overflow-y: auto; diff --git a/frontend/src/components/SideBarRight/tabs/Table/Table.css b/frontend/src/components/SideBarRight/tabs/Table/Table.css new file mode 100644 index 0000000..66a347b --- /dev/null +++ b/frontend/src/components/SideBarRight/tabs/Table/Table.css @@ -0,0 +1,275 @@ +/* 表格面板 */ +.table-panel { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--color-bg-primary); +} + +.tab-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + border-bottom: 1px solid var(--color-border-light); + background-color: var(--color-bg-secondary); +} + +.title-text { + font-size: 0.95rem; + font-weight: 600; + color: var(--color-text-primary); +} + +.table-info { + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.refresh-btn { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 4px; + background-color: transparent; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; + padding: 0; +} + +.refresh-btn:hover:not(:disabled) { + background-color: rgba(102, 126, 234, 0.15); + transform: rotate(180deg); +} + +.refresh-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* 空状态 */ +.empty-table { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + color: var(--color-text-muted); + padding: 16px; +} + +.empty-table p:first-child { + font-size: 2rem; + margin: 0; +} + +.empty-table p:nth-child(2) { + font-size: 0.9rem; + margin: 0; + font-weight: 500; +} + +/* 表格容器 - 支持滚动 */ +.table-container { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* 标签容器 - 横向排列,自动换行 */ +.tags-container { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-content: flex-start; +} + +.no-tags-hint { + width: 100%; + text-align: center; + padding: 12px; + color: var(--color-text-muted); + font-style: italic; + font-size: 0.85rem; +} + +/* 单个标签项 */ +.tag-item { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background-color: var(--color-bg-elevated); + border: 1px solid var(--color-border); + border-radius: 16px; + font-size: 0.82rem; + color: var(--color-text-primary); + transition: all 0.15s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + max-width: 100%; +} + +.tag-item:hover { + border-color: var(--color-accent); + box-shadow: 0 2px 4px rgba(102, 126, 234, 0.15); + transform: translateY(-1px); +} + +.tag-item.editing { + border-color: var(--color-accent); + background-color: rgba(102, 126, 234, 0.1); + padding: 3px 6px; +} + +/* 标签文本 */ +.tag-text { + cursor: pointer; + user-select: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +.tag-text:hover { + color: var(--color-accent); +} + +/* 标签编辑容器 */ +.tag-edit-container { + display: flex; + align-items: center; + width: 100%; +} + +.tag-edit-input { + flex: 1; + padding: 2px 6px; + border: 1px solid var(--color-accent); + border-radius: 12px; + font-size: 0.82rem; + outline: none; + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + min-width: 100px; +} + +.tag-edit-input:focus { + border-color: var(--color-accent); + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); +} + +/* 标签操作按钮组 */ +.tag-actions { + display: flex; + gap: 2px; + opacity: 0; + transition: opacity 0.15s ease; +} + +.tag-item:hover .tag-actions { + opacity: 1; +} + +.tag-action-btn { + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 50%; + cursor: pointer; + font-size: 0.7rem; + transition: all 0.15s ease; + background-color: transparent; + color: var(--color-text-muted); +} + +.tag-action-btn:hover:not(:disabled) { + background-color: rgba(102, 126, 234, 0.15); + color: var(--color-accent); + transform: scale(1.1); +} + +.tag-action-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.tag-action-btn.delete:hover:not(:disabled) { + background-color: rgba(244, 67, 54, 0.15); + color: #f44336; +} + +/* 添加标签输入框 */ +.add-tag-container { + display: flex; + gap: 6px; + padding: 8px; + background-color: var(--color-bg-secondary); + border-radius: 6px; + border: 1px solid var(--color-border-light); +} + +.add-tag-input { + flex: 1; + padding: 6px 10px; + border: 1px solid var(--color-border); + border-radius: 4px; + font-size: 0.82rem; + outline: none; + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + transition: border-color 0.2s ease; +} + +.add-tag-input:focus { + border-color: var(--color-accent); + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1); +} + +.add-tag-input::placeholder { + color: var(--color-text-muted); + opacity: 0.6; +} + +.add-tag-btn { + padding: 6px 12px; + background-color: var(--color-accent); + color: white; + border: none; + border-radius: 4px; + font-size: 0.82rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.add-tag-btn:hover:not(:disabled) { + background-color: var(--color-accent-dark); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); +} + +.add-tag-btn:disabled { + opacity: 0.4; + cursor: not-allowed; + transform: none; +} diff --git a/frontend/src/components/SideBarRight/tabs/Table/Table.jsx b/frontend/src/components/SideBarRight/tabs/Table/Table.jsx index 4064580..9aec6b3 100644 --- a/frontend/src/components/SideBarRight/tabs/Table/Table.jsx +++ b/frontend/src/components/SideBarRight/tabs/Table/Table.jsx @@ -1,10 +1,198 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { useTableStore } from '../../../../Store/SideBarRight'; +import './Table.css'; const Table = () => { + // ✅ 从 Zustand store 获取状态和方法 + const { + tags, + isLoading, + currentRole, + currentChat, + loadTags, + updateTags, + addTag, + deleteTag, + moveLeft, + moveRight, + editTag, + refresh + } = useTableStore(); + + // 本地 UI 状态(不需要持久化) + const [editingIndex, setEditingIndex] = useState(null); + const [editValue, setEditValue] = useState(''); + const [newTagInput, setNewTagInput] = useState(''); + + // 监听角色/聊天变化,自动加载数据 + useEffect(() => { + if (currentRole) { + loadTags(currentRole, currentChat); + } + }, [currentRole, currentChat, loadTags]); + + // 开始编辑标签 + const handleStartEdit = (index, value) => { + setEditingIndex(index); + setEditValue(value); + }; + + // 保存编辑 + const handleSaveEdit = () => { + if (editingIndex === null) return; + + editTag(editingIndex, editValue); + setEditingIndex(null); + setEditValue(''); + }; + + // 取消编辑 + const handleCancelEdit = () => { + setEditingIndex(null); + setEditValue(''); + }; + + // 添加新标签(支持批量输入) + const handleAddTag = () => { + const input = newTagInput.trim(); + if (!input) return; + + // 智能分割:支持空格、逗号、分号作为分隔符 + const newTagsList = input + .split(/[\s,;,;]+/) + .filter(tag => tag.trim() !== '') + .map(tag => tag.trim()); + + addTag(newTagsList); + setNewTagInput(''); + }; + + // 如果没有角色,显示提示 + if (!currentRole) { + return ( +
+
+ 动态表格 +
+
+

📊

+

请选择一个角色

+
+
+ ); + } + return (
-

动态表格

-

这是动态表格面板的占位页面

+
+ 动态表格 +
+ + {tags.length} 个标签 + + +
+
+ +
+ {/* 标签列表 */} +
+ {tags.length === 0 ? ( +
+ 暂无标签,请在下方添加 +
+ ) : ( + tags.map((tag, index) => ( +
+ {editingIndex === index ? ( + // 编辑模式 +
+ setEditValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveEdit(); + if (e.key === 'Escape') handleCancelEdit(); + }} + onBlur={handleSaveEdit} + autoFocus + /> +
+ ) : ( + // 显示模式 + <> + handleStartEdit(index, tag)} + title="双击编辑" + > + {tag} + +
+ + + +
+ + )} +
+ )) + )} +
+ + {/* 添加新标签输入框 */} +
+ setNewTagInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleAddTag(); + }} + /> + +
+
); }; diff --git a/frontend/src/components/TopBar/TopBar.jsx b/frontend/src/components/TopBar/TopBar.jsx index 8fc7588..b3c3183 100644 --- a/frontend/src/components/TopBar/TopBar.jsx +++ b/frontend/src/components/TopBar/TopBar.jsx @@ -1,14 +1,26 @@ // frontend-react/src/components/TopBar/TopBar.jsx -import React, {useState, useRef, useEffect} from 'react'; -import './TopBar.css'; -import ThemeToggle from './items/ThemeToggle'; +import React, { useState, useRef, useEffect } from 'react'; +import useAppLayoutStore from '../../Store/AppLayoutSlice'; // ✅ 新增 +import useUserStore from '../../Store/UserSlice'; // ✅ 新增 - 用户角色 import useWorldBookStore from '../../Store/SideBarLeft/WorldBookSlice'; import useApiConfigStore from '../../Store/SideBarLeft/ApiConfigSlice'; +import ThemeToggle from './items/ThemeToggle'; +import './TopBar.css'; -const Toolbar = ({ sidebarMode, colorTheme, onSidebarModeChange, onColorThemeChange }) => { +const Toolbar = () => { const [activePanel, setActivePanel] = useState(null); const panelRef = useRef(null); - const [currentUserRole, setCurrentUserRole] = useState({ name: '', description: '' }); + + // ✅ 直接从 AppLayoutStore 获取状态和方法 + const { + sidebarMode, + colorTheme, + setSidebarMode, + setColorTheme + } = useAppLayoutStore(); + + // ✅ 从 UserStore 获取用户角色 + const { currentUserRole } = useUserStore(); // 从 Store 获取全局世界书 const { globalWorldBooks } = useWorldBookStore(); @@ -17,41 +29,6 @@ const Toolbar = ({ sidebarMode, colorTheme, onSidebarModeChange, onColorThemeCha const { activeMap, fetchProfile } = useApiConfigStore(); const [coreModel, setCoreModel] = useState('未设置'); const [assistModel, setAssistModel] = useState('未设置'); - - // 加载核心和辅助 API 配置的模型名 - useEffect(() => { - const loadApiModels = async () => { - // 加载核心配置 - if (activeMap['core']) { - try { - const profile = await fetchProfile(activeMap['core']); - const coreApi = profile?.apis?.find(api => api.category === 'core'); - setCoreModel(coreApi?.model || '未设置'); - } catch (e) { - console.error('加载核心配置失败:', e); - setCoreModel('未设置'); - } - } else { - setCoreModel('未设置'); - } - - // 加载辅助配置 - if (activeMap['assist']) { - try { - const profile = await fetchProfile(activeMap['assist']); - const assistApi = profile?.apis?.find(api => api.category === 'assist'); - setAssistModel(assistApi?.model || '未设置'); - } catch (e) { - console.error('加载辅助配置失败:', e); - setAssistModel('未设置'); - } - } else { - setAssistModel('未设置'); - } - }; - - loadApiModels(); - }, [activeMap, fetchProfile]); // 点击外部关闭面板 - 使用 useCallback 优化 const handleClickOutside = React.useCallback((event) => { @@ -67,17 +44,10 @@ const Toolbar = ({ sidebarMode, colorTheme, onSidebarModeChange, onColorThemeCha }; }, [handleClickOutside]); - // 加载当前用户角色 - useEffect(() => { - const savedRole = localStorage.getItem('currentUserRole'); - if (savedRole) { - try { - setCurrentUserRole(JSON.parse(savedRole)); - } catch (e) { - console.error('解析用户角色失败:', e); - } - } - }, []); + // ✅ 简化:直接调用 store 方法 + const handleSidebarModeChange = (mode) => { + setSidebarMode(mode); + }; // 处理面板切换 const handlePanelToggle = (panelName) => { @@ -218,7 +188,7 @@ const Toolbar = ({ sidebarMode, colorTheme, onSidebarModeChange, onColorThemeCha name="sidebarMode" value="fixed" checked={sidebarMode === 'fixed'} - onChange={(e) => onSidebarModeChange(e.target.value)} + onChange={(e) => handleSidebarModeChange(e.target.value)} // ✅ 使用 store 方法 /> 固定 @@ -228,7 +198,7 @@ const Toolbar = ({ sidebarMode, colorTheme, onSidebarModeChange, onColorThemeCha name="sidebarMode" value="smart" checked={sidebarMode === 'smart'} - onChange={(e) => onSidebarModeChange(e.target.value)} + onChange={(e) => handleSidebarModeChange(e.target.value)} // ✅ 使用 store 方法 /> 智能 @@ -238,7 +208,7 @@ const Toolbar = ({ sidebarMode, colorTheme, onSidebarModeChange, onColorThemeCha name="sidebarMode" value="expanded" checked={sidebarMode === 'expanded'} - onChange={(e) => onSidebarModeChange(e.target.value)} + onChange={(e) => handleSidebarModeChange(e.target.value)} // ✅ 使用 store 方法 /> 扩展 diff --git a/frontend/src/index.css b/frontend/src/index.css index d60b338..d68acd8 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -39,15 +39,15 @@ /* CSS Variables for layout ratios */ :root { - /* Fixed mode: 1 : 3.5 : 1.25 (default) */ - --left-fixed: 1fr; - --middle-fixed: 3.5fr; - --right-fixed: 1.25fr; + /* Fixed mode (default): 2 : 6 : 2 */ + --left-fixed: 2fr; + --middle-fixed: 6fr; + --right-fixed: 2fr; - /* Expanded mode: 1.8 : 2 : 1 */ - --left-expanded: 1.8fr; - --middle-expanded: 2fr; - --right-expanded: 1fr; + /* Expanded mode: 4 : 4.5 : 1.5 */ + --left-expanded: 4fr; + --middle-expanded: 4.5fr; + --right-expanded: 1.5fr; } .main-container { @@ -74,13 +74,13 @@ transition: flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1); } -/* Fixed mode (default): 1 : 3.5 : 1.25 */ +/* Fixed mode (default): 2 : 6 : 2 */ .app.chat-mode .main-container, .sidebar-mode-fixed ~ .chat-area-wrapper { grid-template-columns: var(--left-fixed) var(--middle-fixed) var(--right-fixed); } -/* Expanded mode: 1.8 : 2 : 1 */ +/* Expanded mode: 4 : 4.5 : 1.5 */ .app.edit-mode .main-container, .sidebar-mode-expanded, .sidebar-mode-smart.sidebar-expanded { @@ -181,13 +181,13 @@ @media (min-width: 1920px) { .app.chat-mode .main-container, .sidebar-mode-fixed ~ .chat-area-wrapper { - grid-template-columns: minmax(280px, var(--left-fixed)) var(--middle-fixed) minmax(340px, var(--right-fixed)); + grid-template-columns: minmax(320px, var(--left-fixed)) var(--middle-fixed) minmax(320px, var(--right-fixed)); } .app.edit-mode .main-container, .sidebar-mode-expanded, .sidebar-mode-smart.sidebar-expanded { - grid-template-columns: minmax(350px, var(--left-expanded)) var(--middle-expanded) minmax(250px, var(--right-expanded)) !important; + grid-template-columns: minmax(450px, var(--left-expanded)) var(--middle-expanded) minmax(200px, var(--right-expanded)) !important; } } diff --git a/frontend/src/types/internal.types.ts b/frontend/src/types/internal.types.ts index 12d6972..e6b8db9 100644 --- a/frontend/src/types/internal.types.ts +++ b/frontend/src/types/internal.types.ts @@ -134,8 +134,17 @@ export interface ChatHeader { /** AI 角色名称 */ characterName: string; - /** 表格内容(对应角色卡的 outputSchema 字段的值) */ - tableData?: Record; + /** 动态表格表头 (从角色卡继承) */ + tableHeaders?: string[]; + + /** 表格默认值 (键为表头名,值为默认值) */ + tableDefaults?: Record; + + /** 表格数据行 (每行是一个字典) */ + tableData?: Record[]; + + /** 表格数据行时间戳 (键为行索引,值为更新时间戳) */ + tableTimestamps?: Record; /** 创建时间戳 */ createdAt: number; diff --git a/test_data_access.py b/test_data_access.py new file mode 100644 index 0000000..8a5be39 --- /dev/null +++ b/test_data_access.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +测试后端是否能正确访问 data 目录 +""" +import sys +from pathlib import Path + +# 添加 backend 到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent)) + +try: + from backend.core.config import settings +except ImportError: + from core.config import settings + +def test_paths(): + """测试所有路径配置""" + print("=" * 60) + print("路径配置测试") + print("=" * 60) + + # 测试基本路径 + print(f"\n1. 项目根目录 (BASE_PATH):") + print(f" {settings.BASE_PATH}") + print(f" 存在: {settings.BASE_PATH.exists()}") + + print(f"\n2. 数据目录 (DATA_PATH):") + print(f" {settings.DATA_PATH}") + print(f" 存在: {settings.DATA_PATH.exists()}") + + # 测试子目录 + directories = { + "预设目录 (PRESET_PATH)": settings.PRESET_PATH, + "角色卡目录 (CHARACTERS_PATH)": settings.CHARACTERS_PATH, + "聊天目录 (CHAT_PATH)": settings.CHAT_PATH, + "世界书目录 (WORLDBOOKS_PATH)": settings.WORLDBOOKS_PATH, + "API配置目录": settings.DATA_PATH / "apiconfig", + "图片目录 (IMAGES_PATH)": settings.IMAGES_PATH, + } + + print("\n3. 子目录检查:") + for name, path in directories.items(): + exists = path.exists() + status = "✅" if exists else "❌" + print(f" {status} {name}:") + print(f" {path}") + + if exists: + # 列出目录内容(如果有) + try: + items = list(path.iterdir()) + if items: + print(f" 内容: {[item.name for item in items[:5]]}") + else: + print(f" (空目录)") + except Exception as e: + print(f" 读取失败: {e}") + + # 测试预设文件 + print("\n4. 预设文件测试:") + preset_files = list(settings.PRESET_PATH.glob("*.json")) if settings.PRESET_PATH.exists() else [] + print(f" 找到 {len(preset_files)} 个预设文件") + for pf in preset_files[:3]: + print(f" - {pf.name}") + + # 测试角色卡 + print("\n5. 角色卡测试:") + char_dirs = list(settings.CHARACTERS_PATH.iterdir()) if settings.CHARACTERS_PATH.exists() else [] + char_count = sum(1 for d in char_dirs if d.is_dir()) + print(f" 找到 {char_count} 个角色") + for cd in char_dirs[:3]: + if cd.is_dir(): + char_file = cd / "character.json" + has_char_file = char_file.exists() + print(f" - {cd.name} (character.json: {'✅' if has_char_file else '❌'})") + + print("\n" + "=" * 60) + print("测试完成!") + print("=" * 60) + +if __name__ == "__main__": + test_paths() diff --git a/test_route_import.py b/test_route_import.py new file mode 100644 index 0000000..6d112d2 --- /dev/null +++ b/test_route_import.py @@ -0,0 +1,43 @@ +""" +测试路由导入是否正常 +""" +import sys +from pathlib import Path + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent / "backend")) + +print("=" * 70) +print("测试路由导入") +print("=" * 70) + +try: + print("\n1. 尝试导入 presetsRoute...") + from api.routes import presetsRoute + print("✓ 成功导入 presetsRoute") + + print("\n2. 检查 router 对象...") + print(f" router 类型: {type(presetsRoute.router)}") + print(f" router 前缀: {presetsRoute.router.prefix}") + + print("\n3. 尝试调用 list_presets 函数...") + # 注意:这是异步函数,不能直接调用 + print(f" list_presets 函数: {presetsRoute.list_presets}") + print(f" 函数类型: {type(presetsRoute.list_presets)}") + + print("\n4. 直接调用 PresetService.list_presets()...") + result = presetsRoute.PresetService.list_presets() + print(f" 返回结果: {result}") + print(f" 结果长度: {len(result)}") + + if len(result) > 0: + print("\n✓ 路由导入和调用都正常!") + else: + print("\n✗ 警告:返回空列表") + +except Exception as e: + print(f"\n✗ 导入失败: {e}") + import traceback + traceback.print_exc() + +print("\n" + "=" * 70) diff --git a/test_sillytavern_compat.py b/test_sillytavern_compat.py deleted file mode 100644 index c387eb4..0000000 --- a/test_sillytavern_compat.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -测试世界书 SillyTavern 格式兼容性 -""" -import json -from pathlib import Path - -# 项目根目录 -PROJECT_ROOT = Path(r'D:\progarm\python\llm_workflow_engine') -DATA_PATH = PROJECT_ROOT / 'data' -WORLDBOOKS_PATH = DATA_PATH / 'worldbooks' - -print("=" * 60) -print("世界书 SillyTavern 格式兼容性测试") -print("=" * 60) - -# 列出所有JSON文件 -json_files = list(WORLDBOOKS_PATH.glob("*.json")) -print(f"\n找到 {len(json_files)} 个世界书文件") - -for file_path in json_files: - print(f"\n{'='*60}") - print(f"测试文件: {file_path.name}") - print(f"{'='*60}") - - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - entries_raw = data.get("entries", []) - - print(f"\n1. 格式检测:") - if isinstance(entries_raw, dict): - print(f" 类型: SillyTavern 格式 (字典)") - print(f" 条目数量: {len(entries_raw)}") - print(f" 键示例: {list(entries_raw.keys())[:5]}") - - # 模拟转换为列表 - all_entries = [] - for key in sorted(entries_raw.keys(), key=lambda x: int(x) if x.isdigit() else 0): - entry = entries_raw[key] - if 'uid' not in entry: - entry['uid'] = key - all_entries.append(entry) - - print(f"\n2. 转换后:") - print(f" 列表长度: {len(all_entries)}") - if all_entries: - print(f" 第一条目 UID: {all_entries[0].get('uid')}") - print(f" 第一条目键: {all_entries[0].get('key', 'N/A')}") - - # 测试分页 - print(f"\n3. 分页测试 (page=1, page_size=3):") - page = 1 - page_size = 3 - start_idx = (page - 1) * page_size - end_idx = start_idx + page_size - paginated = all_entries[start_idx:end_idx] - total_pages = (len(all_entries) + page_size - 1) // page_size - - print(f" 总条目数: {len(all_entries)}") - print(f" 总页数: {total_pages}") - print(f" 当前页返回: {len(paginated)} 条") - for i, entry in enumerate(paginated, start=1): - print(f" {i}. UID={entry.get('uid')}, 内容长度={len(entry.get('content', ''))}") - - elif isinstance(entries_raw, list): - print(f" 类型: 内部格式 (列表)") - print(f" 条目数量: {len(entries_raw)}") - - # 测试分页 - print(f"\n2. 分页测试 (page=1, page_size=3):") - page = 1 - page_size = 3 - start_idx = (page - 1) * page_size - end_idx = start_idx + page_size - paginated = entries_raw[start_idx:end_idx] - total_pages = (len(entries_raw) + page_size - 1) // page_size - - print(f" 总条目数: {len(entries_raw)}") - print(f" 总页数: {total_pages}") - print(f" 当前页返回: {len(paginated)} 条") - for i, entry in enumerate(paginated, start=1): - print(f" {i}. UID={entry.get('uid')}, 内容长度={len(entry.get('content', ''))}") - else: - print(f" ⚠️ 未知格式: {type(entries_raw)}") - - except Exception as e: - print(f" ❌ 错误: {e}") - import traceback - traceback.print_exc() - -print("\n" + "=" * 60) -print("测试完成") -print("=" * 60) diff --git a/test_simple_model_service.py b/test_simple_model_service.py deleted file mode 100644 index efced87..0000000 --- a/test_simple_model_service.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -简单测试 LLM 模型服务的提供商检测功能 -""" -import sys -import os - -# 添加backend目录到路径 -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend')) - -# 直接导入服务模块,避免导入__init__.py -from services.llm_model_service import LLMModelService - - -def test_detect_provider(): - """测试提供商检测功能""" - print("=" * 60) - print("测试提供商检测功能") - print("=" * 60) - - test_cases = [ - ("https://api.openai.com/v1", "openai"), - ("https://api.anthropic.com/v1", "anthropic"), - ("http://localhost:11434", "ollama"), - ("http://127.0.0.1:11434", "ollama"), - ("https://api.siliconflow.cn/v1", "openai"), - ("https://api.deepseek.com/v1", "openai"), - ("https://custom-api.example.com/v1", "openai"), - ] - - all_passed = True - for url, expected in test_cases: - result = LLMModelService.detect_provider(url) - passed = result == expected - status = "✓" if passed else "✗" - print(f" {status} {url}") - print(f" 结果: {result}, 期望: {expected}") - if not passed: - all_passed = False - - print("=" * 60) - if all_passed: - print("所有测试通过!✓") - else: - print("部分测试失败!✗") - print("=" * 60) - - return all_passed - - -if __name__ == "__main__": - test_detect_provider() diff --git a/test_worldbook_direct.py b/test_worldbook_direct.py deleted file mode 100644 index b9ea373..0000000 --- a/test_worldbook_direct.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -直接测试世界书文件读取(不依赖backend模块) -""" -import json -from pathlib import Path - -# 项目根目录 -PROJECT_ROOT = Path(r'D:\progarm\python\llm_workflow_engine') -DATA_PATH = PROJECT_ROOT / 'data' -WORLDBOOKS_PATH = DATA_PATH / 'worldbooks' - -print("=" * 60) -print("世界书文件直接读取测试") -print("=" * 60) - -print(f"\n1. 路径信息:") -print(f" 世界书目录: {WORLDBOOKS_PATH}") -print(f" 目录存在: {WORLDBOOKS_PATH.exists()}") - -# 列出所有JSON文件 -json_files = list(WORLDBOOKS_PATH.glob("*.json")) -print(f"\n2. JSON文件列表:") -print(f" 文件数量: {len(json_files)}") -for f in json_files: - print(f" - {f.name} ({f.stat().st_size} bytes)") - -if json_files: - # 尝试读取第一个文件 - first_file = json_files[0] - print(f"\n3. 尝试读取文件: {first_file.name}") - - try: - with open(first_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - print(f" ✓ 文件读取成功") - print(f" 文件格式: JSON") - print(f" 顶层键: {list(data.keys())}") - - # 检查是否是内部格式 - if 'entries' in data: - entries = data['entries'] - if isinstance(entries, list): - print(f" 格式类型: 内部格式 (Internal)") - print(f" 条目数量: {len(entries)}") - print(f" 世界书名称: {data.get('name', '未设置')}") - print(f" 描述: {data.get('description', '无')}") - - if entries: - print(f"\n 第一条目预览:") - first_entry = entries[0] - print(f" UID: {first_entry.get('uid', 'N/A')}") - print(f" 内容长度: {len(first_entry.get('content', ''))}") - print(f" 键: {first_entry.get('key', [])}") - elif isinstance(entries, dict): - print(f" 格式类型: SillyTavern格式") - print(f" 条目数量: {len(entries)}") - else: - print(f" ⚠️ 未知格式,缺少 'entries' 键") - - except json.JSONDecodeError as e: - print(f" ❌ JSON解析失败: {e}") - except Exception as e: - print(f" ❌ 读取失败: {e}") - import traceback - traceback.print_exc() - - # 如果有多个文件,也检查第二个 - if len(json_files) > 1: - second_file = json_files[1] - print(f"\n4. 尝试读取第二个文件: {second_file.name}") - - try: - with open(second_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - print(f" ✓ 文件读取成功") - if 'entries' in data: - entries = data['entries'] - if isinstance(entries, list): - print(f" 格式类型: 内部格式 (Internal)") - print(f" 条目数量: {len(entries)}") - elif isinstance(entries, dict): - print(f" 格式类型: SillyTavern格式") - print(f" 条目数量: {len(entries)}") - except Exception as e: - print(f" ❌ 读取失败: {e}") - -print("\n" + "=" * 60) -print("测试完成") -print("=" * 60) diff --git a/test_worldbook_pagination.py b/test_worldbook_pagination.py deleted file mode 100644 index 8096527..0000000 --- a/test_worldbook_pagination.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -测试世界书分页功能 -""" -import sys -import os -from pathlib import Path -import json - -# 添加backend目录到Python路径 -backend_dir = Path(__file__).parent / "backend" -sys.path.insert(0, str(backend_dir)) - -# 直接导入需要的模块,避免导入整个services包 -from core.config import settings -from services.worldbook_service import WorldBookService - -def test_pagination(): - """测试世界书条目分页功能""" - - # 测试数据:世界书名称 - test_worldbook_name = "测试分页世界书" - - print("=" * 60) - print("测试世界书分页功能") - print("=" * 60) - - try: - # 1. 创建测试世界书 - print("\n1. 创建测试世界书...") - try: - worldbook = WorldBookService.create_worldbook(test_worldbook_name, "用于测试分页功能") - print(f" ✓ 世界书创建成功: {worldbook['name']}") - except ValueError as e: - if "already exists" in str(e): - print(f" ℹ 世界书已存在,跳过创建") - else: - raise - - # 2. 添加测试条目(创建30个条目) - print("\n2. 添加测试条目...") - num_entries = 30 - for i in range(num_entries): - entry_data = { - "key": [f"关键词{i}"], - "content": f"这是第{i+1}个测试条目的内容", - "comment": f"测试条目 {i+1}", - "order": i + 1 - } - WorldBookService.create_entry(test_worldbook_name, entry_data) - print(f" ✓ 成功添加 {num_entries} 个条目") - - # 3. 测试分页功能 - print("\n3. 测试分页功能...") - - # 测试第1页,每页10条 - print("\n 测试第1页(每页10条):") - result = WorldBookService.list_entries(test_worldbook_name, page=1, page_size=10) - print(f" - 总条目数: {result['total']}") - print(f" - 当前页: {result['page']}") - print(f" - 每页数量: {result['page_size']}") - print(f" - 总页数: {result['total_pages']}") - print(f" - 返回条目数: {len(result['entries'])}") - assert result['total'] == num_entries, f"总条目数错误: {result['total']} != {num_entries}" - assert result['page'] == 1, f"页码错误: {result['page']} != 1" - assert result['page_size'] == 10, f"每页数量错误: {result['page_size']} != 10" - assert result['total_pages'] == 3, f"总页数错误: {result['total_pages']} != 3" - assert len(result['entries']) == 10, f"返回条目数错误: {len(result['entries'])} != 10" - print(" ✓ 第1页测试通过") - - # 测试第2页 - print("\n 测试第2页(每页10条):") - result = WorldBookService.list_entries(test_worldbook_name, page=2, page_size=10) - print(f" - 当前页: {result['page']}") - print(f" - 返回条目数: {len(result['entries'])}") - assert result['page'] == 2, f"页码错误: {result['page']} != 2" - assert len(result['entries']) == 10, f"返回条目数错误: {len(result['entries'])} != 10" - print(" ✓ 第2页测试通过") - - # 测试第3页(最后一页,应该只有10条) - print("\n 测试第3页(每页10条):") - result = WorldBookService.list_entries(test_worldbook_name, page=3, page_size=10) - print(f" - 当前页: {result['page']}") - print(f" - 返回条目数: {len(result['entries'])}") - assert result['page'] == 3, f"页码错误: {result['page']} != 3" - assert len(result['entries']) == 10, f"返回条目数错误: {len(result['entries'])} != 10" - print(" ✓ 第3页测试通过") - - # 测试不同的每页数量 - print("\n 测试每页20条:") - result = WorldBookService.list_entries(test_worldbook_name, page=1, page_size=20) - print(f" - 总页数: {result['total_pages']}") - print(f" - 返回条目数: {len(result['entries'])}") - assert result['page_size'] == 20, f"每页数量错误: {result['page_size']} != 20" - assert result['total_pages'] == 2, f"总页数错误: {result['total_pages']} != 2" - assert len(result['entries']) == 20, f"返回条目数错误: {len(result['entries'])} != 20" - print(" ✓ 每页20条测试通过") - - print("\n" + "=" * 60) - print("✅ 所有分页测试通过!") - print("=" * 60) - - # 4. 清理测试数据 - print("\n4. 清理测试数据...") - WorldBookService.delete_worldbook(test_worldbook_name) - print(" ✓ 测试世界书已删除") - - return True - - except Exception as e: - print(f"\n❌ 测试失败: {str(e)}") - import traceback - traceback.print_exc() - - # 尝试清理测试数据 - try: - WorldBookService.delete_worldbook(test_worldbook_name) - print(" ✓ 测试数据已清理") - except: - pass - - return False - -if __name__ == "__main__": - success = test_pagination() - sys.exit(0 if success else 1) diff --git a/test_worldbook_read.py b/test_worldbook_read.py deleted file mode 100644 index 8efb11e..0000000 --- a/test_worldbook_read.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -测试世界书读取功能 -""" -import sys -from pathlib import Path - -# 添加backend目录到Python路径 -backend_dir = Path(__file__).parent / "backend" -sys.path.insert(0, str(backend_dir)) - -from core.config import settings -from services.worldbook_service import WorldBookService - -print("=" * 60) -print("世界书读取功能测试") -print("=" * 60) - -print(f"\n1. 配置检查:") -print(f" 世界书目录: {settings.WORLDBOOKS_PATH}") -print(f" 目录存在: {settings.WORLDBOOKS_PATH.exists()}") - -# 列出所有JSON文件 -json_files = list(settings.WORLDBOOKS_PATH.glob("*.json")) -print(f" JSON文件数量: {len(json_files)}") -for f in json_files: - print(f" - {f.name}") - -print(f"\n2. 测试读取世界书列表:") -try: - worldbooks = WorldBookService.list_worldbooks() - print(f" ✓ 成功读取世界书列表") - print(f" 世界书数量: {len(worldbooks)}") - for wb in worldbooks: - print(f" - {wb['name']} (条目数: {wb['entries_count']})") -except Exception as e: - print(f" ❌ 读取失败: {e}") - import traceback - traceback.print_exc() - -if worldbooks: - # 测试读取第一个世界书的详细信息 - first_book = worldbooks[0] - book_name = first_book['name'] - - print(f"\n3. 测试读取世界书详情: {book_name}") - try: - book_detail = WorldBookService.get_worldbook(book_name) - print(f" ✓ 成功读取世界书详情") - print(f" 名称: {book_detail.get('name')}") - print(f" 描述: {book_detail.get('description', '无')}") - print(f" 条目数: {len(book_detail.get('entries', []))}") - except Exception as e: - print(f" ❌ 读取失败: {e}") - import traceback - traceback.print_exc() - - # 测试分页功能 - print(f"\n4. 测试分页功能:") - try: - result = WorldBookService.list_entries(book_name, page=1, page_size=5) - print(f" ✓ 分页查询成功") - print(f" 总条目数: {result['total']}") - print(f" 当前页: {result['page']}") - print(f" 每页数量: {result['page_size']}") - print(f" 总页数: {result['total_pages']}") - print(f" 返回条目数: {len(result['entries'])}") - - if result['entries']: - print(f" 第一条目预览:") - first_entry = result['entries'][0] - print(f" UID: {first_entry.get('uid')}") - print(f" 内容长度: {len(first_entry.get('content', ''))}") - except Exception as e: - print(f" ❌ 分页查询失败: {e}") - import traceback - traceback.print_exc() - -print("\n" + "=" * 60) -print("测试完成") -print("=" * 60)