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 }) => ,
+ ol: ({ children }) => {children}
,
+ li: ({ children }) => {children},
+ // 自定义标题样式
+ h1: ({ children }) => {children}
,
+ h2: ({ children }) => {children}
,
+ h3: ({ children }) => {children}
,
+ h4: ({ children }) => {children}
,
+ // 自定义引用块
+ blockquote: ({ children }) => (
+ {children}
+ ),
+ // 自定义表格
+ table: ({ children }) => ,
+ thead: ({ children }) => {children},
+ tbody: ({ children }) => {children},
+ tr: ({ children }) => {children}
,
+ th: ({ children }) => {children} | ,
+ td: ({ children }) => {children} | ,
+ // 自定义链接
+ a: ({ href, children }) => (
+
+ {children}
+
+ ),
+ // 自定义图片
+ img: ({ src, 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)