11 KiB
预设模块(Preset)导入与启用逻辑优化设计
一、背景与问题
当前系统支持从 SillyTavern / TavernAI 风格的 JSON 文件导入预设,并将其中的采样参数和提示词配置(prompts / prompt_order)存入后端 AIPreset.Extensions 字段中,前端在 PresetManagePage / EditPresetModal 中提供基本的编辑能力。
在实际使用中暴露出一个明显问题:
- 问题:导入预设后,提示词块(prompts)的启用 / 禁用状态与原始 JSON 文件不一致。
具体表现:
- SillyTavern 预设中的「默认启用 / 禁用」信息主要体现在:
prompts[*].enabled(整体默认开关,可能存在)prompt_order[*].order[*].enabled(更精细的 per-character 启用信息,是真正生效的一层)
- 我们当前前端 UI 在编辑时:
- 仅从
extensions.prompts中取出 prompts - 用
marker字段反向推导「启用」状态(checked={!prompt.marker}) - 完全没有解析和尊重
prompt_order里的enabled状态
- 仅从
因此,对于像 Sudachi.Next1.21.json 这种复杂预设:
- 文件中已经通过
prompt_order精确控制了某些块默认enabled: false - 导入后数据虽然保存在
extensions.prompt_order中,但前端不读取,导致 UI 展示的勾选状态与原始预设不一致
二、设计目标
- 忠实还原 ST 预设的启用 / 禁用行为
- 导入后,在不改动 ST 原始结构的前提下,让 UI 所见的启用状态尽可能与 ST 一致。
- 保持数据的完整与可扩展性
- 保留
prompts与prompt_order的原始结构,未来可以继续扩展更高级行为(多角色、多上下文)。
- 保留
- 简化前端编辑模型
- 前端在编辑时只需要关注:
- 哪些 prompt 当前“启用”
- 顺序如何
- 不强制实现 ST 全量复杂逻辑,但要保证基础行为正确。
- 前端在编辑时只需要关注:
- 向后兼容现有数据
- 对于已存在的
AIPreset,在没有prompt_order/enabled信息时,仍然能正常工作。
- 对于已存在的
三、数据结构与语义梳理
3.1 ST 预设结构(简化视图)
- 顶层采样 / 行为字段(已正确映射到
AIPreset)temperature,top_p,top_k,min_p,top_afrequency_penalty,presence_penalty,repetition_penaltyopenai_max_tokens,openai_max_context,stream_openai, 等
prompts: Prompt[]name: stringidentifier: string(如"main","nsfw","jailbreak"或 UUID)role: "system" | "user" | "assistant"content: stringsystem_prompt: booleanmarker: boolean(占位块;本身不一定等于“禁用”)enabled?: boolean(整体缺省启用的开关)injection_position: numberinjection_depth: numberinjection_order: numberinjection_trigger: any[]forbid_overrides: boolean
prompt_order: Array<{ character_id: number; order: Array<{ identifier: string; enabled: boolean }> }>- 按「角色 / 场景」维度,指定各 identifier 的启用状态与顺序
关键点:
真正决定「是否启用」的是 prompt_order[*].order[*].enabled,prompts[*].enabled 更多是全局默认值。
3.2 当前系统存储结构
后端 PresetService.ImportPresetFromJSON 当前的行为:
- 将
prompts/prompt_order原样塞入:
extensions := map[string]interface{}{
"prompts": stPreset.Prompts,
"prompt_order": stPreset.PromptOrder,
}
前端 EditPresetModal 当前的行为:
- 通过
preset.extensions.prompts取得prompts列表 - 「启用」复选框用的是:
checked={!prompt.marker}- 也就是:只要不是 marker,就视为启用
- 没有考虑:
prompt.enabledprompt_order[*].order[*].enabled
四、优化方案总览
整体策略:
- 后端保持“原样存储”原则,继续把 ST 的
prompts和prompt_order保存在extensions里,方便以后做高级兼容。 - 新增一层“系统视角配置”(推荐写在
extensions.stMapping内),用于:- 提取出我们当前真正要用到的“默认启用 / 禁用信息”
- 为前端提供简单直观的数据结构
- 前端 UI 只依赖这层映射来决定默认勾选状态、顺序等,避免直接耦合 ST 的复杂行为。
五、后端导入逻辑优化设计
5.1 导入时的启用状态归一化
在 PresetService.ImportPresetFromJSON 中,对 stPreset.Prompts 与 stPreset.PromptOrder 做一次归一化处理,得到:
map[string]bool:identifier→enabled(系统视角下的默认启用状态)
处理规则建议如下:
- 从
prompt_order中选出一个「主视角 character_id」- 优先策略:取
prompt_order中 第一个条目 的character_id(例如 Sudachi 文件中的100000)。 - 这样可理解为“该预设的主角 / 默认角色对应的一组配置”。
- 优先策略:取
- 针对该
character_id的order数组:- 遍历每一项
{ identifier, enabled } - 记入
enabledMap[identifier] = enabled
- 遍历每一项
- 对于
prompts中存在但在order中未出现的 identifier:- 若
prompt.enabled明确存在,则用该字段 - 否则默认
true(保持向后兼容,除非 ST 未来规范有特别说明)
- 若
归一化后的结果示意:
{
"stMapping": {
"mainCharacterId": 100000,
"enabledByIdentifier": {
"main": true,
"worldInfoBefore": true,
"enhanceDefinitions": false,
"nsfw": true,
...
}
}
}
注意:
- 原始的
prompts/prompt_order仍然原封不动地存放在extensions.prompts/extensions.prompt_order中。 stMapping只是我们系统内部使用的“视图”,不影响原始数据。
5.2 可选:为 prompts 注入统一的 enabled 字段
为了前端更好使用,也可以在导入时(或者在后端读取时)对 prompts 做一次增强:
- 对每个
prompt:- 查询
enabled := stMapping.enabledByIdentifier[prompt.identifier] - 如果找到了,就在该
prompt上写入 / 覆盖prompt["enabled"] = enabled
- 查询
这样,前端只要拿到 extensions.prompts,就能直接看到每条的 enabled 字段,不必额外关联 prompt_order。
兼容建议:
- 不删除原有的
enabled,只是统一成一个规范值;- 如果未来 ST 升级行为,我们仍保留原始 JSON,可以再次调整映射逻辑。
六、前端编辑行为优化设计
6.1 EditPresetModal 中的启用逻辑修改
当前逻辑:
- 通过
getPrompts()从extensions.prompts取出数组 - 复选框逻辑:
<input
type="checkbox"
checked={!prompt.marker}
onChange={(e) => handlePromptChange(index, 'marker', !e.target.checked)}
/>
问题:
- 把 “是否 marker” 当成了 “是否启用”,与 ST 语义不符。
优化方案:
- 新增对
enabled字段的支持- 解析时,优先使用:
prompt.enabled(如果存在)- 否则回退为
!prompt.marker(为了兼容老数据)
- 解析时,优先使用:
- UI 含义调整:
- 勾选框表示「此 prompt 是否启用」,而非「是否 marker」
marker仍然保留,用于区分“占位块”(如chatHistory、dialogueExamples)和“实际有内容的块”,但不再被直接当作启用条件。
示例(伪代码):
const isEnabled = prompt.enabled !== undefined
? prompt.enabled
: !prompt.marker
<input
type="checkbox"
checked={isEnabled}
onChange={(e) => handlePromptChange(index, 'enabled', e.target.checked)}
/>
- 保存时,将
enabled写回extensions.prompts中对应项。
提醒:
- 短期内可以不在前端去写
prompt_order,只要我们的对话生成逻辑是“按 prompts + enabled 过滤”就足够;- 如果未来要做到 ST 完整语义(如多角色不同顺序),再在前端提供更高级的顺序管理 UI。
6.2 默认显示顺序
当前 UI 在 EditPresetModal 里对 prompts 没有太多排序逻辑,直接用数组顺序:
- 初期优化阶段,可以继续沿用 ST 原始
prompts顺序 - 如果希望更贴近 ST 行为,可以:
- 根据
injection_position/injection_depth/injection_order做一个简易排序 - 或者读取
stMapping.mainCharacterId对应prompt_order的顺序,按其中 identifier 顺序排列
- 根据
建议分阶段:
- 第一阶段(快速修复启用逻辑)
- 先只修复
enabled使用,仍按原数组顺序展示
- 先只修复
- 第二阶段(顺序优化)
- 再根据
prompt_order/ injection 字段优化展示顺序
- 再根据
七、对话生成逻辑的使用建议(可选)
目前后端对话生成(Convesation Service)是如何使用预设参数,需要在代码中再具体确认。但从设计角度建议:
- 生成最终 prompt 时:
- 从
extensions.prompts中筛选:enabled != false的 prompt - 按既定顺序(可先用原始数组顺序,或更细致地用
injection_*/prompt_order)拼接成系统提示。
- 从
- 如需高度还原 ST 行为:
- 在后端增加一个专门的「ST Prompt 编译器」:
- 输入:
prompts+prompt_order+ 当前角色 / 会话上下文 - 输出:一串系统消息数组(包含角色、内容、插入位置信息)
- 输入:
- 这一步可以在后续迭代中实现,本次优化先以「启用状态正确」为目标。
- 在后端增加一个专门的「ST Prompt 编译器」:
八、兼容性与迁移策略
- 已有预设数据
- 已有的
AIPreset.Extensions中可能只有prompts,没有prompt_order/stMapping/enabled:EditPresetModal在解析时:- 若找不到
prompt.enabled,则默认enabled = !marker
- 若找不到
- 新导入的预设,将会有完整的
enabled/stMapping信息。
- 已有的
- 导出为 ST JSON
- 目前导出的逻辑是从
AIPreset再组装为 ST 样式的 JSON:- 建议导出时仍以
extensions.prompts/extensions.prompt_order为主 enabled字段如果已更新,也可以同步写回到导出 JSON 中对应位置,保持一致性
- 建议导出时仍以
- 目前导出的逻辑是从
- 前后端升级顺序
- 先在后端实现
stMapping和 prompts 的enabled归一化逻辑 - 再在前端切换到从
prompt.enabled读取启用状态 - 测试导入 Sudachi 预设后,界面上默认启用 / 禁用是否与原文件一致
- 先在后端实现
九、后续扩展方向(可选)
- 多角色 / 多场景支持
- 完整解析
prompt_order中所有character_id条目,根据当前对话绑定的角色 ID 切换不同启用方案。
- 完整解析
- 可视化 prompt 顺序编辑
- 在 Preset 管理页面增加「提示词顺序拖拽调整」、分组展示(角色定义 / 文风 / COT / 变量初始化等)。
- 预设版本管理与对比
- 由于 ST 预设经常迭代,可考虑为
AIPreset增加版本号 / 变更日志,以便回滚与对比。
- 由于 ST 预设经常迭代,可考虑为
以上设计以「最小代价修复启用/禁用逻辑不一致」为优先目标,同时为后续更深入兼容 ST 行为(如多角色、多 prompt_order 视图)预留空间。后续具体实现时,可以按本文件拆分为:
- 后端改动:
PresetService.ImportPresetFromJSON+ 可选的 Prompt 编译辅助函数 - 前端改动:
PresetManagePage/EditPresetModal中的 prompts 解析与复选框逻辑