## 预设模块(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 展示的勾选状态与原始预设不一致 ### 二、设计目标 1. **忠实还原 ST 预设的启用 / 禁用行为** - 导入后,在不改动 ST 原始结构的前提下,让 UI 所见的启用状态尽可能与 ST 一致。 2. **保持数据的完整与可扩展性** - 保留 `prompts` 与 `prompt_order` 的原始结构,未来可以继续扩展更高级行为(多角色、多上下文)。 3. **简化前端编辑模型** - 前端在编辑时只需要关注: - 哪些 prompt 当前“启用” - 顺序如何 - 不强制实现 ST 全量复杂逻辑,但要保证基础行为正确。 4. **向后兼容现有数据** - 对于已存在的 `AIPreset`,在没有 `prompt_order` / `enabled` 信息时,仍然能正常工作。 ### 三、数据结构与语义梳理 #### 3.1 ST 预设结构(简化视图) - 顶层采样 / 行为字段(已正确映射到 `AIPreset`) - `temperature`, `top_p`, `top_k`, `min_p`, `top_a` - `frequency_penalty`, `presence_penalty`, `repetition_penalty` - `openai_max_tokens`, `openai_max_context`, `stream_openai`, 等 - `prompts: Prompt[]` - `name: string` - `identifier: string`(如 `"main"`, `"nsfw"`, `"jailbreak"` 或 UUID) - `role: "system" | "user" | "assistant"` - `content: string` - `system_prompt: boolean` - `marker: boolean`(占位块;本身不一定等于“禁用”) - `enabled?: boolean`(整体缺省启用的开关) - `injection_position: number` - `injection_depth: number` - `injection_order: number` - `injection_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` 原样塞入: ```go extensions := map[string]interface{}{ "prompts": stPreset.Prompts, "prompt_order": stPreset.PromptOrder, } ``` 前端 `EditPresetModal` 当前的行为: - 通过 `preset.extensions.prompts` 取得 `prompts` 列表 - 「启用」复选框用的是: - `checked={!prompt.marker}` - 也就是:只要不是 marker,就视为启用 - 没有考虑: - `prompt.enabled` - `prompt_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`(系统视角下的默认启用状态) 处理规则建议如下: 1. 从 `prompt_order` 中选出一个「主视角 character_id」 - 优先策略:取 `prompt_order` 中 **第一个条目** 的 `character_id`(例如 Sudachi 文件中的 `100000`)。 - 这样可理解为“该预设的主角 / 默认角色对应的一组配置”。 2. 针对该 `character_id` 的 `order` 数组: - 遍历每一项 `{ identifier, enabled }` - 记入 `enabledMap[identifier] = enabled` 3. 对于 `prompts` 中存在但在 `order` 中未出现的 identifier: - 若 `prompt.enabled` 明确存在,则用该字段 - 否则默认 `true`(保持向后兼容,除非 ST 未来规范有特别说明) 归一化后的结果示意: ```json { "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` 取出数组 - 复选框逻辑: ```tsx handlePromptChange(index, 'marker', !e.target.checked)} /> ``` 问题: - 把 “是否 marker” 当成了 “是否启用”,与 ST 语义不符。 优化方案: 1. 新增对 `enabled` 字段的支持 - 解析时,优先使用: - `prompt.enabled`(如果存在) - 否则回退为 `!prompt.marker`(为了兼容老数据) 2. UI 含义调整: - 勾选框表示「此 prompt 是否启用」,而非「是否 marker」 - `marker` 仍然保留,用于区分“占位块”(如 `chatHistory`、`dialogueExamples`)和“实际有内容的块”,但不再被直接当作启用条件。 示例(伪代码): ```tsx const isEnabled = prompt.enabled !== undefined ? prompt.enabled : !prompt.marker handlePromptChange(index, 'enabled', e.target.checked)} /> ``` 3. 保存时,将 `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 顺序排列 建议分阶段: 1. **第一阶段(快速修复启用逻辑)** - 先只修复 `enabled` 使用,仍按原数组顺序展示 2. **第二阶段(顺序优化)** - 再根据 `prompt_order` / injection 字段优化展示顺序 ### 七、对话生成逻辑的使用建议(可选) 目前后端对话生成(Convesation Service)是如何使用预设参数,需要在代码中再具体确认。但从设计角度建议: 1. 生成最终 prompt 时: - 从 `extensions.prompts` 中筛选:`enabled != false` 的 prompt - 按既定顺序(可先用原始数组顺序,或更细致地用 `injection_*` / `prompt_order`)拼接成系统提示。 2. 如需高度还原 ST 行为: - 在后端增加一个专门的「ST Prompt 编译器」: - 输入:`prompts` + `prompt_order` + 当前角色 / 会话上下文 - 输出:一串系统消息数组(包含角色、内容、插入位置信息) - 这一步可以在后续迭代中实现,本次优化先以「启用状态正确」为目标。 ### 八、兼容性与迁移策略 1. **已有预设数据** - 已有的 `AIPreset.Extensions` 中可能只有 `prompts`,没有 `prompt_order` / `stMapping` / `enabled`: - `EditPresetModal` 在解析时: - 若找不到 `prompt.enabled`,则默认 `enabled = !marker` - 新导入的预设,将会有完整的 `enabled` / `stMapping` 信息。 2. **导出为 ST JSON** - 目前导出的逻辑是从 `AIPreset` 再组装为 ST 样式的 JSON: - 建议导出时仍以 `extensions.prompts` / `extensions.prompt_order` 为主 - `enabled` 字段如果已更新,也可以同步写回到导出 JSON 中对应位置,保持一致性 3. **前后端升级顺序** - 先在后端实现 `stMapping` 和 prompts 的 `enabled` 归一化逻辑 - 再在前端切换到从 `prompt.enabled` 读取启用状态 - 测试导入 Sudachi 预设后,界面上默认启用 / 禁用是否与原文件一致 ### 九、后续扩展方向(可选) 1. **多角色 / 多场景支持** - 完整解析 `prompt_order` 中所有 `character_id` 条目,根据当前对话绑定的角色 ID 切换不同启用方案。 2. **可视化 prompt 顺序编辑** - 在 Preset 管理页面增加「提示词顺序拖拽调整」、分组展示(角色定义 / 文风 / COT / 变量初始化等)。 3. **预设版本管理与对比** - 由于 ST 预设经常迭代,可考虑为 `AIPreset` 增加版本号 / 变更日志,以便回滚与对比。 --- 以上设计以「最小代价修复启用/禁用逻辑不一致」为优先目标,同时为后续更深入兼容 ST 行为(如多角色、多 prompt_order 视图)预留空间。后续具体实现时,可以按本文件拆分为: - 后端改动:`PresetService.ImportPresetFromJSON` + 可选的 Prompt 编译辅助函数 - 前端改动:`PresetManagePage` / `EditPresetModal` 中的 prompts 解析与复选框逻辑