🎨 添加文档和测试用例

This commit is contained in:
2026-03-13 21:51:42 +08:00
parent cec46cabbe
commit 5520dafb56
12 changed files with 10248 additions and 1 deletions

458
docs/重构文档.md Normal file
View File

@@ -0,0 +1,458 @@
这是一个需要长期演进、但方向已经比较清晰的重构计划。为了让“云酒馆”既能承载多用户公共场景,又能高度兼容 SillyTavern以下简称 **ST**)生态,本方案结合当前代码与 `st/` 源码,对整体架构和落地路径进行整理与校准。
---
## 1. 总体架构与设计原则
### 1.1 技术栈与目录结构
- **前端**React 18 + TypeScript + Tailwind CSS + Zustand
- 代码目录:`web-app/`
- **后端**Go + Gin + GORM + pgvector
- 代码目录:`server/`
- **数据库**PostgreSQL 15+(大量使用 JSONB部分向量字段使用 pgvector
- **文件存储**S3 兼容MinIO / OSS 等),用于 PNG 角色卡 / 头像等
- **ST 源码**`st/` 目录,用作兼容参考与数据样例
### 1.2 云版 vs 本地版的根本差异
- **ST 当前形态**
- 本地应用,配置与数据以文件形式存在 `default/content/**``data/**`
- 单机 / 小规模用户,无严格多租户和权限隔离
- **云酒馆目标形态**
- **数据库优先**:所有业务数据(用户、角色、预设、世界书、对话、正则脚本等)统一落到 PostgreSQL
- **多租户隔离**:所有核心表带 `user_id`,中间件强制 `WHERE user_id = ?`
- **前后端分层清晰**
- 后端 → 唯一“真相来源”SSOT负责 Prompt pipeline、世界书触发、正则处理、变量替换、AI 调用
- 前端 → 纯 UI 壳:负责编辑与展示,不再私自保存“内存版世界书/预设”
---
## 2. 当前系统现状(结合现有代码)
### 2.1 后端(`server/`
`server/model/app/README.md` 以及各模型文件来看,当前状态大致为:
- **用户与会话**(已完成)
- 模型:`AppUser``AppUserSession`
- JWT 登录 / 刷新 / 注销 / 用户资料修改等已具备
- **角色卡管理AICharacter**(已完成)
- 模型:`AICharacter`(使用 JSONB 存 ST V2 数据,如 `CardData` / `Tags` 等)
- 支持 PNG / JSON 角色卡导入、角色列表、详情、编辑、删除、导出
- **预设管理AIPreset**模型已就绪API 待补齐)
- 模型:`AIPreset`,已拆分采样参数字段 + `Extensions JSONB`
- **世界书AIWorldInfo与正则脚本RegexScript**(模型已存在,逻辑未完全落地)
- 模型:`AIWorldInfo`(世界书实体)
- 模型:`RegexScript`(完全对齐 ST 正则脚本字段)
- **对话与消息**(基础模型已存在)
- 模型:`AIChat``AIMessage``AIMessageSwipe`,用于会话与多候选
- **AI 配置与向量记忆**(已具备)
- 模型:`AIProvider``AIModel``AIMemoryVector`
> 小结后端数据模型已经为「ST 兼容 + 云平台」打好了基础,但 Prompt pipeline、世界书触发引擎、正则处理引擎等运行时逻辑仍需集中落地。
### 2.2 前端(`web-app/`
- **状态管理Zustand**`src/store/index.ts`
- 单一 Store明确区分 `Model` / `Actions` / `Selectors`
- 内置变量系统 `variables: { user, char, ... }``substituteVariables()` 工具
- **用户系统**:登录 / 注册 / 用户资料页已接入后端
- **角色卡管理**:角色列表 / 上传 PNG/JSON / 编辑 / 导出已接入 `/app/character`
- **预设管理**:有 UI 骨架,但部分仍依赖前端假数据,需要接入真实 `/app/preset`
- **聊天界面**`ChatPage` + `ChatArea` + `MessageContent`
- 布局与基本会话切换已完成
- 近期已接入:
- `regexEngine.ts`:前端正则脚本执行引擎
- `textRenderer.ts`:文本渲染管线(解析 `<maintext>` / `<Status_block>` / choices 等)
- `ChatArea.tsx` 中从后端加载 `RegexScript` 列表,将用户 / 角色变量与脚本一并传入 `MessageContent`
> 小结:前端已具备 MVU 风格的状态管理与 ST 风格文本渲染能力,下一步是把预设 / 世界书 / 正则选择与后端 pipeline 串联起来。
---
## 3. 目标架构SillyTavern 兼容实现)
### 3.1 核心领域模型(后端视角)
基于当前 `server/model/app`,目标是让以下模型整体表达 ST 的角色卡 / 世界书 / 正则 / 预设 / 对话体系:
- **AICharacter**
- 存 ST 角色卡 V2/V3 标准化版本(结构化字段)+ 原始 JSON 存档(用于无损导出)
- 预留:
- `raw_card_json JSONB`(若尚未添加,可在后续迁移中补上)
- `bound_worldinfo_ids`:角色绑定世界书 ID 列表(可选)
- `bound_regex_ids`:角色绑定正则脚本 ID 列表(可选)
- **AIWorldInfoWorldbook & WorldbookEntry**
- 支持:
- 角色内世界书(挂在某个角色下)
- 用户级 / 全局世界书(多个角色共享)
- 字段需覆盖 ST 世界书的完整配置:
- `keys / secondary_keys / comment / content / constant / disabled / use_regex / case_sensitive / match_whole_words / selective / selective_logic / position / depth / order / probability / sticky / cooldown / delay / group / 额外 JSON`
- **RegexScript**
- 已与 ST 字段对齐:
- `findRegex / replaceWith / trimStrings(JSONB)`
- `placement`(输入 / 输出 / 世界书 / 显示等阶段)
- `disabled / markdownOnly / runOnEdit / promptOnly`
- `substituteRegex / minDepth / maxDepth`
- `scope (global | character | preset)` + `ownerCharId / ownerPresetId`
- `order`(执行顺序)
- **AIPreset + 绑定关系**
- `AIPreset` 存采样参数 + systemPrompt + 其他配置
- 预留 `PresetPrompt` / `PresetRegexBinding` 等概念,用于:
- 描述 prompt 列表(不同位置、深度)
- 绑定正则脚本到特定预设
- **AIChat / AIMessage**
- 对话Conversation与消息Message
-`AICharacter``AIPreset`、AI 配置等建立关联
- `AIChat.Settings JSONB` 存储会话级设置:
- 当前 presetId
- 活动世界书 ID 列表
- 活动正则脚本 ID 列表
### 3.2 运行时 Prompt Pipeline目标形态
所有客户端(目前只有 React Web未来可能有更多统一通过 Go 后端完成以下完整流程:
1. **加载上下文**
- 根据 conversationId 加载:`AIChat``AICharacter``AIPreset`、AIConfig、绑定的 `AIWorldInfo``RegexScript`
2. **输入正则处理placement = input**
- 对用户输入文本先跑一遍 RegexScriptglobal + character + preset
3. **写入 user message** 到数据库
4. **世界书扫描World Info Engine**
- 在最近 N 条消息 + 角色描述 / 场景 等组成的扫描文本上,套用 ST 的 world-info 算法:
- 主 / 副关键词匹配,支持 `use_regex``caseSensitive``matchWholeWords`
- `selectiveLogic` 控制主 / 副键组合逻辑
- `depth``sticky``cooldown``delay``probability` 等控制触发频次与时间窗
5. **Prompt 构建**
- system 部分:角色 system + scenario + 作者注 + preset.systemPrompt + world info
- history 部分:最近若干 user/assistant 消息
- 预设附加 prompt按 depth / position / order 注入其他内容
6. **模型调用**
- 根据 `AIProvider` / `AIModel` / `/app/ai-config` 配置,调用相应厂商 APIOpenAI 兼容 / Anthropic 等)
- 支持 SSE 流式输出,将 token 流推送给前端
7. **输出正则处理placement = output**
- 对完整 AI 输出再次执行 RegexScriptglobal + character + preset
8. **写入 assistant message** 到数据库
9. **返回前端**
- 返回最终完整消息内容;若使用 SSE还需流式推送增量 token
---
## 4. 关键子系统的详细设计
### 4.1 用户与多租户隔离
- 所有“用户私有”数据表必须带 `user_id`
- 角色:`ai_characters.user_id`
- 预设:`ai_presets.user_id`
- 世界书:`ai_world_info.user_id`
- 正则脚本:`regex_scripts.user_id`
- 对话 / 消息:`ai_chats.user_id``ai_messages.user_id`
- **共有资源**
- 官方 Demo 角色 / 预设,可以 `user_id = 0``NULL`
- **中间件**
- 在 Gin 中实现统一鉴权中间件,从 JWT 中解析 `user_id`
- 在 Service 层封装“自动附带 `user_id` 条件”的查询方法,避免重复书写 `WHERE user_id = ?`
### 4.2 预设系统AIPreset与 ST 兼容
- **ST 现状**
- 预设分散在 `st/default/content/presets/**` 的 JSON 文件中,按 API 类型区分(`context` / `instruct` / `openai` / `textgen` 等)
- 前端 `public/scripts/preset-manager.js` 在内存管理这些预设
- **云酒馆设计**
- 使用 `AIPreset` 表存储统一预设:
- 常见采样参数拆为字段(`Temperature/TopP/TopK/...`
- 复杂或厂商特定配置放入 `Extensions jsonb`
- 导入工具:
- 后台脚本扫描 ST `presets` 目录 → 解析 JSON → 映射为 `AIPreset`
- 前端:
- 新建 `src/api/preset.ts` 封装 `/app/preset` 系列接口
- `PresetManagePage` 完全改为“后端驱动”(删除假数据 `useState`
### 4.3 变量与宏系统Variables & Macros
> 这一块需要区分“现状”与“目标”。
- **ST 当前做法(前端)**
- `public/scripts/variables.js`:维护局部 / 全局变量(基于 `chat_metadata``extension_settings`
- `public/scripts/macros.js` + `scripts/macros/**`:处理 `{{user}} / {{char}} / {{random}} / {{pick:...}}` 等宏
- 宏替换发生在前端构造 prompt 的阶段
- **云酒馆现状(前端)**
- `web-app/src/store/index.ts`
- `variables: Record<string, string>``user` / `char`
- `substituteVariables(text, customVars?)` 支持时间宏、随机数、`pick`
- `web-app/src/lib/textRenderer.ts` 中也实现了一套独立的变量替换逻辑
- **目标设计(后端统一化)**
- 中短期可以继续在前端做变量替换(已实现),减少一次性改动量
- 中长期建议:
- 后端增加 `user_variables` 表,存 `(user_id, key, value)`
- 在 Prompt pipeline 最后一步由后端统一执行递归变量 / 宏替换(直到不再出现 `{{...}}`
- 文档中要清楚标注:**“后端统一替换变量”是重构目标,而非当前 ST 现状,也不是当前实现状态。**
### 4.4 世界书引擎World Info Engine
- **ST 实现(真实代码)**`st/public/scripts/world-info.js`
- 非常复杂,包含:
- 多种扫描策略(按 persona / description / scenario / creatorNotes 等)
- 关键字与正则混用(`use_regex`
- 主键 / 副键组合逻辑(`selectiveLogic`
- `depth` / `sticky` / `cooldown` / `delay` / `probability` / `position` / `group` 等参数
- **云酒馆建议做法**
1. 从以下来源收集“激活世界书列表”:
- 用户全局启用:例如用户偏好中的 `activeWorldInfoIds`
- 角色绑定:`AICharacter` 上的世界书绑定字段
- 会话级设置:`AIChat.Settings.activeWorldInfoIds`
2. 遍历所有激活世界书的 entries
- 跳过 `disabled`
-`constant = true`,无视关键词匹配直接候选(仍受 sticky / cooldown / delay / probability 影响)
- 根据 `use_regex` / `caseSensitive` / `matchWholeWords``keys` / `secondary_keys` 做匹配
-`selectiveLogic` 结合主副键匹配结果
- 应用 `depth` / `sticky` / `cooldown` / `delay` / `probability` 等控制触发频次
3. 将激活的 entries 按 `order` / `position` 规则,注入到 Prompt 中对应位置
- **现实建议**
- 为保证兼容性,尽量参考 / 复刻 ST `world-info.js` 的排序和筛选逻辑,而不是“重写一套简化版”。
### 4.5 正则脚本引擎Regex Scripts
- **后端模型**`RegexScript`(已实现,字段与 ST 对齐)
- **前端执行(显示层)**
- `web-app/src/lib/regexEngine.ts`
- `applyRegexScripts(text, placement, scripts, depth?)`
- `processAIOutput` / `processUserInput` 等便捷函数
- 支持 `/pattern/flags`、捕获组 `$1/$2/...``{{match}}``trimStrings`
- `ChatArea.tsx`:加载 `/api/regex` 列表,过滤掉 `disabled`,传给 `MessageContent`
- **后端执行Prompt 层)**
- 输入阶段:用户文本入库 / 调用模型前,对应 ST 的“input placement”
- 输出阶段:模型输出保存 / 返回前,对应 ST 的“output placement”
- 世界书阶段:若需要在构造 world info 前后也跑一次 Regex可以利用 `placement` 细分
---
## 5. 前端重构与集成方案(`web-app/`
### 5.1 MVU 状态管理Zustand
- **现状**
- `src/store/index.ts` 已以 MVU 思路实现:
- `AppState` = Model
- `AppActions` = Update
- React 组件 = View
- 使用 `persist``devtools` 扩展,变量系统内置在 `variables` 字段中
- **建议**
- 将以下信息也纳入 Store而不是零散地用本地 `useState`
- 当前会话使用的 `presetId`
- 当前会话启用的世界书 ID 列表
- 当前会话启用的正则脚本 ID 列表
- 通过一个统一的 `ConversationSettings` 结构,与后端 `AIChat.Settings` 对应
### 5.2 角色卡管理页面CharacterManagePage
目标:前端字段与 ST V2/V3 完全对齐,为后端世界书 / 正则拆分做好数据基础。
- **字段映射建议**
- `firstMes``first_mes`
- `mesExample``mes_example`
- `systemPrompt``system_prompt`
- `postHistoryInstructions``post_history_instructions`
- `characterBook``character_book`
- `extensions``extensions`
- **世界书编辑 UI**
- 在现有 `keys / content / enabled / position` 基础上,逐步引入高级字段(折叠到“高级设置”):
- `secondary_keys``constant``use_regex``case_sensitive``match_whole_words`
- `selective``selective_logic`
- `depth``order``probability``sticky``cooldown``delay``group`
- 保存时将上述字段完整写入 `characterBook.entries` 中,后端再拆分到 `AIWorldInfo` 相关表
- **导入/导出**
- 前端不对 ST JSON 结构做“智能改写”,只做表单 ↔ JSON 的字段映射
- ST 兼容性由后端 `utils/character_card.go` + `AICharacter` 来保证
### 5.3 预设管理页面PresetManagePage
目标:彻底从“前端内存预设”升级为“后端驱动的 ST 预设系统”。
- 实施步骤:
1. 后端按规划实现 `/app/preset` 的 CRUD + 导入/导出
2. 前端新建 `src/api/preset.ts`,统一管理预设相关请求
3.`PresetManagePage` 中的本地假数据与 `useState` 初始值删除,全部改为从 API 加载
4. 预设 JSON 字段对齐 ST
- 采样参数:`temperature/top_p/top_k/frequency_penalty/presence_penalty/...`
- Prompt 配置:`system_prompt/stop_sequences/...`
### 5.4 聊天与设置面板ChatPage / SettingsPanel / CharacterPanel
- **SettingsPanel 增强**
- 增加:
- 当前会话使用的 preset下拉列表来自 `/app/preset`
- 启用的世界书列表(来自 `/app/worldinfo`,初期可只展示角色内世界书)
- 启用的正则脚本列表(来自 `/app/regex`
- 保存时调用 `/app/chat/:id/settings`(或 `PUT /app/conversation/:id`)更新 `AIChat.Settings`
- **ChatArea & MessageContent 集成**
- `ChatArea.tsx`
- 已从 `useAppStore` 取出 `user``variables`
- 已加载 `RegexScript` 列表,并传给 `MessageContent`
- `MessageContent.tsx`
- 利用 `regexEngine` + `textRenderer` 完成:
- 正则处理AI 输出修饰)
- `<maintext>` / `<Status_block>` / choices 解析
- 变量替换 / HTML 转义 / 代码块提取 / XSS 防护
---
## 6. 分阶段落地路线图
### 阶段 1打通 CRUD 与数据流(短期)
- **后端**
- 巩固 `/app/character`(保证 ST V2/V3 导入/导出稳定)
- 实现 `/app/preset` 全套 CRUD + 导入/导出
- 补齐世界书 `/app/worldinfo` 与正则 `/app/regex` 接口(基于现有模型)
- **前端**
- `PresetManagePage` 完全接入 `/app/preset`
- 角色编辑页面补全世界书 entry 字段,保持与 ST 的字段对齐
### 阶段 2实现 Prompt Pipeline 与对话 API中期
- **后端**
- 在 service 层实现统一的 `chatPipeline`串接世界书、正则、变量、预设、AI 调用逻辑
- 统一对话入口,例如:`POST /app/chat/:id/messages``POST /app/conversation/:id/message`
- 支持 SSE 流式输出,将 token 流推送给前端
- **前端**
- Chat 页面改造为消费 SSE 流(追加消息 / 局部更新)
- SettingsPanel / CharacterPanel 配置的 preset / 世界书 / 正则 与后端 `AIChat.Settings` 打通
### 阶段 3插件系统与高级特性长期
- **后端**
- 引入插件表与 Hook 定义(`onUserInput` / `onWorldInfoScan` / `beforePromptBuild` / `onAssistantDone`
- 使用 goja / WASM 等方式实现安全的插件执行沙箱
- 增强审计、统计与限流机制
- **前端**
- 插件管理页:安装 / 启用 / 配置 / 查看日志
- 调试视图:展示某次回复中有哪些 world info / 正则 / 插件生效
---
## 7. 重要澄清与修正(相对早期文档)
- **关于 Zod**
- 当前 `st/` 源码并未使用 Zod而是通过 `TavernCardValidator` 等自写校验器验证角色卡结构
-`web-app` 中引入 Zod 进行前端 schema 校验是一个“改进方向”,不是 ST 现状
- **关于 MVU**
- ST 当前前端仍是 jQuery + 全局脚本,不是典型 MVU 架构
- MVU 已在云酒馆前端的 Zustand Store 中落地,应把 MVU 归于“云酒馆前端架构”,而非 ST 现状
- **关于变量 / 宏所在层级**
- ST 将变量与宏主要放在前端处理
- 云酒馆的目标是:短期兼容这种做法,中长期逐步迁移到后端统一替换
- **关于 PNG 块类型**
- ST 将角色卡 JSON 植入 PNG 文本 chunk`tEXt` / 自定义块),不必限定为 `iTXt`
- Go 侧应兼容多种文本块,保证最大兼容性
## 8. 核心模块开发方案
### 8.1 用户管理与数据隔离
**ST 现状**: 物理文件夹隔离(`public/users/admin/...`)。
**重构方案**:
* **数据库设计**: 所有表(`characters`, `chats`, `presets`, `lorebooks`)必须包含 `user_id` 字段。
* **权限控制**: 后端中间件统一拦截请求,从 JWT 中提取 `user_id`,并在所有 SQL 查询中强制加入 `WHERE user_id = ?` 约束。
* **隔离策略**:
* **共有资源**: 官方提供的默认预设、系统角色卡(`user_id` 为 0 或 NULL
* **私有资源**: 用户上传或创建的内容。
### 8.2 预设系统 (Presets) 兼容方案
**ST 现状**: 大量的 JSON 文件,分为 API、Instruct、Context 等类别。
**重构方案**:
* **存储**: 在 PostgreSQL 中创建 `presets` 表,核心配置字段设为 `jsonb` 类型。
* **导入逻辑**: 编写一个 Go 工具类,读取 ST 的 `presets` 文件夹,将 JSON 解析后存入数据库。
* **注意事项**: ST 的预设经常更新字段Go 的结构体定义要使用 `map[string]interface{}` 或冗余字段来保证在 ST 更新协议时,你的后端不会因为缺失字段而崩溃。
### 8.3 变量管理与宏引擎 (Variables & Macros)
这是兼容性的灵魂。
**实现原理**:
1. **基础宏**: `{{user}}`, `{{char}}`, `{{description}}` 等。
2. **动态变量**: 用户通过 `/setvar` 定义的变量。
**重构方案**:
* **后端处理**: 不要在前端替换变量,而是在后端构建 Prompt 的最后一刻进行正则替换。
* **变量表**: `user_variables` 表,存储 `(user_id, key, value)`
* **递归解析**: ST 支持嵌套宏,你的 Go 后端需要实现一个递归替换函数,直到字符串中不再包含 `{{...}}`
### 8.4 前端渲染与通信兼容 (Iframe/Sandbox)
**ST 现状**: jQuery 操作 DOM扩展直接注入 HTML。
**重构方案**:
* **渲染分离**: 使用 React 开发 UI。消息列表应支持 Markdown 解析(推荐使用 `react-markdown` 并配合插件)。
* **通信协议**:
* 前端 React 通过 **WebSocket****SSE** 与 Go 后端保持长连接,模拟 ST 的流式输出。
* 定义一套标准的 JSON API 响应格式,模仿 ST 的 `/api/...` 接口,以便未来的脚本迁移。
### 8.5 扩展系统 (Extensions) 的安全性重构
**ST 现状**: 动态加载 JS 脚本,安全性极低。
**重构方案 (公共平台推荐)**:
* **后端扩展**: 模仿 ST 的插件钩子,但在 Go 后端实现。例如,在发送 Prompt 前,调用一个“拦截器”逻辑。
* **前端扩展**: 如果要兼容 ST 的 JS 插件,**必须将其运行在沙箱Sandbox中**。
* 使用 `<iframe>` 隔离扩展 UI。
* 通过 `postMessage` 机制进行通信。
* **注意事项**: 绝不允许第三方扩展直接访问浏览器的 `localStorage` 或获取用户的 `user_token`
---
## 9. Zod 与 MVU 的重构实现
### 9.1 Zod 变量的作用与迁移
在 ST 源码中Zod 用于 **Runtime Schema Validation**
* **作用**: 当用户上传一个外部角色卡JSON 或 PNG 嵌入数据系统无法确定数据是否完整。Zod 会在前端校验数据格式,如果缺失关键字段,会提供默认值或报错。
* **重构建议**:
* **前端**: 继续在 React 中使用 `zod`。在处理角色卡导入逻辑时,定义与 ST 完全一致的 `CharacterSchema`
* **后端**: Go 端使用 `go-playground/validator` 或直接解析 `jsonb`。确保你的 Go 结构体Struct打上 `json:",omitempty"` 标签,以处理 ST 角色卡中不稳定的可选字段。
### 9.2 MVU (Model-View-Update) 的应用
* **作用**: ST 引入 MVU 是为了解决 jQuery 状态难以管理的问题。
* **重构建议**:
* 在 React 中MVU 就是 **Zustand/Redux + React Components**
* **Model**: Zustand 中的 `store`
* **Update**: Store 中的 `actions`
* **View**: React 组件。
* 你不需要在代码里写 `mvu` 变量名,只需遵循这种单向数据流即可实现比 ST 稳定得多的状态管理。
---
## 10. 开发注意事项 (踩坑预警)
1. **角色卡解析**: ST 的 PNG 角色卡数据存放在 PNG 的 `iTXt` 块中。Go 语言需要使用 `image/png` 包并手动读取这些 Metadata 块。不要只支持 JSON因为大部分玩家的资源都是 PNG。
2. **Lorebook (世界书) 逻辑**:
* 这是最难兼容的部分。它涉及:关键字扫描、递归深度、插入位置、插入顺序。
* **建议**: 直接复刻 ST 的 `world-info.js` 中的排序算法。如果这部分算法不一致AI 的表现会与原版 ST 大相径庭。
3. **计算 Token**: ST 使用 `Tiktoken` 或特定模型的 Tokenizer。为了节省服务器资源建议在前端进行初次计算后端在调用 API 前再次校准。
4. **跨域与 API 代理**: 公共平台需要处理大量大模型 API 的转发。Go 后端要实现一个高性能的 **Proxy Layer**处理超时、重试以及不同厂商OpenAI, Anthropic, Google的协议转换。