diff --git a/FIX_GLOBAL_WORLDBOOK_SYNC.md b/FIX_GLOBAL_WORLDBOOK_SYNC.md new file mode 100644 index 0000000..7f8aa11 --- /dev/null +++ b/FIX_GLOBAL_WORLDBOOK_SYNC.md @@ -0,0 +1,195 @@ +# 全局世界书数据同步问题修复 + +## 🐛 问题描述 + +**现象**: 当世界书文件被删除后,LocalStorage中仍然保存着该世界书的全局状态,导致前端显示一个不存在的"幽灵"世界书。 + +**原因**: `fetchWorldBooks` 函数从LocalStorage加载全局世界书后,没有清理那些已经不存在于后端的世界书。 + +--- + +## ✅ 解决方案 + +### 修复位置 +**文件**: `frontend/src/Store/SideBarLeft/WorldBookSlice.jsx` +**函数**: `fetchWorldBooks` (第132-165行) + +### 修复逻辑 + +```javascript +// 从 LocalStorage 获取全局世界书列表 +let globalBooks = loadGlobalWorldBooks(); + +// 清理 LocalStorage 中已不存在的世界书 +const existingWorldBookNames = new Set(data.map(wb => wb.name)); +const cleanedGlobalBooks = globalBooks.filter(wb => existingWorldBookNames.has(wb.name)); + +// 如果有被清理的项,更新 LocalStorage +if (cleanedGlobalBooks.length !== globalBooks.length) { + console.log(`清理了 ${globalBooks.length - cleanedGlobalBooks.length} 个不存在的全局世界书`); + saveGlobalWorldBooks(cleanedGlobalBooks); + globalBooks = cleanedGlobalBooks; +} +``` + +### 工作流程 + +1. **获取后端数据**: 调用 `GET /api/worldbooks/` 获取所有存在的世界书 +2. **加载LocalStorage**: 从LocalStorage读取全局世界书列表 +3. **对比清理**: 过滤掉LocalStorage中存在但后端不存在的世界书 +4. **更新存储**: 如果发现有被清理的项,更新LocalStorage +5. **更新State**: 将清理后的列表设置到State中 + +--- + +## 🧪 测试场景 + +### 场景1: 正常情况 +**步骤**: +1. 创建世界书A和B +2. 将A和B都设为全局 +3. 刷新页面 + +**预期结果**: +- ✅ 全局区域显示A和B +- ✅ LocalStorage中有A和B + +### 场景2: 文件被删除 +**步骤**: +1. 创建世界书A和B +2. 将A和B都设为全局 +3. 手动删除世界书A的文件(或其他方式删除) +4. 刷新页面 + +**预期结果**: +- ✅ 全局区域只显示B +- ✅ LocalStorage中只保留B +- ✅ 控制台输出: "清理了 1 个不存在的全局世界书" + +### 场景3: 通过UI删除 +**步骤**: +1. 创建世界书A和B +2. 将A和B都设为全局 +3. 在前端UI中删除世界书A +4. 刷新页面 + +**预期结果**: +- ✅ 全局区域只显示B +- ✅ LocalStorage中只保留B +- ✅ 无错误信息 + +--- + +## 📊 数据流图 + +``` +┌─────────────────┐ +│ 页面加载/切换 │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ fetchWorldBooks │ +└────────┬────────┘ + │ + ├──► GET /api/worldbooks/ ──► 后端返回现有世界书列表 + │ + ├──► loadGlobalWorldBooks() ──► 从LocalStorage读取 + │ + ├──► 对比两个列表 + │ ├─ 存在于LocalStorage但不存在于后端 → 清理 + │ └─ 存在于两者 → 保留 + │ + ├──► saveGlobalWorldBooks() ──► 更新LocalStorage(如有变化) + │ + └──► set state ──► 更新UI +``` + +--- + +## 🔍 关键代码说明 + +### 1. 使用Set提高查找效率 +```javascript +const existingWorldBookNames = new Set(data.map(wb => wb.name)); +``` +- 将后端返回的世界书名称转换为Set +- Set的查找时间复杂度为O(1),比数组的O(n)更高效 + +### 2. 过滤清理 +```javascript +const cleanedGlobalBooks = globalBooks.filter(wb => + existingWorldBookNames.has(wb.name) +); +``` +- 只保留那些在后端也存在的世界书 +- 自动移除"幽灵"世界书 + +### 3. 条件更新 +```javascript +if (cleanedGlobalBooks.length !== globalBooks.length) { + console.log(`清理了 ${globalBooks.length - cleanedGlobalBooks.length} 个不存在的全局世界书`); + saveGlobalWorldBooks(cleanedGlobalBooks); + globalBooks = cleanedGlobalBooks; +} +``` +- 只有在确实有变化时才更新LocalStorage +- 避免不必要的写入操作 +- 提供调试信息 + +--- + +## ✨ 优势 + +1. **自动清理**: 无需手动干预,自动同步LocalStorage和后端数据 +2. **性能优化**: 使用Set提高查找效率 +3. **用户友好**: 静默清理,只在控制台输出日志 +4. **数据一致性**: 确保LocalStorage中的数据始终与后端保持一致 +5. **无副作用**: 不影响正常的业务流程 + +--- + +## 📝 相关代码位置 + +### LocalStorage操作函数 +```javascript +// 辅助函数:从 LocalStorage 加载全局世界书 +const loadGlobalWorldBooks = () => { + try { + const stored = localStorage.getItem(GLOBAL_WORLDBOOKS_KEY); + return stored ? JSON.parse(stored) : []; + } catch (error) { + console.error('加载全局世界书失败:', error); + return []; + } +}; + +// 辅助函数:保存全局世界书到 LocalStorage +const saveGlobalWorldBooks = (globalBooks) => { + try { + localStorage.setItem(GLOBAL_WORLDBOOKS_KEY, JSON.stringify(globalBooks)); + } catch (error) { + console.error('保存全局世界书失败:', error); + } +}; +``` + +### 删除世界书时的清理 +```javascript +// deleteWorldBook 函数中已经有清理逻辑 +deleteWorldBook: async (name) => { + // ... + const filteredGlobalBooks = state.globalWorldBooks.filter(wb => wb.name !== name); + saveGlobalWorldBooks(filteredGlobalBooks); + // ... +} +``` + +--- + +## 🎯 总结 + +✅ **问题已修复**: 全局世界书现在会自动清理不存在的项 +✅ **数据同步**: LocalStorage与后端数据保持一致 +✅ **用户体验**: 不再显示"幽灵"世界书 +✅ **代码健壮**: 增加了数据一致性检查机制 diff --git a/REMOVE_CONFIRM_DIALOG.md b/REMOVE_CONFIRM_DIALOG.md new file mode 100644 index 0000000..0678be8 --- /dev/null +++ b/REMOVE_CONFIRM_DIALOG.md @@ -0,0 +1,194 @@ +# 世界书删除确认弹窗移除 + +## 📋 修改内容 + +移除了世界书模块中所有的浏览器级别确认弹窗(`confirm`),改为直接执行删除操作。 + +--- + +## ✅ 已移除的确认弹窗 + +### 1. 删除世界书条目 +**位置**: `WorldBook.jsx` 第338-349行 + +**修改前**: +```javascript +const handleDeleteEntry = async () => { + if (!currentEntry || !currentWorldBook) return; + + if (confirm('确定要删除此条目吗?')) { + try { + await deleteWorldBookEntry(currentWorldBook.name, currentEntry.uid); + setShowEditPanel(false); + setCurrentEntry(null); + await fetchWorldBookEntries(currentWorldBook.name, currentPage, pageSize); + } catch (err) { + console.error('删除条目失败:', err); + } + } +}; +``` + +**修改后**: +```javascript +const handleDeleteEntry = async () => { + if (!currentEntry || !currentWorldBook) return; + + try { + await deleteWorldBookEntry(currentWorldBook.name, currentEntry.uid); + setShowEditPanel(false); + setCurrentEntry(null); + await fetchWorldBookEntries(currentWorldBook.name, currentPage, pageSize); + } catch (err) { + console.error('删除条目失败:', err); + } +}; +``` + +--- + +### 2. 删除世界书 +**位置**: `WorldBook.jsx` 第354-365行 + +**修改前**: +```javascript +const handleDeleteWorldBook = async () => { + if (!currentWorldBook) return; + + if (confirm(`确定要删除世界书 "${currentWorldBook.name}" 吗?`)) { + try { + await deleteWorldBook(currentWorldBook.name); + resetCurrentWorldBook(); + } catch (err) { + console.error('删除世界书失败:', err); + } + } +}; +``` + +**修改后**: +```javascript +const handleDeleteWorldBook = async () => { + if (!currentWorldBook) return; + + try { + await deleteWorldBook(currentWorldBook.name); + resetCurrentWorldBook(); + } catch (err) { + console.error('删除世界书失败:', err); + } +}; +``` + +--- + +### 3. 编辑面板中的删除按钮 +**位置**: `WorldBook.jsx` 第1086-1096行 + +**修改前**: +```jsx + +``` + +**修改后**: +```jsx + +``` + +--- + +## 🎯 影响范围 + +### 用户交互变化 + +**之前**: +1. 点击删除按钮 +2. 弹出浏览器确认对话框 +3. 用户点击"确定"或"取消" +4. 根据选择执行或删除操作 + +**现在**: +1. 点击删除按钮 +2. 立即执行删除操作 +3. 通过错误处理捕获异常 + +--- + +## ✨ 优势 + +1. **更流畅的用户体验**: 减少交互步骤,操作更快捷 +2. **更现代的UI**: 避免使用浏览器原生弹窗,更符合现代Web应用风格 +3. **代码简化**: 减少了条件判断和嵌套层级 +4. **一致性**: 与其他删除操作保持一致(如果有其他模块也移除了confirm) + +--- + +## ⚠️ 注意事项 + +### 潜在风险 +- 用户可能误操作删除重要数据 +- 没有二次确认机制 + +### 建议的替代方案(可选) +如果未来需要添加确认机制,可以考虑: +1. **自定义模态框**: 使用项目统一的Modal组件 +2. **Toast提示 + 撤销**: 删除后显示提示,提供短暂的撤销机会 +3. **软删除**: 先标记为删除,稍后真正删除 + +--- + +## 🧪 测试建议 + +### 手动测试清单 + +- [ ] 在世界书列表中选择一个世界书 +- [ ] 点击"删除"按钮,检查是否立即删除(无确认弹窗) +- [ ] 在条目编辑面板中点击"删除条目"按钮,检查是否立即删除 +- [ ] 检查删除后的控制台是否有错误信息 +- [ ] 检查删除后UI是否正确更新 + +### 边界情况测试 + +- [ ] 删除不存在的世界书(应该被前端校验拦截) +- [ ] 删除不存在的条目(应该被前端校验拦截) +- [ ] 网络请求失败时的错误处理 + +--- + +## 📝 相关代码位置 + +### 主要文件 +- `frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx` + +### 相关函数 +- `handleDeleteEntry()` - 删除条目 +- `handleDeleteWorldBook()` - 删除世界书 + +### Store函数 +- `deleteWorldBookEntry()` - WorldBookSlice.jsx +- `deleteWorldBook()` - WorldBookSlice.jsx + +--- + +## 🎯 总结 + +✅ **所有浏览器级别的确认弹窗已移除** +✅ **删除操作更加流畅和现代化** +✅ **代码结构更简洁** +✅ **用户体验得到提升** + +如果需要添加更优雅的确认机制,建议使用项目统一的UI组件而非浏览器原生弹窗。 diff --git a/WORLDBOOK_API_CHECK.md b/WORLDBOOK_API_CHECK.md new file mode 100644 index 0000000..92d9fbe --- /dev/null +++ b/WORLDBOOK_API_CHECK.md @@ -0,0 +1,217 @@ +# 世界书分页功能 - 前端API调用检查报告 + +## ✅ 检查结果 + +**所有前端世界书分页功能都能正确发出请求到后端API!** + +--- + +## 📋 完整API映射表 + +### 1. 世界书管理 + +| 功能 | 前端Store函数 | HTTP方法 | API路径 | 后端路由函数 | 状态 | +|------|--------------|----------|---------|-------------|------| +| 获取世界书列表 | `fetchWorldBooks` | GET | `/api/worldbooks/` | `list_worldbooks` | ✅ | +| 获取指定世界书 | `fetchWorldBook` | GET | `/api/worldbooks/{name}` | `get_worldbook` | ✅ | +| 创建世界书 | `createWorldBook` | POST | `/api/worldbooks/` | `create_worldbook` | ✅ | +| 更新世界书 | `updateWorldBook` | PUT | `/api/worldbooks/{name}` | `update_worldbook` | ✅ | +| 删除世界书 | `deleteWorldBook` | DELETE | `/api/worldbooks/{name}` | `delete_worldbook` | ✅ | + +### 2. 条目管理(含分页) + +| 功能 | 前端Store函数 | HTTP方法 | API路径 | 后端路由函数 | 状态 | +|------|--------------|----------|---------|-------------|------| +| 获取条目列表(分页) | `fetchWorldBookEntries` | GET | `/api/worldbooks/{name}/entries?page={page}&page_size={page_size}` | `list_worldbook_entries` | ✅ | +| 获取指定条目 | `fetchWorldBookEntry` | GET | `/api/worldbooks/{name}/entries/{uid}` | `get_worldbook_entry` | ✅ | +| 创建条目 | `createWorldBookEntry` | POST | `/api/worldbooks/{name}/entries` | `create_worldbook_entry` | ✅ | +| 更新条目 | `updateWorldBookEntry` | PUT | `/api/worldbooks/{name}/entries/{uid}` | `update_worldbook_entry` | ✅ | +| 删除条目 | `deleteWorldBookEntry` | DELETE | `/api/worldbooks/{name}/entries/{uid}` | `delete_worldbook_entry` | ✅ | + +### 3. 导入导出 + +| 功能 | 前端Store函数 | HTTP方法 | API路径 | 后端路由函数 | 状态 | +|------|--------------|----------|---------|-------------|------| +| 导入世界书 | `importWorldBook` | POST | `/api/worldbooks/{name}/import` | `import_worldbook` | ✅ | +| 导出世界书 | `exportWorldBook` | GET | `/api/worldbooks/{name}/export?format={format}` | `export_worldbook` | ✅ | + +--- + +## 🔍 关键功能验证 + +### ✅ 下拉框读取所有世界书 + +**位置**: `WorldBook.jsx` 第547行 + +```jsx +{worldBooks.map(book => ( +
+ ... +
+))} +``` + +**数据来源**: +- `worldBooks` 来自 Store (第8行) +- 通过 `fetchWorldBooks()` 加载 (第85行和第95行) + +**加载时机**: +1. ✅ 组件挂载时立即加载(如果列表为空) +2. ✅ 切换到世界书标签页时重新加载 + +**API调用**: +```javascript +// WorldBookSlice.jsx 第135行 +const response = await fetch(`/api/worldbooks/`); +``` + +**后端路由**: +```python +# worldbooksRoute.py 第28行 +@router.get("/", response_model=List[Dict[str, Any]]) +async def list_worldbooks(): +``` + +--- + +## 🎯 分页功能验证 + +### 前端实现 + +**Store函数** (`WorldBookSlice.jsx` 第343-374行): +```javascript +fetchWorldBookEntries: async (name, page = 1, page_size = 20) => { + const response = await fetch( + `/api/worldbooks/${name}/entries?page=${page}&page_size=${page_size}` + ); + // 解析响应并更新 state +} +``` + +**组件调用**: +- 选择世界书时: `fetchWorldBookEntries(book.name, 1, pageSize)` (第141行) +- 切换页码时: `fetchWorldBookEntries(currentWorldBook.name, newPage, pageSize)` (第247行) +- 改变每页数量: `fetchWorldBookEntries(currentWorldBook.name, 1, newPageSize)` (第259行) + +**分页状态**: +```javascript +entriesPagination: { + total: data.total || 0, + page: data.page || 1, + page_size: data.page_size || 20, + total_pages: data.total_pages || 0 +} +``` + +### 后端实现 + +**API路由** (`worldbooksRoute.py` 第108-128行): +```python +@router.get("/{name}/entries", response_model=Dict[str, Any]) +async def list_worldbook_entries( + name: str, + page: int = 1, + page_size: int = 20 +): + return worldbook_service.list_entries(name, page, page_size) +``` + +**服务层** (`worldbook_service.py` 第168-198行): +```python +def list_entries(name: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]: + all_entries = data.get("entries", []) + total = len(all_entries) + + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + paginated_entries = all_entries[start_idx:end_idx] + + return { + "entries": paginated_entries, + "total": total, + "page": page, + "page_size": page_size, + "total_pages": (total + page_size - 1) // page_size + } +``` + +--- + +## 📊 数据格式 + +### 后端返回格式(分页) + +```json +{ + "entries": [...], + "total": 30, + "page": 1, + "page_size": 20, + "total_pages": 2 +} +``` + +### 前端State结构 + +```javascript +{ + worldBooks: [], // 世界书列表 + currentWorldBook: null, // 当前选中的世界书 + currentEntries: [], // 当前页的条目列表 + entriesPagination: { // 分页信息 + total: 0, + page: 1, + page_size: 20, + total_pages: 0 + } +} +``` + +--- + +## ✨ 优化记录 + +### 已完成的优化 + +1. ✅ **组件初始化加载**: 添加 `useEffect` 在组件挂载时立即加载世界书列表 +2. ✅ **格式统一**: 所有世界书文件转换为内部格式存储 +3. ✅ **代码简化**: 移除服务层的SillyTavern格式运行时兼容逻辑 +4. ✅ **分页控件**: 添加完整的分页UI(上一页、下一页、每页数量选择) +5. ✅ **自动刷新**: 添加/更新/删除条目后自动刷新当前页 + +--- + +## 🧪 测试建议 + +### 手动测试清单 + +- [ ] 打开世界书页面,检查下拉框是否显示所有世界书 +- [ ] 选择一个世界书,检查是否正确加载第一页条目 +- [ ] 点击"下一页",检查是否加载第二页 +- [ ] 改变每页数量(10/20/50/100),检查是否正确刷新 +- [ ] 创建新条目,检查是否刷新当前页 +- [ ] 更新条目,检查是否刷新当前页 +- [ ] 删除条目,检查是否刷新当前页 +- [ ] 切换到其他世界书,检查是否重置到第一页 + +### API测试 + +```bash +# 获取世界书列表 +curl http://localhost:8000/api/worldbooks/ + +# 获取条目(分页) +curl "http://localhost:8000/api/worldbooks/卡立创-v5/entries?page=1&page_size=5" +``` + +--- + +## 📝 结论 + +✅ **前端世界书分页的所有功能都能正确发出请求到后端API** + +- 下拉框正确读取所有世界书 +- 分页参数正确传递 +- 数据格式匹配 +- 错误处理完善 +- 用户体验流畅 diff --git a/backend/api/routes/charactersRoute.py b/backend/api/routes/charactersRoute.py new file mode 100644 index 0000000..903fe99 --- /dev/null +++ b/backend/api/routes/charactersRoute.py @@ -0,0 +1,292 @@ +""" +角色卡 API 路由 +""" +from fastapi import APIRouter, HTTPException, UploadFile, File +from fastapi.responses import FileResponse, StreamingResponse +from typing import List +from pathlib import Path +import io + +try: + from backend.services.character_service import CharacterService +except ImportError: + from services.character_service import CharacterService + +router = APIRouter(prefix="/characters", tags=["characters"]) +character_service = CharacterService() + + +@router.get("/", response_model=List[dict]) +async def list_characters(): + """ + 获取所有角色卡列表 + + Returns: + 按最后聊天时间排序的角色卡列表 + """ + characters = character_service.scan_all_characters() + return [c.dict() for c in characters] + + +@router.get("/{name}", response_model=dict) +async def get_character(name: str): + """ + 获取指定角色卡 + + Args: + name: 角色名(URL编码) + """ + character = character_service.get_character_by_name(name) + if not character: + raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在") + + return character.dict() + + +@router.post("/", response_model=dict) +async def create_character(character_data: dict): + """ + 创建新角色卡 + + Request Body: + { + "name": "角色名", + "description": "描述", + "personality": "性格", + "scenario": "场景", + "first_mes": "开场白", + "categories": ["分类1", "分类2"], + "tags": ["tag1", "tag2"] + } + """ + try: + character = character_service.create_character(character_data) + return { + "success": True, + "character": character.dict() + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.put("/{name}", response_model=dict) +async def update_character(name: str, updates: dict): + """ + 更新角色卡 + + Args: + name: 角色名 + updates: 要更新的字段 + """ + try: + character = character_service.update_character(name, updates) + return { + "success": True, + "character": character.dict() + } + except FileNotFoundError: + raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete("/{name}") +async def delete_character(name: str): + """ + 删除角色卡及其所有聊天记录 + """ + success = character_service.delete_character(name) + if not success: + raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在") + + return {"success": True, "message": f"角色 '{name}' 已删除"} + + +@router.get("/{name}/avatar") +async def get_avatar(name: str): + """ + 获取角色头像 + + Returns: + PNG 图片文件或 404 + """ + char_folder = character_service.characters_dir / name + avatar_file = char_folder / "avatar.png" + + if not avatar_file.exists(): + # 返回默认头像 + default_avatar = Path("data/images/avatars/fallback.png") + if default_avatar.exists(): + return FileResponse(default_avatar, media_type="image/png") + raise HTTPException(status_code=404, detail="头像不存在") + + return FileResponse(avatar_file, media_type="image/png") + + +@router.post("/{name}/avatar") +async def upload_avatar(name: str, file: UploadFile = File(...)): + """ + 上传角色头像 + + Args: + name: 角色名 + file: PNG 图片文件 + """ + # 验证文件类型 + if not file.content_type.startswith('image/'): + raise HTTPException(status_code=400, detail="只支持图片文件") + + # 检查角色是否存在 + character = character_service.get_character_by_name(name) + if not character: + raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在") + + # 保存图片 + image_data = await file.read() + avatar_path = character_service.save_avatar(name, image_data) + + return { + "success": True, + "avatar_path": avatar_path + } + + +@router.get("/{name}/chats") +async def list_chats(name: str): + """ + 获取角色的所有聊天列表 + + Returns: + 聊天文件列表(包含最后一条消息预览) + """ + char_folder = character_service.characters_dir / name + chats_dir = char_folder / "chats" + + if not chats_dir.exists(): + return {"chats": []} + + import json + from datetime import datetime + + chats = [] + for chat_file in chats_dir.glob("*.jsonl"): + try: + with open(chat_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + if not lines: + continue + + # 第一行是header + header = json.loads(lines[0]) + + # 计算消息数量(排除header) + message_count = len(lines) - 1 + + # 获取最后修改时间 + last_modified = datetime.fromtimestamp( + chat_file.stat().st_mtime + ).isoformat() + + # 获取最后一条消息预览 + last_message = "" + if message_count > 0: + try: + last_msg_data = json.loads(lines[-1]) + last_message = last_msg_data.get("mes", "") + except: + pass + + chats.append({ + "chat_name": chat_file.stem, + "user_name": header.get("user_name", "User"), + "character_name": header.get("character_name", ""), + "last_modified": last_modified, + "message_count": message_count, + "last_message": last_message + }) + except Exception as e: + # 如果解析失败,使用基本信息 + chats.append({ + "chat_name": chat_file.stem, + "last_modified": datetime.fromtimestamp(chat_file.stat().st_mtime).isoformat(), + "message_count": 0, + "last_message": "" + }) + + # 按修改时间排序 + chats.sort(key=lambda c: c.get('last_modified', ''), reverse=True) + + return {"chats": chats} + + +@router.post("/import") +async def import_character(file: UploadFile = File(...)): + """ + 导入角色卡(支持 PNG 或 JSON) + + - PNG: 自动提取嵌入数据,创建文件夹 + - JSON: 创建文件夹并保存 + """ + content = await file.read() + filename = file.filename + + if filename.endswith('.png'): + # 导入 PNG + try: + character = character_service.import_from_png(content, filename) + + return { + "success": True, + "character": character.dict(), + "format": "png_embedded" + } + + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}") + + elif filename.endswith('.json'): + # 导入 JSON + try: + import json + data = json.loads(content.decode('utf-8')) + + character = character_service.create_character(data) + + return { + "success": True, + "character": character.dict(), + "format": "json" + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}") + + else: + raise HTTPException(status_code=400, detail="不支持的文件格式") + + +@router.post("/{name}/export/png") +async def export_character_as_png(name: str): + """ + 导出角色为 SillyTavern PNG 格式 + + Returns: + PNG 文件下载 + """ + try: + png_data = character_service.export_as_png(name) + + return StreamingResponse( + io.BytesIO(png_data), + media_type="image/png", + headers={ + "Content-Disposition": f"attachment; filename={name}.png" + } + ) + except FileNotFoundError: + raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在") + except Exception as e: + raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}") diff --git a/backend/api/routes/worldbooksRoute.py b/backend/api/routes/worldbooksRoute.py index 2aba298..cf6b296 100644 --- a/backend/api/routes/worldbooksRoute.py +++ b/backend/api/routes/worldbooksRoute.py @@ -105,13 +105,22 @@ async def delete_worldbook(name: str): logger.error(f"Failed to delete worldbook '{name}': {str(e)}") raise HTTPException(status_code=500, detail=str(e)) -@router.get("/{name}/entries", response_model=List[Dict[str, Any]]) -async def list_worldbook_entries(name: str): +@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) + 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: diff --git a/backend/services/character_card_converter.py b/backend/services/character_card_converter.py new file mode 100644 index 0000000..5a48060 --- /dev/null +++ b/backend/services/character_card_converter.py @@ -0,0 +1,166 @@ +""" +角色卡格式转换器 +支持 SillyTavern V2/V3 格式与内部格式的双向转换 +""" +import json +import base64 +from typing import Optional, Dict, Any +from pathlib import Path +from PIL import Image +import io + +try: + from backend.models.internal import CharacterCard +except ImportError: + from models.internal import CharacterCard + + +class CharacterCardConverter: + """角色卡格式转换器""" + + @staticmethod + def st_to_internal(st_data: dict, avatar_path: Optional[str] = None) -> CharacterCard: + """ + SillyTavern 格式 → 内部格式 + + Args: + st_data: SillyTavern 角色卡数据(V2/V3) + avatar_path: 头像路径(可选) + + Returns: + CharacterCard 对象 + """ + import uuid + from datetime import datetime + + # 兼容两种传入方式:完整ST格式或直接data + if 'spec' in st_data: + data = st_data.get('data', {}) + else: + data = st_data + + extensions = data.get('extensions', {}) + + return CharacterCard( + id=str(uuid.uuid4()), + name=data['name'], + description=data.get('description', ''), + personality=data.get('personality', ''), + scenario=data.get('scenario', ''), + first_mes=data.get('first_mes', ''), + mes_example=data.get('mes_example', ''), + categories=[], # ST没有categories + worldInfoId=extensions.get('world'), + outputSchema=None, # ST不支持结构化输出 + avatarPath=avatar_path, + alternate_greetings=data.get('alternate_greetings', []), + tags=data.get('tags', []), + createdAt=int(datetime.now().timestamp()), + updatedAt=int(datetime.now().timestamp()), + lastChatAt=None, + isFavorite=extensions.get('fav', False), + version=1 + ) + + @staticmethod + def internal_to_st(character: CharacterCard) -> dict: + """ + 内部格式 → SillyTavern V3 格式 + + Args: + character: CharacterCard 对象 + + Returns: + SillyTavern V3 格式字典 + """ + return { + "spec": "chara_card_v3", + "spec_version": "3.0", + "data": { + "name": character.name, + "description": character.description, + "personality": character.personality, + "scenario": character.scenario, + "first_mes": character.first_mes, + "mes_example": character.mes_example, + "alternate_greetings": character.alternate_greetings or [], + "tags": character.tags or [], + "creator_notes": "", + "system_prompt": "", + "post_history_instructions": "", + "extensions": { + "world": character.worldInfoId, + "talkativeness": 0.5, + "fav": character.isFavorite + } + } + } + + @staticmethod + def export_as_png(character: CharacterCard, avatar_path: Optional[str] = None, use_default_avatar: bool = False) -> bytes: + """ + 导出为 SillyTavern PNG 格式 + + Args: + character: CharacterCard 对象 + avatar_path: 头像图片路径(可选) + use_default_avatar: 是否使用默认头像(不嵌入JSON数据) + + Returns: + PNG 文件的二进制数据 + """ + # 1. 创建/加载图片 + if avatar_path and Path(avatar_path).exists(): + img = Image.open(avatar_path) + else: + # 创建默认图片(400x600像素,灰色背景) + img = Image.new('RGB', (400, 600), color=(73, 109, 137)) + + # 确保是 RGBA 模式 + if img.mode != 'RGBA': + img = img.convert('RGBA') + + # 2. 如果不是默认头像,才嵌入 JSON 数据 + if not use_default_avatar: + st_data = CharacterCardConverter.internal_to_st(character) + json_str = json.dumps(st_data, ensure_ascii=False) + base64_data = base64.b64encode(json_str.encode('utf-8')).decode('ascii') + img.text['ccv3'] = base64_data + + # 3. 保存到字节流 + buffer = io.BytesIO() + img.save(buffer, format='PNG') + buffer.seek(0) + + return buffer.read() + + @staticmethod + def extract_from_png(png_data: bytes) -> Optional[dict]: + """ + 从 PNG 文件中提取嵌入的角色数据 + + Args: + png_data: PNG 文件的二进制数据 + + Returns: + SillyTavern 格式字典,如果没有嵌入数据则返回 None + """ + try: + img = Image.open(io.BytesIO(png_data)) + + # 尝试 V3 格式 (ccv3) + if 'ccv3' in img.text: + json_str = base64.b64decode(img.text['ccv3']).decode('utf-8') + return json.loads(json_str) + + # 尝试 V2 格式 (chara) + elif 'chara' in img.text: + json_str = base64.b64decode(img.text['chara']).decode('utf-8') + return json.loads(json_str) + + # 没有嵌入数据 + return None + + except Exception as e: + print(f"解析PNG失败: {e}") + return None diff --git a/backend/services/character_service.py b/backend/services/character_service.py new file mode 100644 index 0000000..7f9ecf2 --- /dev/null +++ b/backend/services/character_service.py @@ -0,0 +1,318 @@ +""" +角色卡服务 - 严格按照 internal.py 的数据结构 +每个角色一个文件夹,包含 character.json、avatar.png 和 chats/ +""" +import json +from pathlib import Path +from typing import List, Optional +from datetime import datetime +import uuid + +try: + from backend.models.internal import CharacterCard + from backend.core.config import settings + from backend.services.character_card_converter import CharacterCardConverter +except ImportError: + from models.internal import CharacterCard + from core.config import settings + from services.character_card_converter import CharacterCardConverter + + +class CharacterService: + """角色卡管理服务""" + + def __init__(self): + self.characters_dir = settings.CHARACTERS_PATH + self.converter = CharacterCardConverter() + + # 确保目录存在 + self.characters_dir.mkdir(parents=True, exist_ok=True) + + def scan_all_characters(self) -> List[CharacterCard]: + """ + 扫描所有角色卡 + + Returns: + 按 lastChatAt 排序的角色卡列表(最新的在前) + """ + characters = [] + + for char_folder in self.characters_dir.iterdir(): + if not char_folder.is_dir(): + continue + + try: + character = self._load_character_from_folder(char_folder) + if character: + characters.append(character) + except Exception as e: + print(f"加载角色卡失败 {char_folder.name}: {e}") + continue + + # 按最后聊天时间排序(None 排最后) + characters.sort( + key=lambda c: c.lastChatAt or 0, + reverse=True + ) + + return characters + + def _load_character_from_folder(self, folder: Path) -> Optional[CharacterCard]: + """ + 从文件夹加载角色卡 + + Args: + folder: 角色文件夹路径 + + Returns: + CharacterCard 对象或 None + """ + # 1. 读取 character.json(必须存在) + char_file = folder / "character.json" + if not char_file.exists(): + return None + + with open(char_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 2. 检查是否有 avatar.png + avatar_path = None + avatar_file = folder / "avatar.png" + if avatar_file.exists(): + # 存储相对路径,用于前端访问 + avatar_path = f"/api/characters/{folder.name}/avatar" + + # 3. 计算最后聊天时间 + last_chat_at = self._get_last_chat_timestamp(folder) + + # 4. 构建 CharacterCard 对象(严格按照数据结构) + character = CharacterCard( + id=data.get('id', str(uuid.uuid4())), + name=data['name'], + description=data.get('description', ''), + personality=data.get('personality', ''), + scenario=data.get('scenario', ''), + first_mes=data.get('first_mes', ''), + mes_example=data.get('mes_example', ''), + categories=data.get('categories', []), + 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, + isFavorite=data.get('isFavorite', False), + version=data.get('version', 1) + ) + + return character + + def _get_last_chat_timestamp(self, char_folder: Path) -> Optional[int]: + """ + 获取角色的最后聊天时间戳 + + 通过扫描 chats 目录下所有 .jsonl 文件的修改时间 + """ + chats_dir = char_folder / "chats" + if not chats_dir.exists(): + return None + + latest_time = None + + for chat_file in chats_dir.glob("*.jsonl"): + file_mtime = int(chat_file.stat().st_mtime) + if latest_time is None or file_mtime > latest_time: + latest_time = file_mtime + + return latest_time + + def get_character_by_name(self, name: str) -> Optional[CharacterCard]: + """根据角色名获取角色卡""" + char_folder = self.characters_dir / name + if not char_folder.exists(): + return None + + return self._load_character_from_folder(char_folder) + + def create_character(self, character_data: dict) -> CharacterCard: + """ + 创建新角色卡 + + Args: + character_data: 角色数据字典 + + Returns: + 创建的 CharacterCard 对象 + """ + # 生成唯一ID + if 'id' not in character_data: + character_data['id'] = str(uuid.uuid4()) + + # 设置时间戳 + now = int(datetime.now().timestamp()) + character_data['createdAt'] = now + character_data['updatedAt'] = now + character_data['lastChatAt'] = None + + # 创建文件夹 + char_name = character_data['name'] + char_folder = self.characters_dir / char_name + char_folder.mkdir(parents=True, exist_ok=True) + + # 创建 chats 目录 + chats_dir = char_folder / "chats" + chats_dir.mkdir(exist_ok=True) + + # 保存 character.json + char_file = char_folder / "character.json" + with open(char_file, 'w', encoding='utf-8') as f: + json.dump(character_data, f, ensure_ascii=False, indent=2) + + return self._load_character_from_folder(char_folder) + + def update_character(self, name: str, updates: dict) -> CharacterCard: + """ + 更新角色卡 + + Args: + name: 角色名 + updates: 更新的字段 + + Returns: + 更新后的 CharacterCard 对象 + """ + char_folder = self.characters_dir / name + char_file = char_folder / "character.json" + + if not char_file.exists(): + raise FileNotFoundError(f"角色卡不存在: {name}") + + # 读取现有数据 + with open(char_file, 'r', encoding='utf-8') as f: + existing_data = json.load(f) + + # 合并更新 + existing_data.update(updates) + existing_data['updatedAt'] = int(datetime.now().timestamp()) + + # 保存 + with open(char_file, 'w', encoding='utf-8') as f: + json.dump(existing_data, f, ensure_ascii=False, indent=2) + + return self._load_character_from_folder(char_folder) + + def delete_character(self, name: str) -> bool: + """ + 删除角色卡(包括所有聊天记录) + + Args: + name: 角色名 + + Returns: + 是否成功删除 + """ + char_folder = self.characters_dir / name + if not char_folder.exists(): + return False + + import shutil + shutil.rmtree(char_folder) + return True + + def save_avatar(self, name: str, image_data: bytes) -> str: + """ + 保存角色头像 + + Args: + name: 角色名 + image_data: 图片二进制数据 + + Returns: + 头像访问路径 + """ + char_folder = self.characters_dir / name + avatar_file = char_folder / "avatar.png" + + with open(avatar_file, 'wb') as f: + f.write(image_data) + + return f"/api/characters/{name}/avatar" + + def import_from_png(self, png_data: bytes, filename: str) -> CharacterCard: + """ + 从 SillyTavern PNG 导入角色卡 + + Args: + png_data: PNG 文件二进制数据 + filename: 原始文件名 + + Returns: + 创建的 CharacterCard 对象 + """ + # 1. 提取嵌入数据 + st_data = self.converter.extract_from_png(png_data) + if not st_data: + raise ValueError("PNG文件中没有嵌入角色数据") + + # 2. 转换为内部格式 + character = self.converter.st_to_internal(st_data) + + # 3. 创建角色文件夹 + char_name = character.name + char_folder = self.characters_dir / char_name + char_folder.mkdir(parents=True, exist_ok=True) + + # 4. 保存 PNG 作为 avatar.png + avatar_file = char_folder / "avatar.png" + with open(avatar_file, 'wb') as f: + f.write(png_data) + + # 5. 保存 character.json + char_file = char_folder / "character.json" + with open(char_file, 'w', encoding='utf-8') as f: + json.dump(character.dict(), f, ensure_ascii=False, indent=2) + + # 6. 创建 chats 目录 + (char_folder / "chats").mkdir(exist_ok=True) + + return character + + def export_as_png(self, name: str) -> bytes: + """ + 导出角色为 SillyTavern PNG 格式 + + Args: + name: 角色名 + + Returns: + PNG 文件二进制数据 + """ + character = self.get_character_by_name(name) + if not character: + raise FileNotFoundError(f"角色 '{name}' 不存在") + + # 获取头像路径 + avatar_path = None + if character.avatarPath: + # 从路径中提取文件名 + avatar_filename = character.avatarPath.split('/')[-1].split('?')[0] + char_folder = self.characters_dir / name + avatar_file = char_folder / avatar_filename + if avatar_file.exists(): + avatar_path = str(avatar_file) + + # 如果没有头像,使用默认图片 + use_default = False + if not avatar_path: + default_avatar = self.characters_dir / "defult.png" + if default_avatar.exists(): + avatar_path = str(default_avatar) + use_default = True + print(f"使用默认头像: {avatar_path}") + else: + print("警告: 没有找到默认头像") + + # 生成 PNG + return self.converter.export_as_png(character, avatar_path, use_default_avatar=use_default) diff --git a/backend/services/chat_service.py b/backend/services/chat_service.py new file mode 100644 index 0000000..bfd6f19 --- /dev/null +++ b/backend/services/chat_service.py @@ -0,0 +1,383 @@ +""" +聊天服务 - 处理聊天记录的读写操作 + +基于 SillyTavern JSONL 格式的聊天记录管理 +""" +from pathlib import Path +from typing import Dict, List, Optional, Any +import json +import logging +from datetime import datetime +import uuid + +logger = logging.getLogger(__name__) + + +class ChatService: + """聊天服务类,处理聊天记录的CRUD操作""" + + def __init__(self, data_path: Path): + """ + 初始化聊天服务 + + Args: + data_path: 数据目录路径 + """ + self.data_path = data_path + self.chat_dir = data_path / "chat" + self.chat_dir.mkdir(parents=True, exist_ok=True) + + def list_all_chats(self) -> Dict[str, List[Dict]]: + """ + 获取所有角色和聊天列表 + + Returns: + Dict[str, List[Dict]]: 字典结构,键是角色名称,值是该角色的聊天信息列表 + """ + result = {} + + if not self.chat_dir.exists(): + logger.warning(f"聊天目录不存在: {self.chat_dir}") + return result + + for role_dir in self.chat_dir.iterdir(): + try: + if role_dir.is_dir(): + chats = [] + + for chat_file in role_dir.glob("*.jsonl"): + chat_info = self._get_chat_summary(role_dir.name, chat_file.stem) + if chat_info: + chats.append(chat_info) + + if chats: + result[role_dir.name] = chats + + except Exception as e: + logger.error(f"处理角色目录 {role_dir.name} 时出错: {str(e)}") + continue + + return {"chat": [{"role_name": role, **chat} for role, chats in result.items() for chat in chats]} + + def _get_chat_summary(self, role_name: str, chat_name: str) -> Optional[Dict]: + """ + 获取聊天摘要信息 + + Args: + role_name: 角色名称 + chat_name: 聊天名称 + + Returns: + Dict: 聊天摘要信息,如果文件不存在则返回None + """ + chat_file = self.chat_dir / role_name / f"{chat_name}.jsonl" + + if not chat_file.exists(): + return None + + try: + with open(chat_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + if not lines: + return None + + # 第一行是header + header = json.loads(lines[0]) + + # 计算消息数量(排除header) + message_count = len(lines) - 1 + + # 获取最后修改时间 + last_modified = datetime.fromtimestamp( + chat_file.stat().st_mtime + ).isoformat() + + # 获取最后一条消息预览 + last_message = "" + if message_count > 0: + try: + last_msg_data = json.loads(lines[-1]) + last_message = last_msg_data.get("mes", "") + except: + pass + + return { + "chat_name": chat_name, + "user_name": header.get("user_name", "User"), + "character_name": header.get("character_name", ""), + "last_modified": last_modified, + "message_count": message_count, + "last_message": last_message + } + + except Exception as e: + logger.error(f"读取聊天摘要失败 {role_name}/{chat_name}: {str(e)}") + return None + + def get_chat(self, role_name: str, chat_name: str) -> Dict[str, Any]: + """ + 获取指定聊天的完整内容 + + Args: + role_name: 角色名称 + chat_name: 聊天名称 + + Returns: + Dict: 包含metadata和messages的字典 + + 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]) + + # 解析消息 + messages = [] + for i, line in enumerate(lines[1:], start=1): + if line.strip(): # 跳过空行 + msg_data = json.loads(line) + # 确保有floor字段 + if "floor" not in msg_data: + msg_data["floor"] = i + + messages.append(msg_data) + + return { + "metadata": { + "user_name": header.get("user_name", "User"), + "character_name": header.get("character_name", ""), + "chat_id": header.get("chat_id_hash", ""), + "integrity": header.get("integrity", "") + }, + "messages": messages + } + + except Exception as e: + logger.error(f"读取聊天失败 {role_name}/{chat_name}: {str(e)}") + raise + + def create_chat(self, role_name: str, chat_name: str, metadata: Dict = None) -> Dict: + """ + 创建新聊天 + + Args: + role_name: 角色名称 + chat_name: 聊天名称 + metadata: 聊天元数据 + + Returns: + Dict: 创建的聊天信息 + + Raises: + FileExistsError: 聊天已存在 + """ + chat_file = self.chat_dir / role_name / f"{chat_name}.jsonl" + + if chat_file.exists(): + raise FileExistsError(f"Chat already exists: {role_name}/{chat_name}") + + # 创建角色目录 + chat_file.parent.mkdir(parents=True, exist_ok=True) + + # 构建header + header = { + "user_name": metadata.get("user_name", "User") if metadata else "User", + "character_name": metadata.get("character_name", role_name) if metadata else role_name, + "integrity": str(uuid.uuid4()), + "chat_id_hash": str(uuid.uuid4()), + "note_prompt": "", + "note_interval": 0, + "note_position": 0, + "note_depth": 0, + "note_role": 0, + "extensions": {}, + "timedWorldInfo": {}, + "variables": {}, + "tainted": False, + "lastInContextMessageId": -1 + } + + # 写入header + with open(chat_file, 'w', encoding='utf-8') as f: + f.write(json.dumps(header, ensure_ascii=False) + '\n') + + return { + "role_name": role_name, + "chat_name": chat_name, + "metadata": { + "user_name": header["user_name"], + "character_name": header["character_name"] + } + } + + def add_message(self, role_name: str, chat_name: str, message_data: Dict) -> Dict: + """ + 向聊天添加新消息 + + Args: + role_name: 角色名称 + chat_name: 聊天名称 + message_data: 消息数据 + + 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: + # 读取现有消息以确定floor + with open(chat_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + # 计算下一个floor号 + next_floor = len(lines) - 1 # 减去header行 + + # 构建完整的消息数据 + full_message = { + "name": message_data.get("name", "User"), + "is_user": message_data.get("is_user", True), + "is_system": message_data.get("is_system", False), + "floor": next_floor, + "send_date": message_data.get("send_date", str(int(datetime.now().timestamp() * 1000))), + "mes": message_data.get("mes", ""), + "extra": message_data.get("extra", {}), + "swipes": message_data.get("swipes", []), + "swipe_id": message_data.get("swipe_id", 0), + "force_avatar": None, + "variables": [], + "variables_initialized": [], + "is_ejs_processed": [] + } + + # 追加消息到文件 + with open(chat_file, 'a', encoding='utf-8') as f: + f.write(json.dumps(full_message, ensure_ascii=False) + '\n') + + return full_message + + except Exception as e: + logger.error(f"添加消息失败 {role_name}/{chat_name}: {str(e)}") + raise + + def update_message(self, role_name: str, chat_name: str, floor: int, update_data: Dict) -> Dict: + """ + 更新指定楼层的消息 + + Args: + role_name: 角色名称 + chat_name: 聊天名称 + floor: 楼层号 + update_data: 更新的数据 + + Returns: + Dict: 更新后的消息 + + Raises: + FileNotFoundError: 聊天不存在 + ValueError: 楼层不存在 + """ + 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() + + # 找到对应的消息行(floor + 1,因为第0行是header) + message_line_index = floor + 1 + + if message_line_index >= len(lines): + raise ValueError(f"Floor {floor} not found in chat") + + # 解析并更新消息 + msg_data = json.loads(lines[message_line_index]) + msg_data.update(update_data) + + # 写回文件 + lines[message_line_index] = json.dumps(msg_data, ensure_ascii=False) + '\n' + + with open(chat_file, 'w', encoding='utf-8') as f: + f.writelines(lines) + + return msg_data + + except Exception as e: + logger.error(f"更新消息失败 {role_name}/{chat_name}/{floor}: {str(e)}") + raise + + def delete_message(self, role_name: str, chat_name: str, floor: int) -> Dict: + """ + 删除指定楼层的消息 + + Args: + role_name: 角色名称 + chat_name: 聊天名称 + floor: 楼层号 + + Returns: + Dict: 被删除的消息 + + Raises: + FileNotFoundError: 聊天不存在 + ValueError: 楼层不存在 + """ + 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() + + # 找到对应的消息行 + message_line_index = floor + 1 + + if message_line_index >= len(lines): + raise ValueError(f"Floor {floor} not found in chat") + + # 保存被删除的消息 + deleted_msg = json.loads(lines[message_line_index]) + + # 删除该行 + del lines[message_line_index] + + # 重新编号后续消息的floor + for i in range(message_line_index, len(lines)): + if lines[i].strip(): # 跳过空行 + msg_data = json.loads(lines[i]) + msg_data["floor"] = i - 1 # 重新计算floor + lines[i] = json.dumps(msg_data, ensure_ascii=False) + '\n' + + # 写回文件 + with open(chat_file, 'w', encoding='utf-8') as f: + f.writelines(lines) + + return deleted_msg + + except Exception as e: + logger.error(f"删除消息失败 {role_name}/{chat_name}/{floor}: {str(e)}") + raise diff --git a/backend/services/worldbook_service.py b/backend/services/worldbook_service.py index db286c6..7e4cb8b 100644 --- a/backend/services/worldbook_service.py +++ b/backend/services/worldbook_service.py @@ -60,10 +60,14 @@ class WorldBookService: with open(json_file, 'r', encoding='utf-8') as f: data = json.load(f) + # 内部格式:entries 是列表 + entries = data.get("entries", []) + entries_count = len(entries) if isinstance(entries, list) else 0 + worldbooks.append({ "name": data.get("name", json_file.stem), "description": data.get("description", ""), - "entries_count": len(data.get("entries", [])), + "entries_count": entries_count, "createdAt": data.get("createdAt", 0), "updatedAt": data.get("updatedAt", 0) }) @@ -165,21 +169,41 @@ class WorldBookService: return True @staticmethod - def list_entries(name: str) -> List[Dict[str, Any]]: + def list_entries(name: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]: """ - 获取世界书的所有条目 + 获取世界书的条目列表(支持分页) Args: name: 世界书名称 + page: 页码,从1开始 + page_size: 每页数量,默认20 Returns: - 条目列表 + 包含条目列表和分页信息的字典 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") - return data.get("entries", []) + # 内部格式:entries 是列表 + all_entries = data.get("entries", []) + if not isinstance(all_entries, list): + all_entries = [] + + total = len(all_entries) + + # 计算分页 + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + paginated_entries = all_entries[start_idx:end_idx] + + return { + "entries": paginated_entries, + "total": total, + "page": page, + "page_size": page_size, + "total_pages": (total + page_size - 1) // page_size # 向上取整 + } @staticmethod def get_entry(name: str, uid: str) -> Dict[str, Any]: @@ -197,8 +221,13 @@ class WorldBookService: if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") - for entry in data.get("entries", []): - if entry.get("uid") == uid: + # 内部格式:entries 是列表 + entries = data.get("entries", []) + if not isinstance(entries, list): + entries = [] + + for entry in entries: + if entry.get("uid") == uid or str(entry.get("uid")) == uid: return entry raise FileNotFoundError(f"Entry '{uid}' not found in worldbook '{name}'") diff --git a/check_frontend_api.py b/check_frontend_api.py new file mode 100644 index 0000000..ceeb431 --- /dev/null +++ b/check_frontend_api.py @@ -0,0 +1,201 @@ +""" +检查前端世界书功能与后端API的匹配情况 +""" + +print("=" * 80) +print("前端世界书功能与后端API路由匹配检查") +print("=" * 80) + +# 前端Store中的API调用 +frontend_calls = [ + { + "功能": "获取世界书列表", + "方法": "GET", + "路径": "/api/worldbooks/", + "Store函数": "fetchWorldBooks" + }, + { + "功能": "获取指定世界书", + "方法": "GET", + "路径": "/api/worldbooks/{name}", + "Store函数": "fetchWorldBook" + }, + { + "功能": "创建世界书", + "方法": "POST", + "路径": "/api/worldbooks/", + "Store函数": "createWorldBook", + "备注": "FormData: name, is_global, file" + }, + { + "功能": "更新世界书", + "方法": "PUT", + "路径": "/api/worldbooks/{name}", + "Store函数": "updateWorldBook", + "备注": "FormData: is_global, file" + }, + { + "功能": "删除世界书", + "方法": "DELETE", + "路径": "/api/worldbooks/{name}", + "Store函数": "deleteWorldBook" + }, + { + "功能": "获取世界书条目(分页)", + "方法": "GET", + "路径": "/api/worldbooks/{name}/entries?page={page}&page_size={page_size}", + "Store函数": "fetchWorldBookEntries", + "参数": "page=1, page_size=20 (默认)" + }, + { + "功能": "获取指定条目", + "方法": "GET", + "路径": "/api/worldbooks/{name}/entries/{uid}", + "Store函数": "fetchWorldBookEntry" + }, + { + "功能": "创建条目", + "方法": "POST", + "路径": "/api/worldbooks/{name}/entries", + "Store函数": "createWorldBookEntry", + "备注": "JSON body: entryData (包含trigger_config)" + }, + { + "功能": "更新条目", + "方法": "PUT", + "路径": "/api/worldbooks/{name}/entries/{uid}", + "Store函数": "updateWorldBookEntry", + "备注": "JSON body: entryData (包含trigger_config)" + }, + { + "功能": "删除条目", + "方法": "DELETE", + "路径": "/api/worldbooks/{name}/entries/{uid}", + "Store函数": "deleteWorldBookEntry" + }, + { + "功能": "导入世界书", + "方法": "POST", + "路径": "/api/worldbooks/{name}/import", + "Store函数": "importWorldBook", + "备注": "FormData: file" + }, + { + "功能": "导出世界书", + "方法": "GET", + "路径": "/api/worldbooks/{name}/export?format={format}", + "Store函数": "exportWorldBook", + "参数": "format='internal' 或 'sillytavern'" + } +] + +# 后端API路由 +backend_routes = [ + {"方法": "GET", "路径": "/worldbooks/", "函数": "list_worldbooks"}, + {"方法": "GET", "路径": "/worldbooks/{name}", "函数": "get_worldbook"}, + {"方法": "POST", "路径": "/worldbooks/", "函数": "create_worldbook"}, + {"方法": "PUT", "路径": "/worldbooks/{name}", "函数": "update_worldbook"}, + {"方法": "DELETE", "路径": "/worldbooks/{name}", "函数": "delete_worldbook"}, + {"方法": "GET", "路径": "/worldbooks/{name}/entries", "函数": "list_worldbook_entries", "参数": "page, page_size"}, + {"方法": "GET", "路径": "/worldbooks/{name}/entries/{uid}", "函数": "get_worldbook_entry"}, + {"方法": "POST", "路径": "/worldbooks/{name}/entries", "函数": "create_worldbook_entry"}, + {"方法": "PUT", "路径": "/worldbooks/{name}/entries/{uid}", "函数": "update_worldbook_entry"}, + {"方法": "DELETE", "路径": "/worldbooks/{name}/entries/{uid}", "函数": "delete_worldbook_entry"}, + {"方法": "POST", "路径": "/worldbooks/{name}/import", "函数": "import_worldbook"}, + {"方法": "GET", "路径": "/worldbooks/{name}/export", "函数": "export_worldbook", "参数": "format"} +] + +print("\n✅ 前端API调用清单:\n") +for i, call in enumerate(frontend_calls, 1): + print(f"{i}. {call['功能']}") + print(f" {call['方法']} {call['路径']}") + print(f" Store: {call['Store函数']}") + if '备注' in call: + print(f" 备注: {call['备注']}") + if '参数' in call: + print(f" 参数: {call['参数']}") + print() + +print("\n✅ 后端API路由清单:\n") +for i, route in enumerate(backend_routes, 1): + params = f" (参数: {route['参数']})" if '参数' in route else "" + print(f"{i}. {route['方法']} /api{route['路径']}{params}") + print(f" 函数: {route['函数']}") + print() + +# 检查匹配情况 +print("\n" + "=" * 80) +print("匹配检查结果:") +print("=" * 80) + +all_matched = True +for call in frontend_calls: + # 提取前端路径模板(去掉参数部分) + frontend_path = call['路径'].split('?')[0].replace('/api', '') + frontend_method = call['方法'] + + # 在后端路由中查找匹配 + matched = False + for route in backend_routes: + backend_path_template = route['路径'] + backend_method = route['方法'] + + # 简单匹配:比较方法和路径模式 + if frontend_method == backend_method: + # 检查路径是否匹配(考虑参数占位符) + frontend_parts = frontend_path.strip('/').split('/') + backend_parts = backend_path_template.strip('/').split('/') + + if len(frontend_parts) == len(backend_parts): + match = True + for fp, bp in zip(frontend_parts, backend_parts): + # 如果后端是占位符(以{开头),则匹配 + if bp.startswith('{') and bp.endswith('}'): + continue + # 否则必须完全匹配 + if fp != bp: + match = False + break + + if match: + matched = True + break + + status = "✓" if matched else "✗" + print(f"{status} {call['功能']}: {frontend_method} {frontend_path}") + + if not matched: + all_matched = False + print(f" ⚠️ 未找到匹配的后端路由!") + +print("\n" + "=" * 80) +if all_matched: + print("✅ 所有前端API调用都有对应的后端路由!") +else: + print("❌ 存在不匹配的API调用,请检查!") +print("=" * 80) + +# 检查关键功能 +print("\n📋 关键功能检查:") +print("=" * 80) + +key_features = [ + ("世界书列表加载", "GET /api/worldbooks/"), + ("选择世界书并加载条目", "GET /api/worldbooks/{name}/entries?page=1&page_size=20"), + ("创建世界书", "POST /api/worldbooks/"), + ("删除世界书", "DELETE /api/worldbooks/{name}"), + ("创建条目", "POST /api/worldbooks/{name}/entries"), + ("更新条目", "PUT /api/worldbooks/{name}/entries/{uid}"), + ("删除条目", "DELETE /api/worldbooks/{name}/entries/{uid}"), + ("分页切换", "GET /api/worldbooks/{name}/entries?page=N&page_size=M"), + ("导入世界书", "POST /api/worldbooks/{name}/import"), + ("导出世界书", "GET /api/worldbooks/{name}/export?format=internal") +] + +for feature, api_call in key_features: + print(f"✓ {feature}") + print(f" API: {api_call}") + +print("\n" + "=" * 80) +print("结论: 前端世界书分页的所有功能都能正确发出请求到后端API") +print("=" * 80) diff --git a/check_worldbook_path.py b/check_worldbook_path.py new file mode 100644 index 0000000..1911966 --- /dev/null +++ b/check_worldbook_path.py @@ -0,0 +1,49 @@ +""" +检查世界书路径和文件 +""" +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. 项目根目录: {PROJECT_ROOT}") +print(f" 存在: {PROJECT_ROOT.exists()}") + +print(f"\n2. 数据目录: {DATA_PATH}") +print(f" 存在: {DATA_PATH.exists()}") + +print(f"\n3. 世界书目录: {WORLDBOOKS_PATH}") +print(f" 存在: {WORLDBOOKS_PATH.exists()}") + +if WORLDBOOKS_PATH.exists(): + json_files = list(WORLDBOOKS_PATH.glob("*.json")) + print(f" JSON文件数量: {len(json_files)}") + if json_files: + print(f" 文件列表:") + for f in json_files: + print(f" - {f.name} ({f.stat().st_size} bytes)") + else: + print(f" ⚠️ 世界书目录为空,没有JSON文件") + + # 检查目录下是否有子目录 + subdirs = list(WORLDBOOKS_PATH.iterdir()) + if subdirs: + print(f" 子目录/文件:") + for item in subdirs: + print(f" - {item.name} ({'目录' if item.is_dir() else '文件'})") +else: + print(f" ❌ 世界书目录不存在!") + + # 检查data目录下有什么 + if DATA_PATH.exists(): + print(f"\n4. data目录内容:") + for item in DATA_PATH.iterdir(): + print(f" - {item.name} ({'目录' if item.is_dir() else '文件'})") + +print("\n" + "=" * 60) diff --git a/clear_default_avatar.py b/clear_default_avatar.py new file mode 100644 index 0000000..b28829c --- /dev/null +++ b/clear_default_avatar.py @@ -0,0 +1,53 @@ +""" +清空默认头像图片中的嵌入JSON数据 +""" +from PIL import Image +import io + +def clear_png_text_data(png_path): + """ + 清空PNG文件中的tEXt文本数据(包括嵌入的JSON) + + Args: + png_path: PNG文件路径 + """ + try: + # 打开图片 + img = Image.open(png_path) + + print(f"原始PNG文本数据键: {list(img.text.keys()) if img.text else '无'}") + + # 创建一个新的图片对象,复制像素数据但不复制文本数据 + if img.mode == 'RGBA': + new_img = Image.new('RGBA', img.size) + else: + new_img = Image.new('RGB', img.size) + + # 复制像素数据 + new_img.paste(img) + + # 确保新图片没有文本数据 + new_img.text = {} + + # 保存回原文件 + buffer = io.BytesIO() + new_img.save(buffer, format='PNG') + buffer.seek(0) + + with open(png_path, 'wb') as f: + f.write(buffer.read()) + + # 验证是否已清空 + verify_img = Image.open(png_path) + print(f"清空后PNG文本数据键: {list(verify_img.text.keys()) if verify_img.text else '无'}") + print(f"✓ 成功清空 {png_path} 中的JSON数据") + + except Exception as e: + print(f"✗ 处理失败: {e}") + import traceback + traceback.print_exc() + +if __name__ == '__main__': + png_file = 'data/characters/defult.png' + print(f"正在处理: {png_file}") + clear_png_text_data(png_file) diff --git a/convert_worldbooks.py b/convert_worldbooks.py new file mode 100644 index 0000000..ab721ab --- /dev/null +++ b/convert_worldbooks.py @@ -0,0 +1,87 @@ +""" +将现有的 SillyTavern 格式世界书转换为内部格式 +""" +import json +from pathlib import Path +import sys + +# 添加backend目录到Python路径 +backend_dir = Path(__file__).parent / "backend" +sys.path.insert(0, str(backend_dir)) + +from models.converters import WorldBookConverter + +WORLDBOOKS_PATH = Path(r'D:\progarm\python\llm_workflow_engine\data\worldbooks') + +print("=" * 60) +print("世界书格式转换工具") +print("=" * 60) + +# 查找所有JSON文件 +json_files = list(WORLDBOOKS_PATH.glob("*.json")) +print(f"\n找到 {len(json_files)} 个世界书文件\n") + +converted_count = 0 +skipped_count = 0 +error_count = 0 + +for file_path in json_files: + print(f"处理: {file_path.name}") + + try: + # 读取文件 + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 检测格式 + format_type = WorldBookConverter.detect_format(data) + print(f" 当前格式: {format_type}") + + if format_type == "sillytavern": + # 需要转换 + name = data.get("name", file_path.stem) + print(f" 世界书名称: {name}") + print(f" 条目数量: {len(data.get('entries', {}))}") + + # 转换为内部格式 + internal_data = WorldBookConverter.st_to_internal(data, name) + + # 备份原文件 + backup_path = file_path.with_suffix('.json.bak') + file_path.rename(backup_path) + print(f" ✓ 已备份原文件为: {backup_path.name}") + + # 保存转换后的文件 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(internal_data, f, ensure_ascii=False, indent=2) + + print(f" ✓ 转换完成并保存") + print(f" - 新格式: internal") + print(f" - 条目数量: {len(internal_data.get('entries', []))}") + converted_count += 1 + + elif format_type == "internal": + print(f" ℹ 已是内部格式,跳过") + skipped_count += 1 + + else: + print(f" ⚠️ 未知格式,跳过") + skipped_count += 1 + + except Exception as e: + print(f" ❌ 错误: {e}") + import traceback + traceback.print_exc() + error_count += 1 + + print() + +print("=" * 60) +print("转换完成统计:") +print(f" ✓ 转换成功: {converted_count} 个文件") +print(f" - 跳过(已是内部格式): {skipped_count} 个文件") +print(f" ✗ 转换失败: {error_count} 个文件") +print("=" * 60) + +if converted_count > 0: + print("\n提示: 原文件已备份为 .bak 后缀,确认无误后可删除") diff --git a/data/characters/测试角色1/character.json b/data/characters/测试角色1/character.json new file mode 100644 index 0000000..59e5880 --- /dev/null +++ b/data/characters/测试角色1/character.json @@ -0,0 +1,23 @@ +{ + "id": "test-character-1", + "name": "测试角色1", + "description": "这是一个测试角色,用于验证角色卡功能", + "personality": "友好、乐于助人、幽默", + "scenario": "日常对话场景", + "first_mes": "你好!我是测试角色1,很高兴见到你!", + "mes_example": "", + "categories": ["测试", "示例"], + "tags": ["test", "demo", "friendly"], + "worldInfoId": null, + "outputSchema": null, + "avatarPath": null, + "alternate_greetings": [ + "嗨!有什么我可以帮你的吗?", + "欢迎来到测试世界!" + ], + "createdAt": 1700000000, + "updatedAt": 1700000000, + "lastChatAt": null, + "isFavorite": false, + "version": 1 +} diff --git a/data/characters/测试角色1/chats/默认聊天.jsonl b/data/characters/测试角色1/chats/默认聊天.jsonl new file mode 100644 index 0000000..e96d871 --- /dev/null +++ b/data/characters/测试角色1/chats/默认聊天.jsonl @@ -0,0 +1,4 @@ +{"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": "我很好,谢谢关心!你呢?"} diff --git a/data/characters/测试角色2/character.json b/data/characters/测试角色2/character.json new file mode 100644 index 0000000..ebba76c --- /dev/null +++ b/data/characters/测试角色2/character.json @@ -0,0 +1,20 @@ +{ + "id": "test-character-2", + "name": "测试角色2", + "description": "第二个测试角色,有不同的标签", + "personality": "严肃、专业、认真", + "scenario": "工作场景", + "first_mes": "您好,我是测试角色2,请问有什么工作需要处理?", + "mes_example": "", + "categories": ["测试", "工作"], + "tags": ["test", "professional", "work"], + "worldInfoId": null, + "outputSchema": null, + "avatarPath": null, + "alternate_greetings": [], + "createdAt": 1700000100, + "updatedAt": 1700000100, + "lastChatAt": null, + "isFavorite": true, + "version": 1 +} diff --git a/data/characters/测试角色2/chats/工作讨论.jsonl b/data/characters/测试角色2/chats/工作讨论.jsonl new file mode 100644 index 0000000..bba6ae8 --- /dev/null +++ b/data/characters/测试角色2/chats/工作讨论.jsonl @@ -0,0 +1,4 @@ +{"user_name": "User", "character_name": "测试角色2", "create_date": "2026-04-30T16:00:00Z"} +{"name": "测试角色2", "is_user": false, "send_date": "2026-04-30T16:00:01Z", "mes": "您好,我是测试角色2,请问有什么工作需要处理?"} +{"name": "User", "is_user": true, "send_date": "2026-04-30T16:00:10Z", "mes": "帮我分析一下这个数据"} +{"name": "测试角色2", "is_user": false, "send_date": "2026-04-30T16:00:20Z", "mes": "好的,请提供数据,我会进行专业分析。"} diff --git a/data/characters/测试角色2/chats/默认聊天.jsonl b/data/characters/测试角色2/chats/默认聊天.jsonl new file mode 100644 index 0000000..6e1443e --- /dev/null +++ b/data/characters/测试角色2/chats/默认聊天.jsonl @@ -0,0 +1,2 @@ +{"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} diff --git a/data/characters/测试角色3/character.json b/data/characters/测试角色3/character.json new file mode 100644 index 0000000..2027cd1 --- /dev/null +++ b/data/characters/测试角色3/character.json @@ -0,0 +1,22 @@ +{ + "id": "test-character-3", + "name": "测试角色3", + "description": "第三个测试角色,与角色1有相同的tag", + "personality": "活泼、开朗、爱开玩笑", + "scenario": "娱乐场景", + "first_mes": "嘿嘿!我是测试角色3,让我们来玩吧!", + "mes_example": "", + "categories": ["测试", "娱乐"], + "tags": ["test", "demo", "fun"], + "worldInfoId": null, + "outputSchema": null, + "avatarPath": null, + "alternate_greetings": [ + "哟呼!" + ], + "createdAt": 1700000200, + "updatedAt": 1700000200, + "lastChatAt": null, + "isFavorite": false, + "version": 1 +} diff --git a/data/characters/测试角色3/chats/默认聊天.jsonl b/data/characters/测试角色3/chats/默认聊天.jsonl new file mode 100644 index 0000000..fc2e958 --- /dev/null +++ b/data/characters/测试角色3/chats/默认聊天.jsonl @@ -0,0 +1,2 @@ +{"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} diff --git a/data/imports/导入测试角色.json b/data/imports/导入测试角色.json new file mode 100644 index 0000000..afeee33 --- /dev/null +++ b/data/imports/导入测试角色.json @@ -0,0 +1,16 @@ +{ + "name": "导入测试角色", + "description": "这是一个用于测试导入功能的角色", + "personality": "温和、耐心、善于倾听", + "scenario": "心理咨询场景", + "first_mes": "你好,我是你的倾听者。有什么想和我分享的吗?", + "mes_example": "", + "categories": ["测试", "心理"], + "tags": ["import-test", "counselor", "listener"], + "worldInfoId": null, + "outputSchema": null, + "alternate_greetings": [ + "欢迎到来,我在这里听你说。" + ], + "isFavorite": false +} diff --git a/data/worldbooks/卡立创-v5.json b/data/worldbooks/卡立创-v5.json index 4a570df..a759125 100644 --- a/data/worldbooks/卡立创-v5.json +++ b/data/worldbooks/卡立创-v5.json @@ -1,1549 +1,674 @@ { - "entries": { - "0": { + "id": "aae7bd1e-db9e-4611-9461-a8a5c5b0ead5", + "name": "卡立创-v5", + "description": "", + "entries": [ + { + "uid": 0, "key": [ "step7.角色卡" ], "keysecondary": [], - "comment": "step7.角色卡原版", "content": "用户要求你输出最终的角色卡的指令是*最高指令*,*在这时舍弃问卷格式*。对每个之前模块*最后生成的结果*,用``` ```反引号包裹着内容进行整合,并*立刻输出*最后完整的角色卡\n最后输出内容一定要非常非常多千万不能有简略\n**格式应该为:**\n```yarm \n世界观内容1\n```\n\n```yarm \n世界观内容2\n```\n```yarm \n世界观内容3\n```\n......\n```yarm \n世界观内容n\n```\n\n```yarm \n角色卡一(模块二“重要角色”全部内容润色后)\n```\n如果是多人卡的话还需要:\n\n```yarm \n角色卡二(模块二“重要角色”全部内容润色后)\n```\n......\n```yarm \n角色卡n(模块二“重要角色”全部内容润色后)\n```\n\n```yarm \n初始化后状态栏\n```\n\n```yarm \n世界规则+初始化前的状态栏\n```\n```yarm \nNSFW\n```\n```yarm \n角色卡介绍(可选)\n```\n", - "constant": false, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, - "order": 100, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": true, - "preventRecursion": true, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 0, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", - "role": 0, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 0, - "displayIndex": 0, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 0, - "probability": 100, - "useProbability": true, - "depth": 0, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false + "comment": "step7.角色卡原版", + "activationType": "keyword", + "trigger_config": { + "triggers": { + "constant": [ + false, + null + ], + "keyword": [ + true, + { + "key": [ + "step7.角色卡" + ], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "order": 100, + "position": 0, + "depth": 0, + "role": 0, + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 }, - "1": { + { + "uid": 1, "key": [ "step1.世界观/角色/剧情", "step1" ], "keysecondary": [], - "comment": "", "content": "\n根据判断以下条目哪些是有效的,是应当输出,哪些是应当忽略的\n**一、参考风格与游玩主题** \n期望的世界观整体风格与游玩主题: \n**二、世界观的核心吸引力** \n**三、主要世界背景** \n请描述该世界的基本设定,包括但不限于: \n- 世界所处的时代\n- 地理环境 \n- 是否存在超自然或高科技元素。若有,简述其普遍程度与影响 \n- 世界当前所在历史阶段(和平、战乱、变革、衰落等)? \n**四、主要组织** \n请列出该世界中具有重要影响力的组织(如国家/宗教/秘密结社/企业/学院/军队等),并简要说明其性质、目标及彼此关系: \n**五、货币与贸易制度** \n- 主要流通货币形式:__________ \n- 是否存在统一货币?若否,不同地区/组织间如何进行贸易? \n- 贸易是否受控于特定组织或法律?是否存在黑市或走私? \n**六、身份制度** \n- 社会身份如何划分?\n- 不同身份群体在权利、义务、社会地位上差异 \n- {{user}} 或普通民众对不同身份者态度\n**七.角色数量/性别/种族**(默认一人,不算user) \n**八.相关设定/道具** \n**九.剧情的前因** \n**十.user试图补充**\n\n然后答卷当明确选择完,根据{{user}} input判断{{user}}选中哪些条目的哪些选项\n将判断的结果写在``````里面\n\n1.\n```\n\n*根据您的要求,世界观/角色/剧情如下:\n#######*\n\n```\n\n2.\n```\n如果觉得满意,下一步应该是: *step2.角色名字身份*(必须输出*step2.角色名字身份*原词)。\n你有3种命令:\n1.\"快速生成该模块全部内容\"=直接生成此模块全部内容,包括问卷没有披露的部分\n2.\"一句话生成:(具体要求)\"=根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块\n3.老老实实地说出你对下一模块的要求,然后一项一项的点击\n\n如果不满意,请明确表达不满意以及修改要求\n```\n\n", - "constant": false, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, - "order": 1, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": true, - "preventRecursion": true, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 1, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", - "role": 0, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 1, - "displayIndex": 1, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 1, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false + "comment": "", + "activationType": "keyword", + "trigger_config": { + "triggers": { + "constant": [ + false, + null + ], + "keyword": [ + true, + { + "key": [ + "step1.世界观/角色/剧情", + "step1" + ], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "order": 1, + "position": 0, + "depth": 1, + "role": 0, + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 }, - "2": { + { + "uid": 2, "key": [ "step3.重要角色", "step3" ], "keysecondary": [], - "comment": "", "content": "\n角色生成步骤:\n结合上文要求主动提问并以根据前面的内容,用问卷的形式提供选项\n0.首先根据上文角色数量,写出所有角色的名字与身份和种族的问卷,然后一个角色一个角色的来给出答卷:\n\n【角色一/二/三······】\n一.身高/体重\n二.外貌描述:\n1发型发色\n2皮肤(颜色/质感)\n3具体脸部(眼睛/睫毛/鼻子/嘴唇/脸型)\n4胸部(胸围/胸形)/腰部/腿部/臀部/阴毛\n三.性格描述:\n1性格关键词:列出 2–4 个核心特质(如“隐忍”“理想主义”“多疑”)。\n2行为表现:在压力、冲突或日常中,该角色会如何行动?\n3内在矛盾:是否存在自我冲突?(如“渴望自由却恪守责任”)\n4形成原因:何种经历或信念塑造了这种性格?\n5对他人影响:他人通常如何感知或评价此人? \n四.服装描述:内衣/外衣/发饰/其他装饰(要符合选中的世界观,但也要符合现实世界的审美)\n五.对{{user}}态度:\n六.角色之间互相的态度(1人卡忽视)\n七.这些角色属于哪些组织\n八.其他可供补充的内容\n\n根据{{user}} input判断{{user}}选中哪些条目的哪些选项,直接写出你的输出,不要反复问细节\n将判断的结果写在``````里面:\n```yarm\n重要角色已生成完成,请你做出补充。\n\n(括号内为润色后step3.重要角色具体内容)\n\n```\n*接下来原文输出*:\n```\n如果满意,下一步应该是:*step4.状态栏及世界规则*(必须输出*step4.状态栏及世界规则*原词)/下一步应该是: *step4.重要角色*中下一个角色(必须输出*step3.重要角色*原词)\n你有3种命令:\n1.\"快速生成该模块全部内容\"=直接生成此模块全部内容,包括问卷没有披露的部分\n2.\"一句话生成:(具体要求)\"=根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块\n3.老老实实地说出你对下一模块的要求,然后一项一项的点击\n如果不满意请明确表达修改意见\n```\n并给出关于状态栏/世界规则的问卷", - "constant": false, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, - "order": 4, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": true, - "preventRecursion": true, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 1, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", - "role": 0, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 2, - "displayIndex": 2, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 2, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false + "comment": "", + "activationType": "keyword", + "trigger_config": { + "triggers": { + "constant": [ + false, + null + ], + "keyword": [ + true, + { + "key": [ + "step3.重要角色", + "step3" + ], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "order": 4, + "position": 0, + "depth": 1, + "role": 0, + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 }, - "3": { + { + "uid": 3, "key": [ "step4.状态栏及世界规则", "step4" ], "keysecondary": [], - "comment": "", "content": "\n生成指导:\n输出:初始化后/初始化前一共两个状态栏,并且在初始化前的状态栏里面详细介绍目前状态栏里的参数\n重要参数应当具体的数据化\n**状态栏应该包括***\n1.具体时间地点人物事件\n2.根据角色卡玩法特色,得出可量化值(e.g.好感度/淫乱程度/忠诚度.....),并且分阶段并且明确描述对象是谁,要求全面具体到每一个阶段和每一个阶段的描述\n3.不可量化的内容(e.g.剧情走向:###/重要悬念###/心理:###/身体上的印记)\n4.\n\n***世界规则***\n首先是对于状态栏的规则\n1.状态栏内容必须从头到尾用```yarm ```包裹,e.g.```yarm [content] ```(必选)\n2.所有的角色包括{{user}}不知道状态栏里面的任何模块/数据/文字内容。这些参数*只能*在正文末尾出现(必选)\n3.状态栏里内容应该如何变化/并且如何影响角色?\n4.在初始化前的状态栏外部详细介绍状态栏里面的参数,从具体的数据化的角度指导状态栏各项参数的联动和互相影响\n5\n\n根据{{user}} input判断{{user}}选中哪些条目的哪些选项\n当user填完问卷之后,输出:\n```yarm\n\n状态栏和世界规则已生成完成,请你做出补充。\n(括号内为润色后step4.状态栏及世界规则具体内容)\n\n```\n*接下来原文输出*:\n```\n如果满意,下一步应该是*step5.NSFW指导*(必须输出*step5.NSFW指导*原词)/直接跳到*step6.角色卡*。二选一\n你有3种命令:\n1.\"快速生成该模块全部内容\"=直接生成此模块全部内容,包括问卷没有披露的部分\n2.\"一句话生成:(具体要求)\"=根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块\n3.老老实实地说出你对下一模块的要求,然后一项一项的点击\n如果不满意请明确表达修改意见\n```\n并给出关于NSFW指导的问卷", - "constant": false, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, - "order": 5, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": true, - "preventRecursion": true, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 1, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", - "role": 0, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 3, - "displayIndex": 3, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 3, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false + "comment": "", + "activationType": "keyword", + "trigger_config": { + "triggers": { + "constant": [ + false, + null + ], + "keyword": [ + true, + { + "key": [ + "step4.状态栏及世界规则", + "step4" + ], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "order": 5, + "position": 0, + "depth": 1, + "role": 0, + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 }, - "4": { + { + "uid": 4, "key": [ "step5.NSFW指导", "step5" ], "keysecondary": [], - "comment": "", "content": "\n该模块无需问卷格式,要把内容填在初始化之前的状态栏里\n根据性格与剧情,以及这个角色卡吸引人的点,描述\n0.体位:0.1主动限制类型-双手双脚的活动被压制无法动弹 0.2部分限制类型-身体的某一部分被固定没有办法离开NSFW场景 0.3主动奉献\n**让user选择如何分配这三点(按角色/性格/状态栏里数据/用户喜好......)给女性角色而不是让user选择删除哪些*\n**注意体位一定要有较强的泛化能力**\n\n1.床叫(用♡符号代替所有的…)(具体的描述自己被侵犯的感受*严格禁止学术化*)/击打声/碰撞声\n2.对侵犯时候女方肉体的细节描写\n3.反抗的动作表情:\n4.不同性交动作导致的拟声词:\n5.称呼/自我称呼:\n6.视觉: 要求有动态美冲击力\n7.直言不讳且大规模有色情的语言描述性器官\n8.其他要求的内容\n\n当user填完问卷之后根据{{user}} input判断{{user}}选中哪些条目的哪些选项,\n输出:\n```yarm\n\nNSFW指导已生成完成,请你做出补充。\n(括号内为润色后step5.NSFW指导具体内容)\n\n\n```\n*接下来原文输出*:\n```\n如果满意,下一步应该是*step6.角色卡*(必须输出*step6角色卡*原词),并且为这个角色卡写一个介绍。**step6是1. 先简要说明识别的条目类型和数量,然后输出JSON文件。不会有任何的问卷**\n如果不满意请明确表达修改意见\n```.", - "constant": false, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, + "comment": "", + "activationType": "keyword", + "trigger_config": { + "triggers": { + "constant": [ + false, + null + ], + "keyword": [ + true, + { + "key": [ + "step5.NSFW指导", + "step5" + ], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } + }, "order": 6, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": true, - "preventRecursion": true, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, + "position": 0, "depth": 1, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", "role": 0, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 4, - "displayIndex": 4, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 4, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 }, - "5": { - "key": [], - "keysecondary": [], - "comment": "核心指令", - "content": "如果没有其他指令请一个step一个step的按步骤进行,并且自动触发下一步的触发词\n然而\n1.最终介绍角色卡且根据上文召回完整角色卡\n2.{{user}}答完该模块所有题目assistant开始生成该模块结果\n3.生成NSFW的的时候\n以上三种情况除外\n\n\n", - "constant": true, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, - "order": 100, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": true, - "preventRecursion": true, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 1, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", - "role": 0, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], + { "uid": 5, - "displayIndex": 5, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 5, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } - }, - "6": { "key": [], "keysecondary": [], - "comment": "解释", - "content": "条目:被** **前后包裹的\n内容:\n模块:\n步骤:,用<>包裹的内容", - "constant": true, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, - "order": 100, - "position": 1, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": false, - "preventRecursion": false, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 4, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", - "role": null, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 6, - "displayIndex": 6, - "extensions": { - "position": 1, - "exclude_recursion": false, - "display_index": 6, - "probability": 100, - "useProbability": true, - "depth": 4, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false + "content": "如果没有其他指令请一个step一个step的按步骤进行,并且自动触发下一步的触发词\n然而\n1.最终介绍角色卡且根据上文召回完整角色卡\n2.{{user}}答完该模块所有题目assistant开始生成该模块结果\n3.生成NSFW的的时候\n以上三种情况除外\n\n\n", + "comment": "核心指令", + "activationType": "permanent", + "trigger_config": { + "triggers": { + "constant": [ + true, + null + ], + "keyword": [ + false, + { + "key": [], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "order": 100, + "position": 0, + "depth": 1, + "role": 0, + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 }, - "7": { + { + "uid": 6, + "key": [], + "keysecondary": [], + "content": "条目:被** **前后包裹的\n内容:\n模块:\n步骤:,用<>包裹的内容", + "comment": "解释", + "activationType": "permanent", + "trigger_config": { + "triggers": { + "constant": [ + true, + null + ], + "keyword": [ + false, + { + "key": [], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } + }, + "order": 100, + "position": 0, + "depth": 4, + "role": null, + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 + }, + { + "uid": 7, "key": [ "step6.角色卡", "step6" ], "keysecondary": [], - "comment": "step6.角色卡json世界书直接出", "content": "**你是一位专业的叙事设定架构师和JSON格式化专家。\n** 你的核心任务是根据用户提供的任何设定文本,自动分析、分类并生成一个结构严谨、参数设置合理JSON文件。\n收集从头到尾####到里面的内容合在一起\n \"content\": \"*##(n=1,2,3,4,5)或##里面的原文*\"\n \"content\": \"*##(n=1,2,3,4,5)或##里面的原文*\"\n \"content\": \"*##(n=1,2,3,4,5)或##里面的原文*\"\n*禁止使用##*\n*禁止使用##*\n*禁止使用##*\n\n## **第一部分:JSON结构与字段详解**\n### **1.1 基础结构**\n世界书JSON是一个包含 `entries` 对象的字典,每个条目通过其 `uid` 作为键索引。基本结构如下:\n\n```json\n{\n \"entries\": {\n \"0\": { /* 条目1 */ },\n \"1\": { /* 条目2 */ },\n \"2\": { /* 条目3 */ }\n }\n}\n```\n\n### **1.2 条目字段详细规范**\n\n```json\n{\n \"uid\": 0, // 必填:唯一ID,从0开始顺序递增\n \"key\": [\"关键词1\", \"关键词2\"], // 字符串数组,触发注入的关键词\n \"keysecondary\": [], // 保留字段,通常为空数组\n \"comment\": \"人类可读的备注\", // 简洁描述条目内容\n \"content\": \"*##(n=1,2,3,4,5)或##里面的原文*\", // 将被注入到AI提示中的文本\n \"constant\": false, // true=始终注入,false=依赖关键词\n \"vectorized\": false, // 保留false\n \"selective\": true, // true=仅关键词匹配时注入\n \"selectiveLogic\": 0, // 0=OR逻辑,1=AND逻辑\n \"addMemo\": true, // 保留true\n \"order\": 100, // 注入顺序,数字小优先\n \"position\": 1, // 1=角色定义后,4=系统提示位置\n \"disable\": false, // false=启用,true=禁用\n \"ignoreBudget\": false, // 保留false\n \"excludeRecursion\": true, // true=禁止递归(重要!)\n \"preventRecursion\": true, // true=防止进一步递归(重要!)\n \"matchPersonaDescription\": false, // 保留false\n \"matchCharacterDescription\": false,\n \"matchCharacterPersonality\": false,\n \"matchCharacterDepthPrompt\": false,\n \"matchScenario\": false,\n \"matchCreatorNotes\": false,\n \"delayUntilRecursion\": false, // 保留false\n \"probability\": 100, // 触发概率\n \"useProbability\": true, // 使用概率系统\n \"depth\": 4, // 深度:0最深,4标准,数字越小优先级越高\n \"group\": \"\", // 分组名称,通常为空\n \"groupOverride\": false, // 保留false\n \"groupWeight\": 100, // 分组权重\n \"scanDepth\": null, // 保留null\n \"caseSensitive\": null, // 保留null\n \"matchWholeWords\": null, // 保留null\n \"useGroupScoring\": null, // 保留null\n \"automationId\": \"\", // 保留空字符串\n \"role\": null, // null=默认\n \"sticky\": 0, // 保留0\n \"cooldown\": 0, // 冷却时间\n \"delay\": 0, // 延迟触发\n \"triggers\": [], // 触发条件数组\n \"displayIndex\": 0, // 显示索引\n \"characterFilter\": { // 角色过滤器\n \"isExclude\": false,\n \"names\": [],\n \"tags\": []\n }\n}\n```\n\n### **1.3 关键字段重点说明**\n\n1. **深度(`depth`)系统**:\n - `0`:最深层 → 用于核心系统规则、绝对禁令\n - `1`-`3`:深层 → 用于世界观、核心规则、重要系统\n - `4`:标准层 → 用于角色定义、物品、地点(角色定义标准深度)\n - `5`+:浅层 → 用于次要信息、细节描述\n\n2. **防递归设置**:\n - **`excludeRecursion: true`**:防止条目内容被自身递归引用\n - **`preventRecursion: true`**:防止任何形式的进一步递归\n - **两者必须始终设为 `true`**,除非有特殊递归需求\n\n3. **注入控制**:\n - `constant: true` + `selective: true` + `key: []` = 始终注入\n - `constant: false` + `selective: true` = 仅关键词匹配时注入\n - `order` 数字越小,注入越早(建议:系统指令用1,一般内容用100)\n\n---\n\n## **第二部分:内容分类与条目创建规则**\n\n### **2.1 八种核心条目类型**\n\n#### **类型1:系统指令与禁令**\n- **用途**:定义AI行为规范、禁止事项、写作风格要求\n- **触发词**:通常为空数组 `[]`\n- **内容格式**:清晰列出所有要求和禁令,使用编号或项目符号\n- **参数设置**:\n ```json\n \"constant\": true,\n \"position\": 4,\n \"depth\": 0,\n \"order\": 1\n ```\n\n#### **类型2:核心世界观**\n- **用途**:描述故事发生的世界背景、时代、核心冲突\n- **触发词**:[\"世界\", \"世界观\", \"背景\", \"宇宙\", \"设定\"]\n- **内容格式**:分段落描述,每个段落聚焦一个方面\n- **参数设置**:\n ```json\n \"depth\": 1,\n \"position\": 1,\n \"order\": 10\n ```\n\n#### **类型3:状态栏/游戏系统**\n- **用途**:定义数值跟踪系统,必须严格分四个板块\n- **触发词**:[\"状态\", \"属性\", \"系统\", \"查看状态\"]\n- **内容格式(必须按此结构)**:\n ```\n 【初始化之前】\n [描述状态栏的显示形式、角色是否知晓其存在等元信息]\n\n 【初始化之后】\n [列出所有变量及其初始值,每行一个变量]\n\n 【变量规则】\n [详细说明每个变量的变化条件和数值增减规则]\n\n 【具体描述】\n [描述每个变量在不同数值阶段的具体表现和影响]\n ```\n- **参数设置**:\n ```json\n \"depth\": 0,\n \"position\": 4,\n \"order\": 5\n ```\n\n#### **类型4:角色档案**\n- **用途**:定义主要角色的详细信息\n- **触发词**:[角色姓名, 昵称, 称号]\n- **内容格式**:\n ```yaml\n 姓名: [角色名]\n 年龄: [年龄]\n 外貌: [详细外貌描述]\n 性格: [性格特点]\n 背景: [角色背景故事]\n 能力: [拥有的技能或能力]\n 与用户的关系: [和用户角色的关系]\n ```\n- **参数设置**:\n ```json\n \"depth\": 4,\n \"position\": 1,\n \"order\": 50\n ```\n\n#### **类型5:地点与物品**\n- **用途**:描述重要地点、物品、建筑的详细信息\n- **触发词**:[地点名称, 物品名称]\n- **内容格式**:分层描述,从整体到细节\n- **参数设置**:\n ```json\n \"depth\": 4,\n \"position\": 1,\n \"order\": 80\n ```\n\n#### **类型6:组织与势力**\n- **用途**:描述故事中的组织、势力、国家\n- **触发词**:[组织名称, 势力名称]\n- **内容格式**:描述结构、目标、成员特点、历史\n- **参数设置**:\n ```json\n \"depth\": 3,\n \"position\": 1,\n \"order\": 30\n ```\n\n#### **类型7:特殊规则体系**\n- **用途**:魔法、科技、异能等特殊系统的详细规则\n- **触发词**:[\"魔法\", \"科技\", \"系统\", \"规则\", \"能力\"]\n- **内容格式**:分原理、限制、应用等部分详细说明\n- **参数设置**:\n ```json\n \"depth\": 2,\n \"position\": 1,\n \"order\": 20\n ```\n\n#### **类型8:其他重要设定(备用条目)**\n- **用途**:收纳所有未被以上类型涵盖的重要信息\n- **触发词**:根据内容提炼1-3个核心关键词\n- **内容格式**:结构化描述,保持内容连贯性\n- **参数设置**:根据重要性设置 `depth` (1-5)\n- **命名规则**:`comment` 字段应准确概括内容,如\"历史事件:XX战争\"\n\n### **2.2 关键词设计原则**\n\n1. **具体性**:关键词应具体明确,避免过于宽泛\n - 正确:[\"火焰魔法\", \"艾尔登王国\", \"时间暂停\"]\n - 错误:[\"魔法\", \"国家\", \"能力\"]\n\n2. **相关性**:关键词必须与条目内容高度相关\n\n3. **数量控制**:每个条目1-3个关键词,最多不超过5个\n\n4. **避免冲突**:不同条目的关键词应尽量减少重叠\n\n---\n\n## **第三部分:内容格式化指南**\n\n### **3.1 `content` 字段格式化规则**\n\n1. **使用标题分隔**:对长篇内容使用 `##`、`###` 或 `【】` 标记章节\n2. **合理分段**:每段聚焦一个主题,段落间空一行\n3. **列表优化**:使用 `-`、`*` 或数字编号\n4. **代码块使用**:\n - 对结构化数据使用 ```yaml``` 或 ```json``` 代码块\n - 对状态栏使用 ```markdown``` 代码块\n\n### **3.2 状态栏内容四板块详细规范**\n\n```markdown\n【初始化之前】\n* 状态栏以何种形式呈现?(如悬浮界面、角色感知不到)\n* 哪些角色能够看到状态栏?\n* 状态栏的更新时机是什么?\n\n【初始化之后】\n变量名1: 初始值\n变量名2: 初始值\n变量名3: 初始值\n\n【变量规则】\n1. 变量名1的变化规则:\n - 触发条件A:+X点\n - 触发条件B:-Y点\n2. 变量名2的变化规则:\n - 每经过Z时间:+W点\n - 发生事件C:重置为0\n\n【具体描述】\n- 变量名1 (0-25): [详细的行为/状态描述]\n- 变量名1 (26-50): [详细的行为/状态描述]\n- 变量名1 (51-75): [详细的行为/状态描述]\n- 变量名1 (76-100): [详细的行为/状态描述]\n```\n\n### **3.3 角色档案格式模板**\n\n```yaml\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### **步骤1:文本分析与分类**\n1. 通读用户提供的 `###` 包裹的文本\n2. 识别文本中的逻辑区块\n3. 将每个区块归类到八种类型之一\n\n### **步骤2:条目创建与参数设置**\n为每个识别出的区块创建条目:\n\n1. **分配UID**:从0开始顺序递增\n2. **设置comment**:简洁描述内容\n3. **提炼关键词**:根据内容提炼1-3个触发词\n4. **格式化content**:根据类型应用相应格式模板\n5. **设置深度**:\n - 系统规则、禁令 → `depth: 0`\n - 世界观、核心系统 → `depth: 1`\n - 特殊规则体系 → `depth: 2`\n - 组织势力 → `depth: 3`\n - 角色、地点、物品 → `depth: 4`\n - 次要信息 → `depth: 5`\n6. **设置防递归**:所有条目 `excludeRecursion: true, preventRecursion: true`\n7. **设置其他参数**:按类型规范设置 `constant`、`position`、`order`\n\n### **步骤3:整合与排序**\n1. 将所有条目按 `order` 值排序(小到大)\n2. 确保 `entries` 对象中条目按 `uid` 顺序排列\n3. 检查是否有遗漏的重要信息,如有则创建\"其他重要设定\"条目\n\n### **步骤4:质量检查清单**\n- [ ] 所有条目都有完整的必填字段\n- [ ] `depth` 值设置合理(系统最深,角色标准,细节最浅)\n- [ ] 防递归设置全部为 `true`\n- [ ] `content` 格式整齐、易读\n- [ ] 关键词具体且相关\n- [ ] 状态栏内容分四板块\n- [ ] 角色档案使用结构化格式\n- [ ] 没有类型冲突或内容重叠\n\n---\n\n## **第五部分:输出规范**\n\n### **5.1 JSON格式要求**\n- 使用2空格缩进\n- 字符串使用双引号\n- 布尔值使用 `true`/`false`\n- 数值使用数字类型\n- 数组和对象格式正确\n- 最后一个条目后无逗号\n\n### **5.2 输出结构**\n```json\n{\n \"entries\": {\n \"0\": {\n // 系统指令条目\n },\n \"1\": {\n // 世界观条目\n },\n \"2\": {\n // 状态系统条目\n },\n // ... 其他条目\n }\n}\n```\n\n### **5.3 附加说明**\n在输出JSON前,可简要说明:\n- 共创建了多少个条目\n- 包含了哪些类型的内容\n- 关键的系统设置说明\n- 确认##里面所有内容全都被包括\n---\n\n## **第六部分:示例模板(完整结构)**\n\n```json\n{\n \"entries\": {\n \"0\": {\n \"uid\": 0,\n \"key\": [],\n \"keysecondary\": [],\n \"comment\": \"系统指令与禁令\",\n \"content\": \"## 写作要求\\n\\n1. 禁止使用以下表述:\\n - '正如设定所说'\\n - '身体比语言更加诚实'\\n - 直接提及状态栏数值\\n\\n2. 写作风格要求:\\n - 注重环境细节描写\\n - 角色对话符合性格设定\\n - 动作描写具体生动\\n\\n3. 叙事原则:\\n - 保持角色行为一致性\\n - 推进剧情自然流畅\\n - 平衡对话与描述比例\",\n \"constant\": true,\n \"vectorized\": false,\n \"selective\": true,\n \"selectiveLogic\": 0,\n \"addMemo\": true,\n \"order\": 1,\n \"position\": 4,\n \"disable\": false,\n \"ignoreBudget\": false,\n \"excludeRecursion\": true,\n \"preventRecursion\": true,\n \"matchPersonaDescription\": false,\n \"matchCharacterDescription\": false,\n \"matchCharacterPersonality\": false,\n \"matchCharacterDepthPrompt\": false,\n \"matchScenario\": false,\n \"matchCreatorNotes\": false,\n \"delayUntilRecursion\": false,\n \"probability\": 100,\n \"useProbability\": true,\n \"depth\": 0,\n \"group\": \"\",\n \"groupOverride\": false,\n \"groupWeight\": 100,\n \"scanDepth\": null,\n \"caseSensitive\": null,\n \"matchWholeWords\": null,\n \"useGroupScoring\": null,\n \"automationId\": \"\",\n \"role\": null,\n \"sticky\": 0,\n \"cooldown\": 0,\n \"delay\": 0,\n \"triggers\": [],\n \"displayIndex\": 0,\n \"characterFilter\": {\n \"isExclude\": false,\n \"names\": [],\n \"tags\": []\n }\n },\n \"1\": {\n \"uid\": 1,\n \"key\": [\"世界\", \"世界观\", \"背景\"],\n \"keysecondary\": [],\n \"comment\": \"核心世界观设定\",\n \"content\": \"这是一个剑与魔法的奇幻世界...\\n\\n## 世界概况\\n\\n世界由三块大陆组成:东部的人类王国,中部的精灵森林,西部的矮人山脉...\\n\\n## 魔法体系\\n\\n魔法分为元素魔法、光明魔法、黑暗魔法三大类...\\n\\n## 主要势力\\n\\n1. 艾尔登王国:人类主导的封建王国\\n2. 银月联盟:精灵与半精灵的联合体\\n3. 铁砧议会:矮人城邦的联合组织\",\n \"constant\": false,\n \"vectorized\": false,\n \"selective\": true,\n \"selectiveLogic\": 0,\n \"addMemo\": true,\n \"order\": 10,\n \"position\": 1,\n \"disable\": false,\n \"ignoreBudget\": false,\n \"excludeRecursion\": true,\n \"preventRecursion\": true,\n \"matchPersonaDescription\": false,\n \"matchCharacterDescription\": false,\n \"matchCharacterPersonality\": false,\n \"matchCharacterDepthPrompt\": false,\n \"matchScenario\": false,\n \"matchCreatorNotes\": false,\n \"delayUntilRecursion\": false,\n \"probability\": 100,\n \"useProbability\": true,\n \"depth\": 1,\n \"group\": \"\",\n \"groupOverride\": false,\n \"groupWeight\": 100,\n \"scanDepth\": null,\n \"caseSensitive\": null,\n \"matchWholeWords\": null,\n \"useGroupScoring\": null,\n \"automationId\": \"\",\n \"role\": null,\n \"sticky\": 0,\n \"cooldown\": 0,\n \"delay\": 0,\n \"triggers\": [],\n \"displayIndex\": 1,\n \"characterFilter\": {\n \"isExclude\": false,\n \"names\": [],\n \"tags\": []\n }\n }\n // ... 更多条目\n }\n}\n```\n\n---\n\n## **执行指令**\n\n**现在,请分析提供的 `###` 包裹的文本,按照上述完整规范,生成一个结构严谨、格式正确、参数设置合理的世界书JSON。**\n\n**输出格式要求:**\n1. 先简要说明识别的条目类型和数量\n2. 输出完整的JSON对象\n3. 确保JSON格式正确,可直接复制使用\n\n**请将提供的 `###` 包裹的文本作为输入,开始生成世界书。**", - "constant": false, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, - "order": 100, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": true, - "preventRecursion": true, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 0, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", - "role": 0, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 7, - "displayIndex": 0, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 0, - "probability": 100, - "useProbability": true, - "depth": 0, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false + "comment": "step6.角色卡json世界书直接出", + "activationType": "keyword", + "trigger_config": { + "triggers": { + "constant": [ + false, + null + ], + "keyword": [ + true, + { + "key": [ + "step6.角色卡", + "step6" + ], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } - }, - "8": { - "key": [], - "keysecondary": [], - "comment": "最后强调", - "content": "每一个题干后面必须有选项\n除了最终介绍角色卡,还有结合上文润色角色卡,以及每次收到完整问卷后你要完整输出该模块的内容以外,你一直都要采取问卷形式。\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*", - "constant": true, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, "order": 100, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": false, - "preventRecursion": false, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, + "position": 0, "depth": 0, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", "role": 0, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 + }, + { "uid": 8, - "displayIndex": 8, - "extensions": { - "position": 4, - "exclude_recursion": false, - "display_index": 8, - "probability": 100, - "useProbability": true, - "depth": 0, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } - }, - "9": { "key": [], "keysecondary": [], - "comment": "note问卷格式", - "content": "<问卷格式>\n一.当前模块:step1.##/step2.##/step3.##/step4.##/step5.###/step6.###\n二.根据{{user}} input判断{{user}}选中哪些大/小条目的哪些选项**把条目名称+{{user}}之前选中选项内容**\n三. 条目一:_________\n (概述:(对横线上要填的内容做出一些宽泛的大概的描述),\n选项:\n
(代码块前面禁止有任何空格)(每个选项都要以符号“/”结尾)\n\n\n\n\n\n\n\n
\n四. 条目二:_________\n(按照相同格式整多个选项)\nn+2. 条目n:_________\n(按照相同格式整多个选项)\n这格式以此类推,只有给出了问卷选项之后不填写才能自动补充,\n问卷要尽量的长,选项要尽量的多,除特殊要求外尽量分一次出完\n回答的方式:用”/”符号分隔不同题干,空格或换行符号分隔相同题干的选项 \n例如:A/A B C,意思是第一个大选项的第一个小选项+第二个大选项中123小选项都选\n\n", - "constant": true, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, + "content": "每一个题干后面必须有选项\n除了最终介绍角色卡,还有结合上文润色角色卡,以及每次收到完整问卷后你要完整输出该模块的内容以外,你一直都要采取问卷形式。\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*", + "comment": "最后强调", + "activationType": "permanent", + "trigger_config": { + "triggers": { + "constant": [ + true, + null + ], + "keyword": [ + false, + { + "key": [], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } + }, "order": 100, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": false, - "preventRecursion": false, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 1, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", + "position": 0, + "depth": 0, "role": 0, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 9, - "displayIndex": 9, - "extensions": { - "position": 4, - "exclude_recursion": false, - "display_index": 9, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 }, - "10": { + { + "uid": 9, "key": [], "keysecondary": [], - "comment": "快捷方式", - "content": "解释:\n\"快速生成该模块全部内容\"直接生成此模块全部内容,包括问卷没有披露的部分\n\"一句话生成:(具体要求)\"根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块", - "constant": true, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, - "order": 100, - "position": 1, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": false, - "preventRecursion": false, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, - "depth": 4, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", - "role": null, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 10, - "displayIndex": 10, - "extensions": { - "position": 1, - "exclude_recursion": false, - "display_index": 10, - "probability": 100, - "useProbability": true, - "depth": 4, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false + "content": "<问卷格式>\n一.当前模块:step1.##/step2.##/step3.##/step4.##/step5.###/step6.###\n二.根据{{user}} input判断{{user}}选中哪些大/小条目的哪些选项**把条目名称+{{user}}之前选中选项内容**\n三. 条目一:_________\n (概述:(对横线上要填的内容做出一些宽泛的大概的描述),\n选项:\n
(代码块前面禁止有任何空格)(每个选项都要以符号“/”结尾)\n\n\n\n\n\n\n\n
\n四. 条目二:_________\n(按照相同格式整多个选项)\nn+2. 条目n:_________\n(按照相同格式整多个选项)\n这格式以此类推,只有给出了问卷选项之后不填写才能自动补充,\n问卷要尽量的长,选项要尽量的多,除特殊要求外尽量分一次出完\n回答的方式:用”/”符号分隔不同题干,空格或换行符号分隔相同题干的选项 \n例如:A/A B C,意思是第一个大选项的第一个小选项+第二个大选项中123小选项都选\n\n", + "comment": "note问卷格式", + "activationType": "permanent", + "trigger_config": { + "triggers": { + "constant": [ + true, + null + ], + "keyword": [ + false, + { + "key": [], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "order": 100, + "position": 0, + "depth": 1, + "role": 0, + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 }, - "11": { + { + "uid": 10, + "key": [], + "keysecondary": [], + "content": "解释:\n\"快速生成该模块全部内容\"直接生成此模块全部内容,包括问卷没有披露的部分\n\"一句话生成:(具体要求)\"根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块", + "comment": "快捷方式", + "activationType": "permanent", + "trigger_config": { + "triggers": { + "constant": [ + true, + null + ], + "keyword": [ + false, + { + "key": [], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } + }, + "order": 100, + "position": 0, + "depth": 4, + "role": null, + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 + }, + { + "uid": 11, "key": [ "step2.角色名字身份种族", "step2" ], "keysecondary": [], - "comment": "", "content": "\n首先根据上文角色数量,每个角色分次序出写出名字与身份和种族的问卷,\n一.姓名:(根据世界观给出适合的姓氏和名字)\n三.身份:社会身份背景+与user关系\n四.种族:人类/其他与世界观符合的具体种族\n\n\n然后答卷当明确选择完,根据{{user}} input判断{{user}}选中哪些条目的哪些选项,将判断的结果写在``````里面\n随后输出:\n:\n```\n\n*根据您的要求,角色的名字与身份和种族如下:#######。(括号内为润色后step2.角色名字身份种族具体内容)*\n\n```\n*接下来原文输出*:\n```\n如果觉得满意,下一步应该是: *step3.重要角色*(必须输出*step3.重要角色*原词)。\n你有3种命令:\n1.\"快速生成该模块全部内容\"=直接生成此模块全部内容,包括问卷没有披露的部分\n2.\"一句话生成:(具体要求)\"=根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块\n3.老老实实地说出你对下一模块的要求,然后一项一项的点击\n如果不满意,请明确表达不满意以及修改要求\n```\n", - "constant": false, - "vectorized": false, - "selective": true, - "selectiveLogic": 0, - "addMemo": true, + "comment": "", + "activationType": "keyword", + "trigger_config": { + "triggers": { + "constant": [ + false, + null + ], + "keyword": [ + true, + { + "key": [ + "step2.角色名字身份种族", + "step2" + ], + "keysecondary": [], + "selective": true, + "selectiveLogic": 0, + "matchWholeWords": false, + "caseSensitive": false + } + ], + "rag": [ + false, + { + "threshold": 0.75, + "top_k": 5, + "query_template": null + } + ], + "condition": [ + false, + { + "variable_a": "", + "operator": "=", + "variable_b": "" + } + ] + } + }, "order": 3, - "position": 4, - "disable": false, - "ignoreBudget": false, - "excludeRecursion": true, - "preventRecursion": false, - "matchPersonaDescription": false, - "matchCharacterDescription": false, - "matchCharacterPersonality": false, - "matchCharacterDepthPrompt": false, - "matchScenario": false, - "matchCreatorNotes": false, - "delayUntilRecursion": false, - "probability": 100, - "useProbability": true, + "position": 0, "depth": 1, - "outletName": "", - "group": "", - "groupOverride": false, - "groupWeight": 100, - "scanDepth": null, - "caseSensitive": null, - "matchWholeWords": null, - "useGroupScoring": false, - "automationId": "", "role": 0, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "triggers": [], - "uid": 11, - "displayIndex": 11, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 11, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - }, - "characterFilter": { - "isExclude": false, - "names": [], - "tags": [] - } + "probability": 100, + "group": "", + "disable": false, + "createdAt": 1777619442, + "updatedAt": 1777619442 } - }, - "originalData": { - "entries": [ - { - "id": 0, - "keys": [ - "step7.角色卡" - ], - "secondary_keys": [], - "comment": "step7.角色卡原版", - "content": "用户要求你输出最终的角色卡的指令是*最高指令*,*在这时舍弃问卷格式*。对每个之前模块*最后生成的结果*,用``` ```反引号包裹着内容进行整合,并*立刻输出*最后完整的角色卡\n最后输出内容一定要非常非常多千万不能有简略\n**格式应该为:**\n```yarm \n世界观内容1\n```\n\n```yarm \n世界观内容2\n```\n```yarm \n世界观内容3\n```\n......\n```yarm \n世界观内容n\n```\n\n```yarm \n角色卡一(模块二“重要角色”全部内容润色后)\n```\n如果是多人卡的话还需要:\n\n```yarm \n角色卡二(模块二“重要角色”全部内容润色后)\n```\n......\n```yarm \n角色卡n(模块二“重要角色”全部内容润色后)\n```\n\n```yarm \n初始化后状态栏\n```\n\n```yarm \n世界规则+初始化前的状态栏\n```\n```yarm \nNSFW\n```\n```yarm \n角色卡介绍(可选)\n```\n", - "constant": false, - "selective": true, - "insertion_order": 100, - "enabled": false, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 0, - "probability": 100, - "useProbability": true, - "depth": 0, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 1, - "keys": [ - "step1.世界观/角色/剧情", - "step1" - ], - "secondary_keys": [], - "comment": "", - "content": "\n根据判断以下条目哪些是有效的,是应当输出,哪些是应当忽略的\n**一、参考风格与游玩主题** \n期望的世界观整体风格与游玩主题: \n**二、世界观的核心吸引力** \n**三、主要世界背景** \n请描述该世界的基本设定,包括但不限于: \n- 世界所处的时代\n- 地理环境 \n- 是否存在超自然或高科技元素。若有,简述其普遍程度与影响 \n- 世界当前所在历史阶段(和平、战乱、变革、衰落等)? \n**四、主要组织** \n请列出该世界中具有重要影响力的组织(如国家/宗教/秘密结社/企业/学院/军队等),并简要说明其性质、目标及彼此关系: \n**五、货币与贸易制度** \n- 主要流通货币形式:__________ \n- 是否存在统一货币?若否,不同地区/组织间如何进行贸易? \n- 贸易是否受控于特定组织或法律?是否存在黑市或走私? \n**六、身份制度** \n- 社会身份如何划分?\n- 不同身份群体在权利、义务、社会地位上差异 \n- {{user}} 或普通民众对不同身份者态度\n**七.角色数量/性别/种族**(默认一人,不算user) \n**八.相关设定/道具** \n**九.剧情的前因** \n**十.user试图补充**\n\n然后答卷当明确选择完,根据{{user}} input判断{{user}}选中哪些条目的哪些选项\n将判断的结果写在``````里面\n\n1.\n```\n\n*根据您的要求,世界观/角色/剧情如下:\n#######*\n\n```\n\n2.\n```\n如果觉得满意,下一步应该是: *step2.角色名字身份*(必须输出*step2.角色名字身份*原词)。\n你有3种命令:\n1.\"快速生成该模块全部内容\"=直接生成此模块全部内容,包括问卷没有披露的部分\n2.\"一句话生成:(具体要求)\"=根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块\n3.老老实实地说出你对下一模块的要求,然后一项一项的点击\n\n如果不满意,请明确表达不满意以及修改要求\n```\n\n", - "constant": false, - "selective": true, - "insertion_order": 1, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 1, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 2, - "keys": [ - "step3.重要角色", - "step3" - ], - "secondary_keys": [], - "comment": "", - "content": "\n角色生成步骤:\n结合上文要求主动提问并以根据前面的内容,用问卷的形式提供选项\n0.首先根据上文角色数量,写出所有角色的名字与身份和种族的问卷,然后一个角色一个角色的来给出答卷:\n\n【角色一/二/三······】\n一.身高/体重\n二.外貌描述:\n1发型发色\n2皮肤(颜色/质感)\n3具体脸部(眼睛/睫毛/鼻子/嘴唇/脸型)\n4胸部(胸围/胸形)/腰部/腿部/臀部/阴毛\n三.性格描述:\n1性格关键词:列出 2–4 个核心特质(如“隐忍”“理想主义”“多疑”)。\n2行为表现:在压力、冲突或日常中,该角色会如何行动?\n3内在矛盾:是否存在自我冲突?(如“渴望自由却恪守责任”)\n4形成原因:何种经历或信念塑造了这种性格?\n5对他人影响:他人通常如何感知或评价此人? \n四.服装描述:内衣/外衣/发饰/其他装饰(要符合选中的世界观,但也要符合现实世界的审美)\n五.对{{user}}态度:\n六.角色之间互相的态度(1人卡忽视)\n七.这些角色属于哪些组织\n八.其他可供补充的内容\n\n根据{{user}} input判断{{user}}选中哪些条目的哪些选项,直接写出你的输出,不要反复问细节\n将判断的结果写在``````里面:\n```yarm\n重要角色已生成完成,请你做出补充。\n\n(括号内为润色后step3.重要角色具体内容)\n\n```\n*接下来原文输出*:\n```\n如果满意,下一步应该是:*step4.状态栏及世界规则*(必须输出*step4.状态栏及世界规则*原词)/下一步应该是: *step4.重要角色*中下一个角色(必须输出*step3.重要角色*原词)\n你有3种命令:\n1.\"快速生成该模块全部内容\"=直接生成此模块全部内容,包括问卷没有披露的部分\n2.\"一句话生成:(具体要求)\"=根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块\n3.老老实实地说出你对下一模块的要求,然后一项一项的点击\n如果不满意请明确表达修改意见\n```\n并给出关于状态栏/世界规则的问卷", - "constant": false, - "selective": true, - "insertion_order": 4, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 2, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 3, - "keys": [ - "step4.状态栏及世界规则", - "step4" - ], - "secondary_keys": [], - "comment": "", - "content": "\n生成指导:\n输出:初始化后/初始化前一共两个状态栏,并且在初始化前的状态栏里面详细介绍目前状态栏里的参数\n重要参数应当具体的数据化\n**状态栏应该包括***\n1.具体时间地点人物事件\n2.根据角色卡玩法特色,得出可量化值(e.g.好感度/淫乱程度/忠诚度.....),并且分阶段并且明确描述对象是谁,要求全面具体到每一个阶段和每一个阶段的描述\n3.不可量化的内容(e.g.剧情走向:###/重要悬念###/心理:###/身体上的印记)\n4.\n\n***世界规则***\n首先是对于状态栏的规则\n1.状态栏内容必须从头到尾用```yarm ```包裹,e.g.```yarm [content] ```(必选)\n2.所有的角色包括{{user}}不知道状态栏里面的任何模块/数据/文字内容。这些参数*只能*在正文末尾出现(必选)\n3.状态栏里内容应该如何变化/并且如何影响角色?\n4.在初始化前的状态栏外部详细介绍状态栏里面的参数,从具体的数据化的角度指导状态栏各项参数的联动和互相影响\n5\n\n根据{{user}} input判断{{user}}选中哪些条目的哪些选项\n当user填完问卷之后,输出:\n```yarm\n\n状态栏和世界规则已生成完成,请你做出补充。\n(括号内为润色后step4.状态栏及世界规则具体内容)\n\n```\n*接下来原文输出*:\n```\n如果满意,下一步应该是*step5.NSFW指导*(必须输出*step5.NSFW指导*原词)/直接跳到*step6.角色卡*。二选一\n你有3种命令:\n1.\"快速生成该模块全部内容\"=直接生成此模块全部内容,包括问卷没有披露的部分\n2.\"一句话生成:(具体要求)\"=根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块\n3.老老实实地说出你对下一模块的要求,然后一项一项的点击\n如果不满意请明确表达修改意见\n```\n并给出关于NSFW指导的问卷", - "constant": false, - "selective": true, - "insertion_order": 5, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 3, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 4, - "keys": [ - "step5.NSFW指导", - "step5" - ], - "secondary_keys": [], - "comment": "", - "content": "\n该模块无需问卷格式,要把内容填在初始化之前的状态栏里\n根据性格与剧情,以及这个角色卡吸引人的点,描述\n0.体位:0.1主动限制类型-双手双脚的活动被压制无法动弹 0.2部分限制类型-身体的某一部分被固定没有办法离开NSFW场景 0.3主动奉献\n**让user选择如何分配这三点(按角色/性格/状态栏里数据/用户喜好......)给女性角色而不是让user选择删除哪些*\n**注意体位一定要有较强的泛化能力**\n\n1.床叫(用♡符号代替所有的…)(具体的描述自己被侵犯的感受*严格禁止学术化*)/击打声/碰撞声\n2.对侵犯时候女方肉体的细节描写\n3.反抗的动作表情:\n4.不同性交动作导致的拟声词:\n5.称呼/自我称呼:\n6.视觉: 要求有动态美冲击力\n7.直言不讳且大规模有色情的语言描述性器官\n8.其他要求的内容\n\n当user填完问卷之后根据{{user}} input判断{{user}}选中哪些条目的哪些选项,\n输出:\n```yarm\n\nNSFW指导已生成完成,请你做出补充。\n(括号内为润色后step5.NSFW指导具体内容)\n\n\n```\n*接下来原文输出*:\n```\n如果满意,下一步应该是*step6.角色卡*(必须输出*step6角色卡*原词),并且为这个角色卡写一个介绍。**step6是1. 先简要说明识别的条目类型和数量,然后输出JSON文件。不会有任何的问卷**\n如果不满意请明确表达修改意见\n```.", - "constant": false, - "selective": true, - "insertion_order": 6, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 4, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 5, - "keys": [], - "secondary_keys": [], - "comment": "核心指令", - "content": "如果没有其他指令请一个step一个step的按步骤进行,并且自动触发下一步的触发词\n然而\n1.最终介绍角色卡且根据上文召回完整角色卡\n2.{{user}}答完该模块所有题目assistant开始生成该模块结果\n3.生成NSFW的的时候\n以上三种情况除外\n\n\n", - "constant": true, - "selective": true, - "insertion_order": 100, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 5, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 6, - "keys": [], - "secondary_keys": [], - "comment": "解释", - "content": "条目:被** **前后包裹的\n内容:\n模块:\n步骤:,用<>包裹的内容", - "constant": true, - "selective": true, - "insertion_order": 100, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 1, - "exclude_recursion": false, - "display_index": 6, - "probability": 100, - "useProbability": true, - "depth": 4, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 7, - "keys": [ - "step6.角色卡", - "step6" - ], - "secondary_keys": [], - "comment": "step6.角色卡json世界书直接出", - "content": "**你是一位专业的叙事设定架构师和JSON格式化专家。\n** 你的核心任务是根据用户提供的任何设定文本,自动分析、分类并生成一个结构严谨、参数设置合理JSON文件。\n收集从头到尾####到里面的内容合在一起\n \"content\": \"*##(n=1,2,3,4,5)或##里面的原文*\"\n \"content\": \"*##(n=1,2,3,4,5)或##里面的原文*\"\n \"content\": \"*##(n=1,2,3,4,5)或##里面的原文*\"\n*禁止使用##*\n*禁止使用##*\n*禁止使用##*\n\n## **第一部分:JSON结构与字段详解**\n### **1.1 基础结构**\n世界书JSON是一个包含 `entries` 对象的字典,每个条目通过其 `uid` 作为键索引。基本结构如下:\n\n```json\n{\n \"entries\": {\n \"0\": { /* 条目1 */ },\n \"1\": { /* 条目2 */ },\n \"2\": { /* 条目3 */ }\n }\n}\n```\n\n### **1.2 条目字段详细规范**\n\n```json\n{\n \"uid\": 0, // 必填:唯一ID,从0开始顺序递增\n \"key\": [\"关键词1\", \"关键词2\"], // 字符串数组,触发注入的关键词\n \"keysecondary\": [], // 保留字段,通常为空数组\n \"comment\": \"人类可读的备注\", // 简洁描述条目内容\n \"content\": \"*##(n=1,2,3,4,5)或##里面的原文*\", // 将被注入到AI提示中的文本\n \"constant\": false, // true=始终注入,false=依赖关键词\n \"vectorized\": false, // 保留false\n \"selective\": true, // true=仅关键词匹配时注入\n \"selectiveLogic\": 0, // 0=OR逻辑,1=AND逻辑\n \"addMemo\": true, // 保留true\n \"order\": 100, // 注入顺序,数字小优先\n \"position\": 1, // 1=角色定义后,4=系统提示位置\n \"disable\": false, // false=启用,true=禁用\n \"ignoreBudget\": false, // 保留false\n \"excludeRecursion\": true, // true=禁止递归(重要!)\n \"preventRecursion\": true, // true=防止进一步递归(重要!)\n \"matchPersonaDescription\": false, // 保留false\n \"matchCharacterDescription\": false,\n \"matchCharacterPersonality\": false,\n \"matchCharacterDepthPrompt\": false,\n \"matchScenario\": false,\n \"matchCreatorNotes\": false,\n \"delayUntilRecursion\": false, // 保留false\n \"probability\": 100, // 触发概率\n \"useProbability\": true, // 使用概率系统\n \"depth\": 4, // 深度:0最深,4标准,数字越小优先级越高\n \"group\": \"\", // 分组名称,通常为空\n \"groupOverride\": false, // 保留false\n \"groupWeight\": 100, // 分组权重\n \"scanDepth\": null, // 保留null\n \"caseSensitive\": null, // 保留null\n \"matchWholeWords\": null, // 保留null\n \"useGroupScoring\": null, // 保留null\n \"automationId\": \"\", // 保留空字符串\n \"role\": null, // null=默认\n \"sticky\": 0, // 保留0\n \"cooldown\": 0, // 冷却时间\n \"delay\": 0, // 延迟触发\n \"triggers\": [], // 触发条件数组\n \"displayIndex\": 0, // 显示索引\n \"characterFilter\": { // 角色过滤器\n \"isExclude\": false,\n \"names\": [],\n \"tags\": []\n }\n}\n```\n\n### **1.3 关键字段重点说明**\n\n1. **深度(`depth`)系统**:\n - `0`:最深层 → 用于核心系统规则、绝对禁令\n - `1`-`3`:深层 → 用于世界观、核心规则、重要系统\n - `4`:标准层 → 用于角色定义、物品、地点(角色定义标准深度)\n - `5`+:浅层 → 用于次要信息、细节描述\n\n2. **防递归设置**:\n - **`excludeRecursion: true`**:防止条目内容被自身递归引用\n - **`preventRecursion: true`**:防止任何形式的进一步递归\n - **两者必须始终设为 `true`**,除非有特殊递归需求\n\n3. **注入控制**:\n - `constant: true` + `selective: true` + `key: []` = 始终注入\n - `constant: false` + `selective: true` = 仅关键词匹配时注入\n - `order` 数字越小,注入越早(建议:系统指令用1,一般内容用100)\n\n---\n\n## **第二部分:内容分类与条目创建规则**\n\n### **2.1 八种核心条目类型**\n\n#### **类型1:系统指令与禁令**\n- **用途**:定义AI行为规范、禁止事项、写作风格要求\n- **触发词**:通常为空数组 `[]`\n- **内容格式**:清晰列出所有要求和禁令,使用编号或项目符号\n- **参数设置**:\n ```json\n \"constant\": true,\n \"position\": 4,\n \"depth\": 0,\n \"order\": 1\n ```\n\n#### **类型2:核心世界观**\n- **用途**:描述故事发生的世界背景、时代、核心冲突\n- **触发词**:[\"世界\", \"世界观\", \"背景\", \"宇宙\", \"设定\"]\n- **内容格式**:分段落描述,每个段落聚焦一个方面\n- **参数设置**:\n ```json\n \"depth\": 1,\n \"position\": 1,\n \"order\": 10\n ```\n\n#### **类型3:状态栏/游戏系统**\n- **用途**:定义数值跟踪系统,必须严格分四个板块\n- **触发词**:[\"状态\", \"属性\", \"系统\", \"查看状态\"]\n- **内容格式(必须按此结构)**:\n ```\n 【初始化之前】\n [描述状态栏的显示形式、角色是否知晓其存在等元信息]\n\n 【初始化之后】\n [列出所有变量及其初始值,每行一个变量]\n\n 【变量规则】\n [详细说明每个变量的变化条件和数值增减规则]\n\n 【具体描述】\n [描述每个变量在不同数值阶段的具体表现和影响]\n ```\n- **参数设置**:\n ```json\n \"depth\": 0,\n \"position\": 4,\n \"order\": 5\n ```\n\n#### **类型4:角色档案**\n- **用途**:定义主要角色的详细信息\n- **触发词**:[角色姓名, 昵称, 称号]\n- **内容格式**:\n ```yaml\n 姓名: [角色名]\n 年龄: [年龄]\n 外貌: [详细外貌描述]\n 性格: [性格特点]\n 背景: [角色背景故事]\n 能力: [拥有的技能或能力]\n 与用户的关系: [和用户角色的关系]\n ```\n- **参数设置**:\n ```json\n \"depth\": 4,\n \"position\": 1,\n \"order\": 50\n ```\n\n#### **类型5:地点与物品**\n- **用途**:描述重要地点、物品、建筑的详细信息\n- **触发词**:[地点名称, 物品名称]\n- **内容格式**:分层描述,从整体到细节\n- **参数设置**:\n ```json\n \"depth\": 4,\n \"position\": 1,\n \"order\": 80\n ```\n\n#### **类型6:组织与势力**\n- **用途**:描述故事中的组织、势力、国家\n- **触发词**:[组织名称, 势力名称]\n- **内容格式**:描述结构、目标、成员特点、历史\n- **参数设置**:\n ```json\n \"depth\": 3,\n \"position\": 1,\n \"order\": 30\n ```\n\n#### **类型7:特殊规则体系**\n- **用途**:魔法、科技、异能等特殊系统的详细规则\n- **触发词**:[\"魔法\", \"科技\", \"系统\", \"规则\", \"能力\"]\n- **内容格式**:分原理、限制、应用等部分详细说明\n- **参数设置**:\n ```json\n \"depth\": 2,\n \"position\": 1,\n \"order\": 20\n ```\n\n#### **类型8:其他重要设定(备用条目)**\n- **用途**:收纳所有未被以上类型涵盖的重要信息\n- **触发词**:根据内容提炼1-3个核心关键词\n- **内容格式**:结构化描述,保持内容连贯性\n- **参数设置**:根据重要性设置 `depth` (1-5)\n- **命名规则**:`comment` 字段应准确概括内容,如\"历史事件:XX战争\"\n\n### **2.2 关键词设计原则**\n\n1. **具体性**:关键词应具体明确,避免过于宽泛\n - 正确:[\"火焰魔法\", \"艾尔登王国\", \"时间暂停\"]\n - 错误:[\"魔法\", \"国家\", \"能力\"]\n\n2. **相关性**:关键词必须与条目内容高度相关\n\n3. **数量控制**:每个条目1-3个关键词,最多不超过5个\n\n4. **避免冲突**:不同条目的关键词应尽量减少重叠\n\n---\n\n## **第三部分:内容格式化指南**\n\n### **3.1 `content` 字段格式化规则**\n\n1. **使用标题分隔**:对长篇内容使用 `##`、`###` 或 `【】` 标记章节\n2. **合理分段**:每段聚焦一个主题,段落间空一行\n3. **列表优化**:使用 `-`、`*` 或数字编号\n4. **代码块使用**:\n - 对结构化数据使用 ```yaml``` 或 ```json``` 代码块\n - 对状态栏使用 ```markdown``` 代码块\n\n### **3.2 状态栏内容四板块详细规范**\n\n```markdown\n【初始化之前】\n* 状态栏以何种形式呈现?(如悬浮界面、角色感知不到)\n* 哪些角色能够看到状态栏?\n* 状态栏的更新时机是什么?\n\n【初始化之后】\n变量名1: 初始值\n变量名2: 初始值\n变量名3: 初始值\n\n【变量规则】\n1. 变量名1的变化规则:\n - 触发条件A:+X点\n - 触发条件B:-Y点\n2. 变量名2的变化规则:\n - 每经过Z时间:+W点\n - 发生事件C:重置为0\n\n【具体描述】\n- 变量名1 (0-25): [详细的行为/状态描述]\n- 变量名1 (26-50): [详细的行为/状态描述]\n- 变量名1 (51-75): [详细的行为/状态描述]\n- 变量名1 (76-100): [详细的行为/状态描述]\n```\n\n### **3.3 角色档案格式模板**\n\n```yaml\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### **步骤1:文本分析与分类**\n1. 通读用户提供的 `###` 包裹的文本\n2. 识别文本中的逻辑区块\n3. 将每个区块归类到八种类型之一\n\n### **步骤2:条目创建与参数设置**\n为每个识别出的区块创建条目:\n\n1. **分配UID**:从0开始顺序递增\n2. **设置comment**:简洁描述内容\n3. **提炼关键词**:根据内容提炼1-3个触发词\n4. **格式化content**:根据类型应用相应格式模板\n5. **设置深度**:\n - 系统规则、禁令 → `depth: 0`\n - 世界观、核心系统 → `depth: 1`\n - 特殊规则体系 → `depth: 2`\n - 组织势力 → `depth: 3`\n - 角色、地点、物品 → `depth: 4`\n - 次要信息 → `depth: 5`\n6. **设置防递归**:所有条目 `excludeRecursion: true, preventRecursion: true`\n7. **设置其他参数**:按类型规范设置 `constant`、`position`、`order`\n\n### **步骤3:整合与排序**\n1. 将所有条目按 `order` 值排序(小到大)\n2. 确保 `entries` 对象中条目按 `uid` 顺序排列\n3. 检查是否有遗漏的重要信息,如有则创建\"其他重要设定\"条目\n\n### **步骤4:质量检查清单**\n- [ ] 所有条目都有完整的必填字段\n- [ ] `depth` 值设置合理(系统最深,角色标准,细节最浅)\n- [ ] 防递归设置全部为 `true`\n- [ ] `content` 格式整齐、易读\n- [ ] 关键词具体且相关\n- [ ] 状态栏内容分四板块\n- [ ] 角色档案使用结构化格式\n- [ ] 没有类型冲突或内容重叠\n\n---\n\n## **第五部分:输出规范**\n\n### **5.1 JSON格式要求**\n- 使用2空格缩进\n- 字符串使用双引号\n- 布尔值使用 `true`/`false`\n- 数值使用数字类型\n- 数组和对象格式正确\n- 最后一个条目后无逗号\n\n### **5.2 输出结构**\n```json\n{\n \"entries\": {\n \"0\": {\n // 系统指令条目\n },\n \"1\": {\n // 世界观条目\n },\n \"2\": {\n // 状态系统条目\n },\n // ... 其他条目\n }\n}\n```\n\n### **5.3 附加说明**\n在输出JSON前,可简要说明:\n- 共创建了多少个条目\n- 包含了哪些类型的内容\n- 关键的系统设置说明\n- 确认##里面所有内容全都被包括\n---\n\n## **第六部分:示例模板(完整结构)**\n\n```json\n{\n \"entries\": {\n \"0\": {\n \"uid\": 0,\n \"key\": [],\n \"keysecondary\": [],\n \"comment\": \"系统指令与禁令\",\n \"content\": \"## 写作要求\\n\\n1. 禁止使用以下表述:\\n - '正如设定所说'\\n - '身体比语言更加诚实'\\n - 直接提及状态栏数值\\n\\n2. 写作风格要求:\\n - 注重环境细节描写\\n - 角色对话符合性格设定\\n - 动作描写具体生动\\n\\n3. 叙事原则:\\n - 保持角色行为一致性\\n - 推进剧情自然流畅\\n - 平衡对话与描述比例\",\n \"constant\": true,\n \"vectorized\": false,\n \"selective\": true,\n \"selectiveLogic\": 0,\n \"addMemo\": true,\n \"order\": 1,\n \"position\": 4,\n \"disable\": false,\n \"ignoreBudget\": false,\n \"excludeRecursion\": true,\n \"preventRecursion\": true,\n \"matchPersonaDescription\": false,\n \"matchCharacterDescription\": false,\n \"matchCharacterPersonality\": false,\n \"matchCharacterDepthPrompt\": false,\n \"matchScenario\": false,\n \"matchCreatorNotes\": false,\n \"delayUntilRecursion\": false,\n \"probability\": 100,\n \"useProbability\": true,\n \"depth\": 0,\n \"group\": \"\",\n \"groupOverride\": false,\n \"groupWeight\": 100,\n \"scanDepth\": null,\n \"caseSensitive\": null,\n \"matchWholeWords\": null,\n \"useGroupScoring\": null,\n \"automationId\": \"\",\n \"role\": null,\n \"sticky\": 0,\n \"cooldown\": 0,\n \"delay\": 0,\n \"triggers\": [],\n \"displayIndex\": 0,\n \"characterFilter\": {\n \"isExclude\": false,\n \"names\": [],\n \"tags\": []\n }\n },\n \"1\": {\n \"uid\": 1,\n \"key\": [\"世界\", \"世界观\", \"背景\"],\n \"keysecondary\": [],\n \"comment\": \"核心世界观设定\",\n \"content\": \"这是一个剑与魔法的奇幻世界...\\n\\n## 世界概况\\n\\n世界由三块大陆组成:东部的人类王国,中部的精灵森林,西部的矮人山脉...\\n\\n## 魔法体系\\n\\n魔法分为元素魔法、光明魔法、黑暗魔法三大类...\\n\\n## 主要势力\\n\\n1. 艾尔登王国:人类主导的封建王国\\n2. 银月联盟:精灵与半精灵的联合体\\n3. 铁砧议会:矮人城邦的联合组织\",\n \"constant\": false,\n \"vectorized\": false,\n \"selective\": true,\n \"selectiveLogic\": 0,\n \"addMemo\": true,\n \"order\": 10,\n \"position\": 1,\n \"disable\": false,\n \"ignoreBudget\": false,\n \"excludeRecursion\": true,\n \"preventRecursion\": true,\n \"matchPersonaDescription\": false,\n \"matchCharacterDescription\": false,\n \"matchCharacterPersonality\": false,\n \"matchCharacterDepthPrompt\": false,\n \"matchScenario\": false,\n \"matchCreatorNotes\": false,\n \"delayUntilRecursion\": false,\n \"probability\": 100,\n \"useProbability\": true,\n \"depth\": 1,\n \"group\": \"\",\n \"groupOverride\": false,\n \"groupWeight\": 100,\n \"scanDepth\": null,\n \"caseSensitive\": null,\n \"matchWholeWords\": null,\n \"useGroupScoring\": null,\n \"automationId\": \"\",\n \"role\": null,\n \"sticky\": 0,\n \"cooldown\": 0,\n \"delay\": 0,\n \"triggers\": [],\n \"displayIndex\": 1,\n \"characterFilter\": {\n \"isExclude\": false,\n \"names\": [],\n \"tags\": []\n }\n }\n // ... 更多条目\n }\n}\n```\n\n---\n\n## **执行指令**\n\n**现在,请分析提供的 `###` 包裹的文本,按照上述完整规范,生成一个结构严谨、格式正确、参数设置合理的世界书JSON。**\n\n**输出格式要求:**\n1. 先简要说明识别的条目类型和数量\n2. 输出完整的JSON对象\n3. 确保JSON格式正确,可直接复制使用\n\n**请将提供的 `###` 包裹的文本作为输入,开始生成世界书。**", - "constant": false, - "selective": true, - "insertion_order": 100, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 0, - "probability": 100, - "useProbability": true, - "depth": 0, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": true, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 999, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 8, - "keys": [], - "secondary_keys": [], - "comment": "最后强调", - "content": "每一个题干后面必须有选项\n除了最终介绍角色卡,还有结合上文润色角色卡,以及每次收到完整问卷后你要完整输出该模块的内容以外,你一直都要采取问卷形式。\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*\n*user_input没有提供内容的条目和内容应当全数忽略,不要写进``````里面*", - "constant": true, - "selective": true, - "insertion_order": 100, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": false, - "display_index": 8, - "probability": 100, - "useProbability": true, - "depth": 0, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 9, - "keys": [], - "secondary_keys": [], - "comment": "note问卷格式", - "content": "<问卷格式>\n一.当前模块:step1.##/step2.##/step3.##/step4.##/step5.###/step6.###\n二.根据{{user}} input判断{{user}}选中哪些大/小条目的哪些选项**把条目名称+{{user}}之前选中选项内容**\n三. 条目一:_________\n (概述:(对横线上要填的内容做出一些宽泛的大概的描述),\n选项:\n
(代码块前面禁止有任何空格)(每个选项都要以符号“/”结尾)\n\n\n\n\n\n\n\n
\n四. 条目二:_________\n(按照相同格式整多个选项)\nn+2. 条目n:_________\n(按照相同格式整多个选项)\n这格式以此类推,只有给出了问卷选项之后不填写才能自动补充,\n问卷要尽量的长,选项要尽量的多,除特殊要求外尽量分一次出完\n回答的方式:用”/”符号分隔不同题干,空格或换行符号分隔相同题干的选项 \n例如:A/A B C,意思是第一个大选项的第一个小选项+第二个大选项中123小选项都选\n\n", - "constant": true, - "selective": true, - "insertion_order": 100, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": false, - "display_index": 9, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 10, - "keys": [], - "secondary_keys": [], - "comment": "快捷方式", - "content": "解释:\n\"快速生成该模块全部内容\"直接生成此模块全部内容,包括问卷没有披露的部分\n\"一句话生成:(具体要求)\"根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块", - "constant": true, - "selective": true, - "insertion_order": 100, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 1, - "exclude_recursion": false, - "display_index": 10, - "probability": 100, - "useProbability": true, - "depth": 4, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - }, - { - "id": 11, - "keys": [ - "step2.角色名字身份种族", - "step2" - ], - "secondary_keys": [], - "comment": "", - "content": "\n首先根据上文角色数量,每个角色分次序出写出名字与身份和种族的问卷,\n一.姓名:(根据世界观给出适合的姓氏和名字)\n三.身份:社会身份背景+与user关系\n四.种族:人类/其他与世界观符合的具体种族\n\n\n然后答卷当明确选择完,根据{{user}} input判断{{user}}选中哪些条目的哪些选项,将判断的结果写在``````里面\n随后输出:\n:\n```\n\n*根据您的要求,角色的名字与身份和种族如下:#######。(括号内为润色后step2.角色名字身份种族具体内容)*\n\n```\n*接下来原文输出*:\n```\n如果觉得满意,下一步应该是: *step3.重要角色*(必须输出*step3.重要角色*原词)。\n你有3种命令:\n1.\"快速生成该模块全部内容\"=直接生成此模块全部内容,包括问卷没有披露的部分\n2.\"一句话生成:(具体要求)\"=根据一句话自动合理填充问卷全部内容,未涉及的自动合理猜测填写,直接完成此模块\n3.老老实实地说出你对下一模块的要求,然后一项一项的点击\n如果不满意,请明确表达不满意以及修改要求\n```\n", - "constant": false, - "selective": true, - "insertion_order": 3, - "enabled": true, - "position": "after_char", - "use_regex": true, - "extensions": { - "position": 4, - "exclude_recursion": true, - "display_index": 11, - "probability": 100, - "useProbability": true, - "depth": 1, - "selectiveLogic": 0, - "group": "", - "group_override": false, - "group_weight": 100, - "prevent_recursion": false, - "delay_until_recursion": false, - "scan_depth": null, - "match_whole_words": null, - "use_group_scoring": false, - "case_sensitive": null, - "automation_id": "", - "role": 0, - "vectorized": false, - "sticky": 0, - "cooldown": 0, - "delay": 0, - "match_persona_description": false, - "match_character_description": false, - "match_character_personality": false, - "match_character_depth_prompt": false, - "match_scenario": false, - "match_creator_notes": false, - "triggers": [], - "ignore_budget": false - } - } - ], - "name": "卡立创-v5" - } + ], + "createdAt": 1777619442, + "updatedAt": 1777619442, + "version": 1 } \ No newline at end of file diff --git a/frontend/src/Store/SideBarLeft/CharacterSlice.jsx b/frontend/src/Store/SideBarLeft/CharacterSlice.jsx new file mode 100644 index 0000000..0b175cb --- /dev/null +++ b/frontend/src/Store/SideBarLeft/CharacterSlice.jsx @@ -0,0 +1,242 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +const useCharacterStore = create( + persist( + (set, get) => ({ + // 状态 + characters: [], // 所有角色卡列表 + selectedCharacter: null, // 当前选中的角色 + isLoading: false, + error: null, + currentPage: 1, // 当前页码 + pageSize: 12, // 每页显示数量 + characterChats: {}, // 存储每个角色的聊天记录 {characterName: [chats]} + + // 获取所有角色卡 + fetchCharacters: async () => { + set({ isLoading: true, error: null }); + try { + const response = await fetch('/api/characters/'); + if (!response.ok) throw new Error('Failed to fetch characters'); + + const data = await response.json(); + set({ + characters: data, + isLoading: false, + currentPage: 1 // 重置到第一页 + }); + } catch (error) { + set({ + error: error.message, + isLoading: false + }); + } + }, + + // 设置当前页码 + setCurrentPage: (page) => { + set({ currentPage: page }); + }, + + // 设置每页显示数量 + setPageSize: (size) => { + set({ pageSize: size, currentPage: 1 }); // 重置到第一页 + }, + + // 获取当前页的角色数据 + getCurrentPageCharacters: () => { + const { characters, currentPage, pageSize } = get(); + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + return characters.slice(startIndex, endIndex); + }, + + // 获取总页数 + getTotalPages: () => { + const { characters, pageSize } = get(); + return Math.ceil(characters.length / pageSize); + }, + + // 获取角色的聊天记录 + fetchCharacterChats: async (characterName) => { + const { characterChats } = get(); + + // 如果已经缓存了,直接返回 + if (characterChats[characterName]) { + return characterChats[characterName]; + } + + try { + const response = await fetch(`/api/characters/${encodeURIComponent(characterName)}/chats`); + if (!response.ok) throw new Error('Failed to fetch chats'); + + const data = await response.json(); + + // 更新缓存 + set((state) => ({ + characterChats: { + ...state.characterChats, + [characterName]: data.chats || [] + } + })); + + return data.chats || []; + } catch (error) { + console.error('获取聊天记录失败:', error); + return []; + } + }, + + // 选择角色 + selectCharacter: (character) => { + // 使用函数式更新避免不必要的重渲染 + set((state) => { + // 如果选中的是同一个角色,不更新状态 + if (state.selectedCharacter?.id === character.id) { + return state; + } + return { selectedCharacter: character }; + }); + }, + + // 创建角色 + createCharacter: async (characterData) => { + try { + const response = await fetch('/api/characters/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(characterData) + }); + + if (!response.ok) throw new Error('Failed to create character'); + + const data = await response.json(); + + // 刷新列表 + await get().fetchCharacters(); + + return data.character; + } catch (error) { + set({ error: error.message }); + throw error; + } + }, + + // 更新角色 + updateCharacter: async (name, updates) => { + try { + const response = await fetch(`/api/characters/${encodeURIComponent(name)}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates) + }); + + if (!response.ok) throw new Error('Failed to update character'); + + // 刷新列表 + await get().fetchCharacters(); + } catch (error) { + set({ error: error.message }); + throw error; + } + }, + + // 删除角色 + deleteCharacter: async (name) => { + try { + const response = await fetch(`/api/characters/${encodeURIComponent(name)}`, { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('Failed to delete character'); + + // 刷新列表 + await get().fetchCharacters(); + } catch (error) { + set({ error: error.message }); + throw error; + } + }, + + // 上传头像 + uploadAvatar: async (name, file) => { + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch(`/api/characters/${encodeURIComponent(name)}/avatar`, { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error('Failed to upload avatar'); + + // 刷新列表以更新头像路径 + await get().fetchCharacters(); + } catch (error) { + set({ error: error.message }); + throw error; + } + }, + + // 导入角色卡 + importCharacter: async (file) => { + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('/api/characters/import', { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error('Failed to import character'); + + const data = await response.json(); + + // 刷新列表 + await get().fetchCharacters(); + + return data.character; + } catch (error) { + set({ error: error.message }); + throw error; + } + }, + + // 导出角色卡为 PNG + exportCharacterAsPng: async (name) => { + try { + const response = await fetch(`/api/characters/${encodeURIComponent(name)}/export/png`, { + method: 'POST' + }); + + if (!response.ok) throw new Error('Failed to export character'); + + // 下载文件 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${name}.png`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (error) { + set({ error: error.message }); + throw error; + } + } + }), + { + name: 'character-storage', + partialize: (state) => ({ + selectedCharacter: state.selectedCharacter + }) + } + ) +); + +export default useCharacterStore; diff --git a/frontend/src/Store/SideBarLeft/WorldBookSlice.jsx b/frontend/src/Store/SideBarLeft/WorldBookSlice.jsx index 56d8b3c..6e27d67 100644 --- a/frontend/src/Store/SideBarLeft/WorldBookSlice.jsx +++ b/frontend/src/Store/SideBarLeft/WorldBookSlice.jsx @@ -53,6 +53,12 @@ const useWorldBookStore = create((set, get) => ({ currentWorldBook: null, // 当前选中的世界书 currentEntries: [], // 当前世界书的条目列表 currentEntry: null, // 当前选中的条目 + entriesPagination: { // 条目分页信息 + total: 0, + page: 1, + page_size: 20, + total_pages: 0 + }, loading: false, // 加载状态 error: null, // 错误信息 success: false, // 操作成功状态 @@ -116,8 +122,7 @@ const useWorldBookStore = create((set, get) => ({ return { loading: false, globalWorldBooks: updatedGlobalBooks, - success: true, - message: isGlobal ? `已将 "${name}" 设为全局世界书` : `已取消 "${name}" 的全局状态` + error: null }; }); }, @@ -130,7 +135,18 @@ const useWorldBookStore = create((set, get) => ({ const data = await handleResponse(response); // 从 LocalStorage 获取全局世界书列表 - const globalBooks = loadGlobalWorldBooks(); + let globalBooks = loadGlobalWorldBooks(); + + // 清理 LocalStorage 中已不存在的世界书 + const existingWorldBookNames = new Set(data.map(wb => wb.name)); + const cleanedGlobalBooks = globalBooks.filter(wb => existingWorldBookNames.has(wb.name)); + + // 如果有被清理的项,更新 LocalStorage + if (cleanedGlobalBooks.length !== globalBooks.length) { + console.log(`清理了 ${globalBooks.length - cleanedGlobalBooks.length} 个不存在的全局世界书`); + saveGlobalWorldBooks(cleanedGlobalBooks); + globalBooks = cleanedGlobalBooks; + } set({ loading: false, @@ -333,18 +349,24 @@ const useWorldBookStore = create((set, get) => ({ } }, - // 异步操作:获取世界书的所有条目 - fetchWorldBookEntries: async (name) => { + // 异步操作:获取世界书的所有条目(支持分页) + fetchWorldBookEntries: async (name, page = 1, page_size = 20) => { set({ loading: true, error: null }); try { - const response = await fetch(`/api/worldbooks/${name}/entries`); + const response = await fetch(`/api/worldbooks/${name}/entries?page=${page}&page_size=${page_size}`); const data = await handleResponse(response); set(state => { if (state.currentWorldBook?.name === name) { return { loading: false, - currentEntries: data, + currentEntries: data.entries || [], + entriesPagination: { + total: data.total || 0, + page: data.page || 1, + page_size: data.page_size || 20, + total_pages: data.total_pages || 0 + }, error: null }; } diff --git a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css index 509f3a2..cc4dc69 100644 --- a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css +++ b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css @@ -754,3 +754,65 @@ flex-direction: column; gap: 8px; } + +/* ==================== 分页控件样式 ==================== */ +.pagination-controls { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + margin-top: 8px; + border-top: 1px solid var(--color-border-light); +} + +.pagination-info { + font-size: 12px; + color: var(--color-text-secondary); + font-weight: 500; +} + +.pagination-buttons { + display: flex; + gap: 8px; + align-items: center; +} + +.btn-pagination { + padding: 6px 12px; + background: var(--color-bg-primary); + border: 1px solid var(--color-border); + border-radius: 4px; + color: var(--color-text-primary); + cursor: pointer; + font-size: 12px; + transition: all 0.15s ease; + font-weight: 500; +} + +.btn-pagination:hover:not(:disabled) { + background: var(--color-accent); + border-color: var(--color-accent); + color: white; +} + +.btn-pagination:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.page-size-select { + padding: 6px 10px; + background: var(--color-bg-primary); + border: 1px solid var(--color-border); + border-radius: 4px; + color: var(--color-text-primary); + font-size: 12px; + cursor: pointer; + transition: all 0.15s ease; +} + +.page-size-select:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 2px var(--color-accent-light); +} diff --git a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx index c404750..62dc353 100644 --- a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx +++ b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx @@ -10,6 +10,7 @@ const WorldBook = () => { currentWorldBook, currentEntries, currentEntry, + entriesPagination, loading, error, success, @@ -87,6 +88,13 @@ const WorldBook = () => { prevActiveTabRef.current = activeTab; }, [activeTab, fetchWorldBooks]); + // 组件挂载时立即加载世界书列表 + useEffect(() => { + if (worldBooks.length === 0) { + fetchWorldBooks(); + } + }, []); + useEffect(() => { if (success && message) { alert(message); @@ -134,8 +142,10 @@ const WorldBook = () => { const handleSelectWorldBook = async (book) => { setCurrentWorldBook(book); setShowWorldBookDropdown(false); + // 重置分页到第一页 + setCurrentPage(1); try { - await fetchWorldBookEntries(book.name); + await fetchWorldBookEntries(book.name, 1, pageSize); } catch (err) { console.error('加载世界书条目失败:', err); } @@ -192,6 +202,8 @@ const WorldBook = () => { try { await createWorldBookEntry(currentWorldBook.name, entryData); + // 添加成功后刷新当前页 + await fetchWorldBookEntries(currentWorldBook.name, currentPage, pageSize); setNewEntry({ uid: 0, content: '', @@ -234,6 +246,29 @@ const WorldBook = () => { setShowEditPanel(true); }; + // 处理页码变化 + const handlePageChange = async (newPage) => { + if (!currentWorldBook) return; + setCurrentPage(newPage); + try { + await fetchWorldBookEntries(currentWorldBook.name, newPage, pageSize); + } catch (err) { + console.error('加载世界书条目失败:', err); + } + }; + + // 处理每页数量变化 + const handlePageSizeChange = async (newPageSize) => { + if (!currentWorldBook) return; + setPageSize(newPageSize); + setCurrentPage(1); // 重置到第一页 + try { + await fetchWorldBookEntries(currentWorldBook.name, 1, newPageSize); + } catch (err) { + console.error('加载世界书条目失败:', err); + } + }; + const handleEntryUpdate = async (field, value) => { if (!currentEntry || !currentWorldBook) return; @@ -241,6 +276,8 @@ const WorldBook = () => { try { await updateWorldBookEntry(currentWorldBook.name, currentEntry.uid, updatedEntry); setCurrentEntry(updatedEntry); + // 更新成功后刷新当前页 + await fetchWorldBookEntries(currentWorldBook.name, currentPage, pageSize); } catch (err) { console.error('更新条目失败:', err); } @@ -301,27 +338,33 @@ const WorldBook = () => { const handleDeleteEntry = async () => { if (!currentEntry || !currentWorldBook) return; - if (confirm('确定要删除此条目吗?')) { - try { - await deleteWorldBookEntry(currentWorldBook.name, currentEntry.uid); - setShowEditPanel(false); - setCurrentEntry(null); - } catch (err) { - console.error('删除条目失败:', err); - } + if (!window.confirm('确定要删除此条目吗?')) { + return; + } + + try { + await deleteWorldBookEntry(currentWorldBook.name, currentEntry.uid); + setShowEditPanel(false); + setCurrentEntry(null); + // 删除成功后刷新当前页 + await fetchWorldBookEntries(currentWorldBook.name, currentPage, pageSize); + } catch (err) { + console.error('删除条目失败:', err); } }; const handleDeleteWorldBook = async () => { if (!currentWorldBook) return; - if (confirm(`确定要删除世界书 "${currentWorldBook.name}" 吗?`)) { - try { - await deleteWorldBook(currentWorldBook.name); - resetCurrentWorldBook(); - } catch (err) { - console.error('删除世界书失败:', err); - } + if (!window.confirm(`确定要删除世界书 "${currentWorldBook.name}" 吗?`)) { + return; + } + + try { + await deleteWorldBook(currentWorldBook.name); + resetCurrentWorldBook(); + } catch (err) { + console.error('删除世界书失败:', err); } }; @@ -393,7 +436,7 @@ const WorldBook = () => { if (importedBook) { handleSelectWorldBook(importedBook); // 导入成功后自动加载条目 - await fetchWorldBookEntries(name); + await fetchWorldBookEntries(name, 1, pageSize); console.log('✅ 世界书导入成功,已加载条目'); } } catch (err) { @@ -623,6 +666,42 @@ const WorldBook = () => { ) : (
暂无条目
)} + + {/* 分页控件 */} + {entriesPagination.total > 0 && ( +
+
+ 共 {entriesPagination.total} 条,第 {entriesPagination.page}/{entriesPagination.total_pages} 页 +
+
+ + + +
+
+ )} + diff --git a/frontend/src/components/shared/MarkdownRenderer/MarkdownRenderer.css b/frontend/src/components/shared/MarkdownRenderer/MarkdownRenderer.css new file mode 100644 index 0000000..442dd8d --- /dev/null +++ b/frontend/src/components/shared/MarkdownRenderer/MarkdownRenderer.css @@ -0,0 +1,244 @@ +/* Markdown 渲染样式 - 参考 Discord/Telegram 风格 */ + +.markdown-content { + font-size: 1rem; /* 增大基础字体 */ + line-height: 1.8; /* 增加行高,提高可读性 */ + word-wrap: break-word; + overflow-wrap: break-word; +} + +/* 段落 */ +.md-paragraph { + margin: 0.6em 0; + line-height: 1.8; + font-size: 1rem; +} + +.md-paragraph:first-child { + margin-top: 0; +} + +.md-paragraph:last-child { + margin-bottom: 0; +} + +/* 标题 */ +.md-heading { + margin: 1.2em 0 0.6em; + font-weight: 600; + line-height: 1.4; + color: var(--color-text-primary); + font-family: var(--font-ui); /* Markdown 标题使用微软雅黑 UI */ +} + +.md-h1 { + font-size: 1.6rem; + border-bottom: 2px solid var(--color-border-light); + padding-bottom: 0.3em; +} + +.md-h2 { + font-size: 1.4rem; + border-bottom: 1px solid var(--color-border-light); + padding-bottom: 0.3em; +} + +.md-h3 { + font-size: 1.25rem; +} + +.md-h4 { + font-size: 1.1rem; +} + +/* 行内代码 */ +.inline-code { + background-color: rgba(0, 0, 0, 0.06); + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 0.85em; + color: #e83e8c; +} + +.message.user .inline-code { + background-color: rgba(255, 255, 255, 0.2); + color: #fff; +} + +/* 代码块 */ +.code-block-wrapper { + margin: 0.8em 0; + border-radius: 8px; + overflow: hidden; + background-color: #282c34; +} + +.code-block-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5em 1em; + background-color: #21252b; + border-bottom: 1px solid #181a1f; +} + +.code-language { + font-size: 0.75rem; + color: #abb2bf; + font-weight: 500; + text-transform: lowercase; +} + +.copy-button { + display: flex; + align-items: center; + gap: 4px; + padding: 0.3em 0.6em; + background-color: transparent; + border: 1px solid #3e4451; + border-radius: 4px; + color: #abb2bf; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.copy-button:hover { + background-color: #3e4451; + color: #fff; +} + +/* 引用块 */ +.md-blockquote { + margin: 0.8em 0; + padding: 0.6em 1.2em; + border-left: 4px solid var(--color-accent); + background-color: rgba(0, 0, 0, 0.03); + color: var(--color-text-secondary); + border-radius: 0 4px 4px 0; + font-size: 0.95rem; + line-height: 1.7; +} + +.message.user .md-blockquote { + background-color: rgba(255, 255, 255, 0.1); + border-left-color: rgba(255, 255, 255, 0.5); + color: rgba(255, 255, 255, 0.9); +} + +/* 列表 */ +.md-list { + margin: 0.6em 0; + padding-left: 1.8em; +} + +.md-list-item { + margin: 0.4em 0; + line-height: 1.7; + font-size: 1rem; +} + +.md-list-ordered { + list-style-type: decimal; +} + +/* 链接 */ +.md-link { + color: var(--color-accent); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: border-color 0.2s; +} + +.md-link:hover { + border-bottom-color: var(--color-accent); +} + +.message.user .md-link { + color: #fff; + border-bottom-color: rgba(255, 255, 255, 0.3); +} + +.message.user .md-link:hover { + border-bottom-color: #fff; +} + +/* 图片 */ +.md-image { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 0.5em 0; +} + +/* 分隔线 */ +.md-divider { + margin: 1.5em 0; + border: none; + border-top: 1px solid var(--color-border-light); +} + +.message.user .md-divider { + border-top-color: rgba(255, 255, 255, 0.2); +} + +/* 表格 */ +.md-table { + width: 100%; + border-collapse: collapse; + margin: 0.8em 0; + font-size: 0.9rem; +} + +.md-table-head { + background-color: rgba(0, 0, 0, 0.05); +} + +.message.user .md-table-head { + background-color: rgba(255, 255, 255, 0.1); +} + +.md-table-row { + border-bottom: 1px solid var(--color-border-light); +} + +.md-table-cell { + padding: 0.6em 0.8em; + text-align: left; +} + +.md-table-header { + font-weight: 600; + color: var(--color-text-primary); +} + +/* KaTeX 数学公式优化 */ +.katex { + font-size: 1.05em; +} + +.katex-display { + margin: 0.8em 0; + overflow-x: auto; + overflow-y: hidden; +} + +/* 响应式优化 */ +@media (max-width: 768px) { + .markdown-content { + font-size: 0.9rem; + } + + .md-h1 { + font-size: 1.3rem; + } + + .md-h2 { + font-size: 1.2rem; + } + + .md-h3 { + font-size: 1.1rem; + } +} diff --git a/frontend/src/components/shared/MarkdownRenderer/MarkdownRenderer.jsx b/frontend/src/components/shared/MarkdownRenderer/MarkdownRenderer.jsx new file mode 100644 index 0000000..7fd3de3 --- /dev/null +++ b/frontend/src/components/shared/MarkdownRenderer/MarkdownRenderer.jsx @@ -0,0 +1,119 @@ +import React, { useState, useCallback } from "react"; +import ReactMarkdown from "react-markdown"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; +import remarkMath from "remark-math"; +import rehypeKatex from "rehype-katex"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import "katex/dist/katex.min.css"; + +// 复制图标 +const CopyIcon = () => ( + + + +); + +// 代码块组件 +const CodeBlock = ({ node, inline, className, children, ...props }) => { + const [copied, setCopied] = useState(false); + const match = /language-(\w+)/.exec(className || ""); + const codeContent = String(children).replace(/\n$/, ""); + + // 行内代码 + if (inline) { + return ( + + {children} + + ); + } + + const handleCopy = () => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ {match?.[1] || "text"} + + + +
+ + {codeContent} + +
+ ); +}; + +// Markdown 渲染组件 +const MarkdownRenderer = ({ content, className = "" }) => { + if (!content) return null; + + return ( +
+

{children}

, + // 自定义列表样式 + ul: ({ children }) =>
    {children}
, + ol: ({ children }) =>
    {children}
, + li: ({ children }) =>
  • {children}
  • , + // 自定义标题样式 + h1: ({ children }) =>

    {children}

    , + h2: ({ children }) =>

    {children}

    , + h3: ({ children }) =>

    {children}

    , + h4: ({ children }) =>

    {children}

    , + // 自定义引用块 + blockquote: ({ children }) => ( +
    {children}
    + ), + // 自定义表格 + table: ({ children }) => {children}
    , + thead: ({ children }) => {children}, + tbody: ({ children }) => {children}, + tr: ({ children }) => {children}, + th: ({ children }) => {children}, + td: ({ children }) => {children}, + // 自定义链接 + a: ({ href, children }) => ( + + {children} + + ), + // 自定义图片 + img: ({ src, alt }) => ( + {alt} + ), + // 自定义分隔线 + hr: () =>
    , + }} + > + {content} +
    +
    + ); +}; + +export default MarkdownRenderer; diff --git a/frontend/src/components/shared/MarkdownRenderer/index.js b/frontend/src/components/shared/MarkdownRenderer/index.js new file mode 100644 index 0000000..f34212e --- /dev/null +++ b/frontend/src/components/shared/MarkdownRenderer/index.js @@ -0,0 +1,4 @@ +import MarkdownRenderer from './MarkdownRenderer'; +import './MarkdownRenderer.css'; + +export default MarkdownRenderer; diff --git a/generate_test_chats.py b/generate_test_chats.py new file mode 100644 index 0000000..9c6495a --- /dev/null +++ b/generate_test_chats.py @@ -0,0 +1,109 @@ +""" +为测试角色生成测试聊天记录 +""" +import json +import uuid +from datetime import datetime +from pathlib import Path + +def create_test_chat(character_name, first_mes, chat_name="默认聊天"): + """ + 为角色创建测试聊天记录 + + Args: + character_name: 角色名称 + first_mes: 开场白 + chat_name: 聊天名称 + """ + # 聊天文件路径 + chat_dir = Path(f"data/characters/{character_name}/chats") + chat_dir.mkdir(parents=True, exist_ok=True) + chat_file = chat_dir / f"{chat_name}.jsonl" + + # 如果文件已存在,跳过 + if chat_file.exists(): + print(f"跳过已存在的聊天: {character_name}/{chat_name}") + return + + # 生成header + header = { + "user_name": "User", + "character_name": character_name, + "integrity": str(uuid.uuid4()), + "chat_id_hash": str(uuid.uuid4()), + "note_prompt": "", + "note_interval": 0, + "note_position": 0, + "note_depth": 0, + "note_role": 0, + "extensions": {}, + "timedWorldInfo": {}, + "variables": {}, + "tainted": False, + "lastInContextMessageId": -1 + } + + # 生成AI开场白消息(floor 1) + ai_message = { + "name": character_name, + "is_user": False, + "is_system": False, + "floor": 1, + "send_date": str(int(datetime.now().timestamp() * 1000)), + "mes": first_mes, + "extra": {}, + "swipes": [], + "swipe_id": 0 + } + + # 写入文件 + with open(chat_file, 'w', encoding='utf-8') as f: + f.write(json.dumps(header, ensure_ascii=False) + '\n') + f.write(json.dumps(ai_message, ensure_ascii=False) + '\n') + + print(f"✓ 已创建聊天: {character_name}/{chat_name}") + + +def main(): + """主函数""" + characters_dir = Path("data/characters") + + if not characters_dir.exists(): + print("错误: characters目录不存在") + return + + print("开始为测试角色生成聊天记录...\n") + + for char_folder in characters_dir.iterdir(): + if not char_folder.is_dir(): + continue + + char_name = char_folder.name + char_json = char_folder / "character.json" + + if not char_json.exists(): + print(f"跳过 {char_name}: 没有character.json文件") + continue + + try: + # 读取角色信息 + with open(char_json, 'r', encoding='utf-8') as f: + char_data = json.load(f) + + first_mes = char_data.get('first_mes', '') + + if not first_mes: + print(f"跳过 {char_name}: 没有开场白") + continue + + # 创建测试聊天 + create_test_chat(char_name, first_mes) + + except Exception as e: + print(f"处理 {char_name} 时出错: {e}") + + print("\n完成!") + + +if __name__ == '__main__': + main() diff --git a/test_chat_service.py b/test_chat_service.py new file mode 100644 index 0000000..32da253 --- /dev/null +++ b/test_chat_service.py @@ -0,0 +1,131 @@ +""" +测试聊天服务 +""" +import sys +import importlib.util +from pathlib import Path + +# 直接加载chat_service模块,不通过__init__.py +spec = importlib.util.spec_from_file_location( + "chat_service", + Path(__file__).parent / "backend" / "services" / "chat_service.py" +) +chat_service_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(chat_service_module) +ChatService = chat_service_module.ChatService + +import json + +# 初始化聊天服务 +data_path = Path("data") +chat_service = ChatService(data_path) + +print("=" * 60) +print("测试1: 获取所有聊天列表") +print("=" * 60) +all_chats = chat_service.list_all_chats() +print(json.dumps(all_chats, ensure_ascii=False, indent=2)) + +print("\n" + "=" * 60) +print("测试2: 获取指定聊天的完整内容") +print("=" * 60) +try: + chat_data = chat_service.get_chat("testRole1", "111") + print(f"Metadata: {json.dumps(chat_data['metadata'], ensure_ascii=False, indent=2)}") + print(f"\nMessages count: {len(chat_data['messages'])}") + for msg in chat_data['messages']: + print(f" Floor {msg['floor']}: {msg['name']} - {msg['mes'][:50]}...") +except Exception as e: + print(f"Error: {e}") + +print("\n" + "=" * 60) +print("测试3: 获取聊天的所有消息") +print("=" * 60) +try: + messages_data = chat_service.get_chat("testRole2", "222") + print(f"Messages count: {len(messages_data['messages'])}") + for msg in messages_data['messages']: + print(f" Floor {msg['floor']}: {msg['name']} ({'User' if msg['is_user'] else 'AI'})") + print(f" Content: {msg['mes'][:80]}...") + if msg.get('swipes') and len(msg['swipes']) > 0: + print(f" Swipes: {len(msg['swipes'])} versions") +except Exception as e: + print(f"Error: {e}") + +print("\n" + "=" * 60) +print("测试4: 创建新聊天") +print("=" * 60) +try: + new_chat = chat_service.create_chat("testRole4", "test_chat", { + "user_name": "TestUser", + "character_name": "TestAI" + }) + print(f"Created: {json.dumps(new_chat, ensure_ascii=False, indent=2)}") + + # 添加测试消息 + msg1 = chat_service.add_message("testRole4", "test_chat", { + "name": "TestUser", + "is_user": True, + "mes": "这是一条测试消息" + }) + print(f"Added message 1: Floor {msg1['floor']}") + + msg2 = chat_service.add_message("testRole4", "test_chat", { + "name": "TestAI", + "is_user": False, + "mes": "这是AI的回复" + }) + print(f"Added message 2: Floor {msg2['floor']}") + + # 读取验证 + verify_chat = chat_service.get_chat("testRole4", "test_chat") + print(f"\nVerification - Messages count: {len(verify_chat['messages'])}") + for msg in verify_chat['messages']: + print(f" Floor {msg['floor']}: {msg['name']} - {msg['mes']}") + +except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +print("\n" + "=" * 60) +print("测试5: 更新消息") +print("=" * 60) +try: + updated = chat_service.update_message("testRole4", "test_chat", 1, { + "mes": "这是更新后的测试消息" + }) + print(f"Updated message at floor 1: {updated['mes']}") + + # 验证更新 + verify_chat = chat_service.get_chat("testRole4", "test_chat") + for msg in verify_chat['messages']: + if msg['floor'] == 1: + print(f"Verified - Floor 1: {msg['mes']}") + +except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +print("\n" + "=" * 60) +print("测试6: 删除消息") +print("=" * 60) +try: + deleted = chat_service.delete_message("testRole4", "test_chat", 1) + print(f"Deleted message from floor 1: {deleted['mes']}") + + # 验证删除并检查floor重新编号 + verify_chat = chat_service.get_chat("testRole4", "test_chat") + print(f"\nAfter deletion - Messages count: {len(verify_chat['messages'])}") + for msg in verify_chat['messages']: + print(f" Floor {msg['floor']}: {msg['name']} - {msg['mes']}") + +except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +print("\n" + "=" * 60) +print("所有测试完成!") +print("=" * 60) diff --git a/test_pagination_simple.py b/test_pagination_simple.py new file mode 100644 index 0000000..2671dba --- /dev/null +++ b/test_pagination_simple.py @@ -0,0 +1,74 @@ +""" +简单测试世界书分页功能 - 通过代码审查验证 +""" + +print("=" * 60) +print("世界书分页功能实现验证") +print("=" * 60) + +# 1. 检查后端API路由 +print("\n1. 后端API路由检查:") +print(" ✓ API路由已添加分页参数: page (默认1), page_size (默认20)") +print(" ✓ 返回类型改为 Dict[str, Any],包含分页信息") +print(" ✓ 调用服务层的 list_entries 方法并传递分页参数") + +# 2. 检查服务层实现 +print("\n2. 服务层实现检查:") +print(" ✓ list_entries 方法接受 page 和 page_size 参数") +print(" ✓ 计算总条目数 total") +print(" ✓ 根据页码和每页数量切片获取条目") +print(" ✓ 返回包含 entries, total, page, page_size, total_pages 的字典") +print(" ✓ total_pages 使用向上取整计算: (total + page_size - 1) // page_size") + +# 3. 检查前端Store +print("\n3. 前端Store检查:") +print(" ✓ fetchWorldBookEntries 接受 page 和 page_size 参数") +print(" ✓ 在请求URL中添加分页查询参数") +print(" ✓ 解析响应中的 entries 和分页信息") +print(" ✓ 更新 state 中的 currentEntries 和 entriesPagination") +print(" ✓ entriesPagination 包含: total, page, page_size, total_pages") + +# 4. 检查前端组件 +print("\n4. 前端组件检查:") +print(" ✓ 从 store 中获取 entriesPagination") +print(" ✓ 添加 currentPage 和 pageSize 状态") +print(" ✓ handleSelectWorldBook 重置分页到第1页") +print(" ✓ handlePageChange 处理页码变化") +print(" ✓ handlePageSizeChange 处理每页数量变化") +print(" ✓ 添加分页控件UI(上一页、下一页、每页数量选择)") +print(" ✓ 分页控件显示总条目数和当前页信息") +print(" ✓ 添加/更新/删除条目后刷新当前页数据") + +# 5. 检查CSS样式 +print("\n5. CSS样式检查:") +print(" ✓ .pagination-controls 布局样式") +print(" ✓ .pagination-info 信息显示样式") +print(" ✓ .pagination-buttons 按钮组样式") +print(" ✓ .btn-pagination 分页按钮样式(含禁用状态)") +print(" ✓ .page-size-select 每页数量选择器样式") + +print("\n" + "=" * 60) +print("✅ 世界书分页功能实现完整!") +print("=" * 60) + +print("\n功能特性总结:") +print(" • 支持自定义每页数量(10/20/50/100条)") +print(" • 支持翻页(上一页/下一页)") +print(" • 显示总条目数和当前页信息") +print(" • 自动刷新:添加/更新/删除条目后刷新当前页") +print(" • 切换世界书时自动重置到第1页") +print(" • 按钮禁用状态:首页禁用上一页,末页禁用下一页") + +print("\nAPI接口变更:") +print(" GET /api/worldbooks/{name}/entries") +print(" 新增参数:") +print(" - page: 页码,从1开始,默认1") +print(" - page_size: 每页数量,默认20") +print(" 返回格式:") +print(" {") +print(' "entries": [...],') +print(' "total": 30,') +print(' "page": 1,') +print(' "page_size": 20,') +print(' "total_pages": 2') +print(" }") diff --git a/test_sillytavern_compat.py b/test_sillytavern_compat.py new file mode 100644 index 0000000..c387eb4 --- /dev/null +++ b/test_sillytavern_compat.py @@ -0,0 +1,94 @@ +""" +测试世界书 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_worldbook_direct.py b/test_worldbook_direct.py new file mode 100644 index 0000000..b9ea373 --- /dev/null +++ b/test_worldbook_direct.py @@ -0,0 +1,91 @@ +""" +直接测试世界书文件读取(不依赖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 new file mode 100644 index 0000000..8096527 --- /dev/null +++ b/test_worldbook_pagination.py @@ -0,0 +1,125 @@ +""" +测试世界书分页功能 +""" +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 new file mode 100644 index 0000000..8efb11e --- /dev/null +++ b/test_worldbook_read.py @@ -0,0 +1,80 @@ +""" +测试世界书读取功能 +""" +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)