这是一个需要长期演进、但方向已经比较清晰的重构计划。为了让“云酒馆”既能承载多用户公共场景,又能高度兼容 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`:文本渲染管线(解析 `` / `` / 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 列表(可选) - **AIWorldInfo(Worldbook & 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)**: - 对用户输入文本先跑一遍 RegexScript(global + 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` 配置,调用相应厂商 API(OpenAI 兼容 / Anthropic 等) - 支持 SSE 流式输出,将 token 流推送给前端 7. **输出正则处理(placement = output)**: - 对完整 AI 输出再次执行 RegexScript(global + 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` 存 `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 输出修饰) - `` / `` / 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)中**。 * 使用 `