23 KiB
23 KiB
这是一个需要长期演进、但方向已经比较清晰的重构计划。为了让“云酒馆”既能承载多用户公共场景,又能高度兼容 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()工具
- 单一 Store,明确区分
- 用户系统:登录 / 注册 / 用户资料页已接入后端
- 角色卡管理:角色列表 / 上传 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 列表(可选)
- 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 / promptOnlysubstituteRegex / minDepth / maxDepthscope (global | character | preset)+ownerCharId / ownerPresetIdorder(执行顺序)
- 已与 ST 字段对齐:
- 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 后端完成以下完整流程:
- 加载上下文:
- 根据 conversationId 加载:
AIChat、AICharacter、AIPreset、AIConfig、绑定的AIWorldInfo与RegexScript
- 根据 conversationId 加载:
- 输入正则处理(placement = input):
- 对用户输入文本先跑一遍 RegexScript(global + character + preset)
- 写入 user message 到数据库
- 世界书扫描(World Info Engine):
- 在最近 N 条消息 + 角色描述 / 场景 等组成的扫描文本上,套用 ST 的 world-info 算法:
- 主 / 副关键词匹配,支持
use_regex、caseSensitive、matchWholeWords selectiveLogic控制主 / 副键组合逻辑depth、sticky、cooldown、delay、probability等控制触发频次与时间窗
- 主 / 副关键词匹配,支持
- 在最近 N 条消息 + 角色描述 / 场景 等组成的扫描文本上,套用 ST 的 world-info 算法:
- Prompt 构建:
- system 部分:角色 system + scenario + 作者注 + preset.systemPrompt + world info
- history 部分:最近若干 user/assistant 消息
- 预设附加 prompt:按 depth / position / order 注入其他内容
- 模型调用:
- 根据
AIProvider/AIModel//app/ai-config配置,调用相应厂商 API(OpenAI 兼容 / Anthropic 等) - 支持 SSE 流式输出,将 token 流推送给前端
- 根据
- 输出正则处理(placement = output):
- 对完整 AI 输出再次执行 RegexScript(global + character + preset)
- 写入 assistant message 到数据库
- 返回前端:
- 返回最终完整消息内容;若使用 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
- 官方 Demo 角色 / 预设,可以
- 中间件:
- 在 Gin 中实现统一鉴权中间件,从 JWT 中解析
user_id - 在 Service 层封装“自动附带
user_id条件”的查询方法,避免重复书写WHERE user_id = ?
- 在 Gin 中实现统一鉴权中间件,从 JWT 中解析
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
- 后台脚本扫描 ST
- 前端:
- 新建
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等参数
- 非常复杂,包含:
- 云酒馆建议做法:
- 从以下来源收集“激活世界书列表”:
- 用户全局启用:例如用户偏好中的
activeWorldInfoIds - 角色绑定:
AICharacter上的世界书绑定字段 - 会话级设置:
AIChat.Settings.activeWorldInfoIds
- 用户全局启用:例如用户偏好中的
- 遍历所有激活世界书的 entries:
- 跳过
disabled - 若
constant = true,无视关键词匹配直接候选(仍受 sticky / cooldown / delay / probability 影响) - 根据
use_regex/caseSensitive/matchWholeWords对keys/secondary_keys做匹配 - 按
selectiveLogic结合主副键匹配结果 - 应用
depth/sticky/cooldown/delay/probability等控制触发频次
- 跳过
- 将激活的 entries 按
order/position规则,注入到 Prompt 中对应位置
- 从以下来源收集“激活世界书列表”:
- 现实建议:
- 为保证兼容性,尽量参考 / 复刻 ST
world-info.js的排序和筛选逻辑,而不是“重写一套简化版”。
- 为保证兼容性,尽量参考 / 复刻 ST
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= ModelAppActions= Update- React 组件 = View
- 使用
persist与devtools扩展,变量系统内置在variables字段中
- 建议:
- 将以下信息也纳入 Store,而不是零散地用本地
useState:- 当前会话使用的
presetId - 当前会话启用的世界书 ID 列表
- 当前会话启用的正则脚本 ID 列表
- 当前会话使用的
- 通过一个统一的
ConversationSettings结构,与后端AIChat.Settings对应
- 将以下信息也纳入 Store,而不是零散地用本地
5.2 角色卡管理页面(CharacterManagePage)
目标:前端字段与 ST V2/V3 完全对齐,为后端世界书 / 正则拆分做好数据基础。
- 字段映射建议:
firstMes↔first_mesmesExample↔mes_examplesystemPrompt↔system_promptpostHistoryInstructions↔post_history_instructionscharacterBook↔character_bookextensions↔extensions
- 世界书编辑 UI:
- 在现有
keys / content / enabled / position基础上,逐步引入高级字段(折叠到“高级设置”):secondary_keys、constant、use_regex、case_sensitive、match_whole_wordsselective、selective_logicdepth、order、probability、sticky、cooldown、delay、group
- 保存时将上述字段完整写入
characterBook.entries中,后端再拆分到AIWorldInfo相关表
- 在现有
- 导入/导出:
- 前端不对 ST JSON 结构做“智能改写”,只做表单 ↔ JSON 的字段映射
- ST 兼容性由后端
utils/character_card.go+AICharacter来保证
5.3 预设管理页面(PresetManagePage)
目标:彻底从“前端内存预设”升级为“后端驱动的 ST 预设系统”。
- 实施步骤:
- 后端按规划实现
/app/preset的 CRUD + 导入/导出 - 前端新建
src/api/preset.ts,统一管理预设相关请求 - 将
PresetManagePage中的本地假数据与useState初始值删除,全部改为从 API 加载 - 预设 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)
- 当前会话使用的 preset(下拉列表来自
- 保存时调用
/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 流推送给前端
- 在 service 层实现统一的
- 前端:
- Chat 页面改造为消费 SSE 流(追加消息 / 局部更新)
- SettingsPanel / CharacterPanel 配置的 preset / 世界书 / 正则 与后端
AIChat.Settings打通
阶段 3:插件系统与高级特性(长期)
- 后端:
- 引入插件表与 Hook 定义(
onUserInput/onWorldInfoScan/beforePromptBuild/onAssistantDone) - 使用 goja / WASM 等方式实现安全的插件执行沙箱
- 增强审计、统计与限流机制
- 引入插件表与 Hook 定义(
- 前端:
- 插件管理页:安装 / 启用 / 配置 / 查看日志
- 调试视图:展示某次回复中有哪些 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 侧应兼容多种文本块,保证最大兼容性
- ST 将角色卡 JSON 植入 PNG 文本 chunk(如
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)
这是兼容性的灵魂。 实现原理:
- 基础宏:
{{user}},{{char}},{{description}}等。 - 动态变量: 用户通过
/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. 开发注意事项 (踩坑预警)
- 角色卡解析: ST 的 PNG 角色卡数据存放在 PNG 的
iTXt块中。Go 语言需要使用image/png包并手动读取这些 Metadata 块。不要只支持 JSON,因为大部分玩家的资源都是 PNG。 - Lorebook (世界书) 逻辑:
- 这是最难兼容的部分。它涉及:关键字扫描、递归深度、插入位置、插入顺序。
- 建议: 直接复刻 ST 的
world-info.js中的排序算法。如果这部分算法不一致,AI 的表现会与原版 ST 大相径庭。
- 计算 Token: ST 使用
Tiktoken或特定模型的 Tokenizer。为了节省服务器资源,建议在前端进行初次计算,后端在调用 API 前再次校准。 - 跨域与 API 代理: 公共平台需要处理大量大模型 API 的转发。Go 后端要实现一个高性能的 Proxy Layer,处理超时、重试以及不同厂商(OpenAI, Anthropic, Google)的协议转换。