Files
tianshu-engine/docs/E18_VERSION_AND_DIFF_PROPOSAL.md

337 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# E18: 版本回滚 & AI 修改标记
## 概述
为编辑器的 AI 助手增加两个生产级功能:
1. **版本回滚** — AI 修改前自动存档,错误时可恢复到任意历史版本
2. **修改标记** — AI 修改后在 SceneGraph 节点上显示角标(新增/修改/删除),视觉可感知变更
---
## 一、版本回滚
### 流程
```
用户发起 AI 请求
→ buildMessage → POST /api/ai
→ AIPanel.send() 在调用 sendAIRequest 前:
editorStore.saveVersion("AI 修改前")
→ AI 修改完成 → reloadFromDisk() → gameData 刷新
用户发现改错了
→ AIPanel 版本下拉 → 选择历史版本
→ editorStore.restoreVersion(index)
→ gameData = version.gameData → autoSave()
→ SceneGraph + NodeEditor 重绘
```
### 涉及文件
| 文件 | 改动 |
|------|------|
| `editor/stores/editorStore.ts` | 新增 `versions` ref、`saveVersion(label)``restoreVersion(idx)` |
| `editor/db/editorDB.ts` | **新建** — IndexedDB 封装:`put`/`get`/`clear`,按 `sourcePath` 分组 |
| `editor/components/AIPanel.vue` | 新增版本下拉 UI在聊天区顶部send() 前调用 `saveVersion()` |
### 版本数据结构
```ts
interface EditorVersion {
id: string // crypto.randomUUID()
sourcePath: string // "/scenes/demo.json"
timestamp: number // Date.now()
label: string // "AI 修改前" / "初始版本"
gameData: GameData // 完整游戏数据快照
}
```
### IndexedDB 设计
```
数据库: editor_versions
表: versions
索引: sourcePath (查询同一文件的所有版本)
索引: timestamp (排序)
```
保留策略:同一 `sourcePath` 最多 20 个版本,超出删除最旧的。
### editorDB.ts API
```ts
// 保存版本
putVersion(v: EditorVersion): Promise<void>
// 获取某文件的所有版本(按时间倒序)
getVersions(sourcePath: string): Promise<EditorVersion[]>
// 清除某文件的所有版本(切换文件时)
clearVersions(sourcePath: string): Promise<void>
```
### 版本 UI 设计
```
AIPanel 顶部
┌──────────────────────┐
│ 历史版本: [AI 修改前 ▼] │ ← 下拉列表,默认隐藏
│ 2026-06-15 14:30 AI 修改前 │
│ 2026-06-15 14:28 初始版本 │ ← restoreVersion 后刷新全部
└──────────────────────┘
```
恢复后逻辑:
1. `gameData = version.gameData`
2. `autoSave()` 写回磁盘
3. `selectedNodeId = null`(重置选中)
4. AIPanel 聊天区显示 "已恢复到 'AI 修改前'"
5. 不清除该版本快照(用户可能想在不同版本间多次切换)
### saveVersion 触发时机
| 时机 | 标签 |
|------|------|
| AI 模式 `send()` 调用前(`reloadFromDisk` 之前) | `"AI 修改前"` |
| `App.vue` 首次加载 demo.json 后 | `"初始版本"` |
不自动在手动编辑时保存版本(用户手动点"保存版本"按钮即可,可选后续添加)。
---
## 二、AI 修改标记
### 流程
```
reloadFromDisk() 前:
记下 preGameData = gameData深拷贝
reloadFromDisk() 后:
diff = computeDiff(preGameData, newGameData)
aiChanges = { added, modified, deleted }
SceneGraph:
渲染时根据 aiChanges 给对应节点加角标
```
### 数据结构
```ts
interface AIDiff {
added: string[] // 新 gameData 中有,旧 gameData 中没有的 scene ID
modified: string[] // 相同 IDJSON.stringify 不同的 scene ID
deleted: string[] // 旧 gameData 中有,新 gameData 中没有的 scene ID
globalFields: string[] // 变更的全局配置字段名,例如 ["title", "assetBase"]
}
```
### diff 计算逻辑editorStore.reloadFromDisk 内)
```ts
const { scenes: oldScenes, ...oldGlobal } = oldGameData
const { scenes: newScenes, ...newGlobal } = newGameData
const diff: AIDiff = { added: [], modified: [], deleted: [], globalFields: [] }
for (const id of Object.keys(newScenes)) {
if (!oldScenes[id]) diff.added.push(id)
else if (JSON.stringify(oldScenes[id]) !== JSON.stringify(newScenes[id])) diff.modified.push(id)
}
for (const id of Object.keys(oldScenes)) {
if (!newScenes[id]) diff.deleted.push(id)
}
for (const key of Object.keys({ ...oldGlobal, ...newGlobal })) {
if (JSON.stringify(oldGlobal[key]) !== JSON.stringify(newGlobal[key])) {
diff.globalFields.push(key)
}
}
```
同时检测全局配置变更startScene、title 等),记录到 `globalFields`
### 角标视觉效果
Vue Flow 自定义节点模板SceneGraph.vue 已有 `CustomNode` 组件):
```
┌──────────────┐
│ ● NEW │ ← 绿色角标 (纯 CSS 色块)
│ scene_A │
└──────────────┘
┌──────────────┐
│ ● MOD │ ← 橙色角标
│ scene_B │
└──────────────┘
```
CSS 实现:在节点元素右上角添加绝对定位的 `<span class="diff-badge">`
### 全局配置变更标记
`aiChanges.globalFields` 非空时,在 `NodeEditor` 的"全局配置"标题区展示:
```
┌─────────────────────────────┐
│ 全局配置 ● 已修改 3 字段 │ ← 橙色圆点 + 变更字段数
│ ╭──────────────────╮ │
│ │ title: "xxx" │ │ ← 鼠标 hover 时弹出 tooltip
│ │ startScene: "yyy" │ │ 列出具体变更的字段名
│ │ assetBase: "zzz" │ │
│ ╰──────────────────╯ │
│ { │
│ "title": "xxx", ← 正常 JSON textarea 不变
│ ... │
└─────────────────────────────┘
```
**不变更项占位**:用逗号分隔字段名,如 `已修改 3 字段`hover 列出所有字段名。
**清除**:用户 blur JSON textarea 后标记消失(手工编辑即为最终确认)。
### 标记清除时机
| 条件 | 行为 |
|------|------|
| 用户手动编辑任意节点后 blur 保存 | 清除 `aiChanges`,所有标记消失 |
| 用户手动编辑全局配置后 blur 保存 | 清除全局配置标记 |
| AIPanel 点击"清除高亮"按钮 | 手动清除所有标记 |
| 新一轮 AI 修改 | 覆盖旧 diff新 diff 替换旧 diff |
### 涉及文件
| 文件 | 改动 |
|------|------|
| `editor/stores/editorStore.ts` | 新增 `aiChanges` ref、diff 计算逻辑、标记清除 |
| `editor/components/SceneGraph.vue` | watch `aiChanges`、自定义节点模板加角标 |
| `editor/components/NodeEditor.vue` | 全局配置标题加变更提示 |
---
## 三、完整交互时序
```
用户输入需求 "添加一个新场景 scene_battle"
AIPanel.send()
① saveVersion("AI 修改前") ← 快照
② preGameData = clone gameData ← 记下旧值
③ sendAIRequest(buildMessage(msg), ...)
opencode 读文件 → 新增 scene_battle → 写盘 → 回复 "已创建 scene_battle"
AIPanel 收到 result
④ 聊天显示 result
⑤ store.reloadFromDisk()
├─ fetch → 新 gameData
├─ diff(oldGameData, newGameData) → aiChanges = { added: ["scene_battle"], modified: [], deleted: [], globalFields: [] }
├─ gameData = newGameData
└─ selectedNodeId = null
SceneGraph 检测到 aiChanges 非空
→ 新增节点 scene_battle 渲染时显示绿色 "NEW" 角标
→ 已有节点无角标
用户手动编辑任一节点后 blur
→ store.clearAIMarkers()
→ 所有角标消失
```
---
## 四、改动清单
### editor/db/editorDB.ts新建
| API | 说明 |
|-----|------|
| `initDB()` | 打开/创建 `editor_versions` 数据库 |
| `putVersion(v)` | 存入版本快照 |
| `getVersions(sourcePath)` | 获取某文件所有版本(按时间倒序,最多 20 |
| `clearVersions(sourcePath)` | 清除某文件的版本(切换文件/删除旧版本时) |
### editor/stores/editorStore.ts
| 新增 | 说明 |
|------|------|
| `versions: Ref<EditorVersion[]>` | 当前文件的所有历史版本 |
| `aiChanges: Ref<AIDiff>` | 上次 AI 修改的 diff 结果 |
| `saveVersion(label)` | 深拷贝 gameData → 存 IndexedDB → 更新 versions |
| `restoreVersion(idx)` | gameData = versions[idx].gameData → autoSave |
| `reloadFromDisk()` 增强 | diff 计算 + 设置 aiChanges |
| `clearAIMarkers()` | aiChanges = null |
| `loadVersions()` | 加载当前 sourcePath 的版本列表 |
### editor/components/SceneGraph.vue
| 新增 | 说明 |
|------|------|
| `watch(aiChanges)` | 重新渲染时按 aiChanges 给节点加 badge |
| CSS `.diff-badge` / `.diff-badge-added` / `.diff-badge-modified` / `.diff-badge-deleted` | 角标样式 |
### editor/components/AIPanel.vue
| 新增 | 说明 |
|------|------|
| 版本下拉 UI | `<select v-model="selectedVersion">` + 恢复按钮 |
| `send()` 前调 `saveVersion("AI 修改前")` | 在 `loading = true` 前 |
| "清除高亮" 按钮 | 调用 `store.clearAIMarkers()` |
### editor/components/NodeEditor.vue
| 新增 | 说明 |
|------|------|
| 全局配置标题加标记 | `全局配置 ● 已修改 N 字段` + hover tooltip 显示字段名,当 `aiChanges.globalFields` 非空 |
| `onBlur()` 时调 `clearAIMarkers()` | 编辑后清除标记 |
---
## 五、非侵入性保证
| 项 | 保障 |
|----|------|
| IndexedDB 故障容忍 | try/catch 包裹所有操作,失败不影响主流程 |
| 版本存储增长 | 20 个上限 + 切换文件时清空旧文件的版本 |
| SceneGraph 性能 | diff 比较仅用 `Object.keys` + `JSON.stringify`,百级节点 < 2ms |
| 不影响手动编辑 | `clearAIMarkers()` blur 时调用角标不留存 |
| 不影响 Code 模式 | 仅在 JSON 模式 `reloadFromDisk()` 时做 diff |
| 不使用 emoji | CSS 纯色圆点 + 文字实现角标 |
---
## 六、测试用例
### V1版本自动保存
| 步骤 | 预期 |
|------|------|
| 编辑器中加载 demo.json | 自动创建版本 "初始版本" |
| 打开 AIPanelJSON 模式发送"添加场景" | 自动创建版本 "AI 修改前" |
| 打开版本下拉 | 显示两个版本 |
### V2版本恢复
| 步骤 | 预期 |
|------|------|
| AI 修改后选择 "AI 修改前" 版本 | 点击恢复 |
| 编辑器内容回到修改前 | SceneGraph + NodeEditor 刷新为旧内容 |
### V3修改标记
| 步骤 | 预期 |
|------|------|
| AI 新增 scene_battle | SceneGraph 中该节点显示绿色 "NEW" 角标 |
| AI 修改 scene_start | 该节点显示橙色 "MOD" 角标 |
| AI 修改 title + startScene | NodeEditor 全局配置标题显示 "● 已修改 2 字段"hover 列出 title/startScene |
| 用户手动编辑任一节点后 blur | 所有角标消失 |
| 用户手动编辑全局配置后 blur | 全局配置标记消失 |
### V4非侵入
| 步骤 | 预期 |
|------|------|
| 不通过 AI 直接手动编辑 JSON | 不触发版本保存不显示角标 |
| 浏览器不支持 IndexedDB | 版本功能静默失效不影响编辑 |