268 lines
11 KiB
Markdown
268 lines
11 KiB
Markdown
## 预设模块(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
|
||
<input
|
||
type="checkbox"
|
||
checked={!prompt.marker}
|
||
onChange={(e) => 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
|
||
|
||
<input
|
||
type="checkbox"
|
||
checked={isEnabled}
|
||
onChange={(e) => 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 解析与复选框逻辑
|
||
|