Files
tianshu-engine/docs/E18_VERSION_AND_DIFF_PROPOSAL.md

11 KiB
Raw Permalink Blame History

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()

版本数据结构

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

// 保存版本
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 给对应节点加角标

数据结构

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 内)

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 版本功能静默失效,不影响编辑