Files
tianshu-engine/docs/E17_AI_ASSISTANT_PROPOSAL.md

15 KiB
Raw Blame History

E17: AI 编码助手 — opencode + DeepSeek 集成

概述

编辑器内嵌 AI 对话面板,使用 opencode Agent + DeepSeek 后端,支持两种模式:

  • JSON 模式 — 修改场景配置,填充 textarea 供用户审查后接受
  • 代码模式 — 直接修改 src/ 目录下的 Vue 组件和 CSSVite HMR 实时预览

完整架构

浏览器 (Editor)
  ├── AIPanel.vue          用户输入自然语言
  ├── NodeEditor.vue       接收 AI 返回的 JSON[接受]/[撤销]
  └── App.vue              管理 AI 面板状态

              ↓ POST /api/ai { sessionId, userMessage, apiKey, mode, nodeId? }

Vite 中间件(零状态)
  └── spawn('node_modules/.bin/opencode', ['run', '--session', sessionId, '--model', 'deepseek', '--format', 'json', fullMessage])
       ├── fullMessage = modePrefix + userMessage
       │     JSON模式:  "JSON模式只返回修改后的 JSON 文本,不要写任何文件。需求:..." + nodeJson
       │     代码模式:  "代码模式:直接修改 src/ 下的源码文件并保存。需求:..."
       ├── opencode 自身管理会话上下文(--session 复用已有会话)
       ├── 自动读取项目文件构建 prompt → 调 DeepSeek API
       └── 返回 stdout

JSON 模式交互时序

用户                 编辑器                 Vite 中间件           opencode               DeepSeek
 │                      │                       │                    │                      │
 │ 选中节点+输入需求     │                       │                    │                      │
 │─────────────────────→│                       │                    │                      │
 │                      │ POST /api/ai          │                    │                      │
 │                      │──────────────────────→│                    │                      │
 │                      │                       │ spawn opencode     │                      │
 │                      │                       │───────────────────→│                      │
 │                      │                       │                    │ 读 story.json +     │
 │                      │                       │                    │ SPEC.md + 用户需求  │
 │                      │                       │                    │────────────────────→│
 │                      │                       │                    │      返回 JSON       │
 │                      │                       │                    │←────────────────────│
 │                      │                       │  stdout: JSON      │                      │
 │                      │                       │←───────────────────│                      │
 │                      │   200 { result }       │                    │                      │
 │                      │←──────────────────────│                    │                      │
 │                      │                       │                    │                      │
 │ 显示 [接受] [撤销]    │                       │                    │                      │
 │─────→ textarea 填充   │                       │                    │                      │
 │                      │                       │                    │                      │
 │ 点击 [接受]           │                       │                    │                      │
 │─────────────────────→│                       │                    │                      │
 │                      │ JSON.parse → update   │                    │                      │
 │                      │ → autoSave 写磁盘     │                    │                      │

代码模式交互时序

用户                 编辑器                 Vite 中间件           opencode               DeepSeek
 │                      │                       │                    │                      │
 │ 输入 UI 修改需求      │                       │                    │                      │
 │─────────────────────→│                       │                    │                      │
 │                      │ POST /api/ai          │                    │                      │
 │                      │──────────────────────→│                    │                      │
 │                      │                       │ spawn opencode     │                      │
 │                      │                       │───────────────────→│                      │
 │                      │                       │                    │ 读 src/components/  │
 │                      │                       │                    │ 改 Vue/CSS 文件     │
 │                      │                       │                    │────────────────────→│
 │                      │                       │                    │      写文件到 src/   │
 │                      │                       │                    │←────────────────────│
 │                      │                       │  stdout: done      │                      │
 │                      │                       │←───────────────────│                      │
 │                      │   200 { result }       │                    │                      │
 │                      │←──────────────────────│                    │                      │
 │                      │                       │                    │                      │
 │   Vite HMR 检测变化   │                       │                    │                      │
 │←─────────────────────│                       │                    │                      │
 │   浏览器热更新预览    │                       │                    │                      │

关键设计决策

决策 做法 原因
AI 后端 opencode Agent + DeepSeek 唯一覆盖 JSON + 代码双模式;自动读取项目文件构建上下文
opencode 安装 npm 包 opencode-ai,作为 devDependenciesnpm installnode_modules/.bin/opencode 即可用 clone 即用,无需手动全局安装
API Key 存储 编辑器设置中输入,存 localStorage每次请求传给 /api/ai 不硬编码,创作者自行管理
API Key 传输 通过 POST body 传到 Vite 中间件,不暴露在浏览器网络日志 XSS 只能获取 localStorage看不到服务端日志
JSON 编辑 AI 返回 JSON 填充 textarea用户审查后 [接受] 才保存 保留创作者对故事数据的最终控制权
代码编辑 opencode 直接写 src/ 目录Vite HMR 秒级刷新 代码修改即时可视化,不需要手动刷新
上下文构建 opencode 读取项目文件自成上下文 不需要手动拼系统 prompt
模式切换 AIPanel 当前模式标签:选中节点 → "JSON 模式";未选中节点 → "代码模式"。手动可切换 减少用户认知负担,大部分场景自动判断正确
DeepSeek 模型 --model deepseek,通过 opencode providers 配置 Key opencode 自身管理模型路由

会话管理

opencode 原生支持会话:opencode run --session <id> 复用已有上下文,opencode session list 列出历史。

架构原则:前端持 sessionId中间件无状态

AIPanel (Vue)
  ├── sessionId = localStorage.getItem('editor_ai_session') || crypto.randomUUID()
  ├── 每次请求 POST /api/ai 带上 sessionId
  ├── "新建对话" → 新 UUID → 覆盖 localStorage
  └── 历史列表 → GET /api/ai/sessions → opencode session list → 前端渲染

Vite 中间件 — 零状态
  └── spawn('opencode', ['run', '--session', sessionId, '--format', 'json', userMessage])
  └── 不存任何 sessionId不维护子进程

会话操作

操作 opencode CLI 触发方式
首次对话 opencode run --session <新UUID> --model deepseek --format json "..." AIPanel 检测 localStorage 无 sessionId
继续对话 opencode run --session <已有UUID> --model deepseek --format json "..." 带上同一 sessionId
新建对话 前端生成新 UUID → 旧会话仍存在于 opencode 内部 DB AIPanel "新建对话" 按钮
历史列表 opencode session list AIPanel 顶部下拉(可选)

为什么不用长活子进程

长活子进程 + stdin/stdout 独立进程 + --session
进程管理 需管道通信、心跳检测 spawn 等待 exit0 行管理代码
中间件状态 需维护 sessionId→process Map 零状态
dev server 重启 会话丢失 localStorage 持久化,重启可恢复
稳定性 子进程可能 crash 每次独立进程,天然隔离
延迟 ~1-3s管道通信 ~5s启动 Node + 加载会话),对编辑器 AI 场景可接受

安全设计

层级 措施
API Key AES 加密存 localStorage→ 不用Key 本身是服务端 API 凭证浏览器存储已是业界实践VS Code Copilot、Cursor 同理)
传输 POST body 中传递HTTPS 加密。仅 /api/ai 路由可读取,上游不打印日志
文件写入 opencode 通过 Vite 中间件 spawn中间件做路径白名单JSON 模式只允许 public/scenes/*.json;代码模式只允许 src/**/*.vuesrc/**/*.tssrc/locales/*.json
请求频率 中间件加 3s 内去重(同一 mode+userMessage 不重复 spawn

错误处理

错误场景 处理
DeepSeek API 超时15s 中间件 kill 子进程,返回 504 { error: "timeout" }
opencode 进程崩溃exit code ≠ 0 中间件返回 500 { error: "opencode exited with code " + code }
DeepSeek API 余额不足 中间件返回 402 { error: "insufficient_quota" }
opencode 返回非 JSON代码模式 直接视为成功stdout 文本无关紧要)
opencode 返回非 JSONJSON 模式) 中间件尝试用正则提取 JSON 块,无法提取则返回 500 给前端

文件改动清单

文件 职能
package.json 新增 opencode-ai devDependency"opencode-ai": "^1.17"
vite.config.ts 新增 POST /api/ai 中间件:接收 sessionId/userMessage/apiKey/mode构造 modePrefix + spawn(opencode, ['run', '--session', sessionId, '--model', 'deepseek', '--format', 'json', fullMessage])GET /api/ai/sessionsopencode session list
editor/composables/useAI.ts 新建sendAIRequest(sessionId, userMessage, mode) → fetch(/api/ai)listSessions() → fetch(/api/ai/sessions)
editor/components/AIPanel.vue 新建 — 消息历史 + <input> + loading + "正在生成..." + "新建对话"按钮 + 会话列表下拉
editor/stores/editorStore.ts 新增 deepseekKeylocalStorageshowAIPanelaiResultsessionId
editor/App.vue 新增 "AI 助手" 按钮 + AIPanel 组件 + 设置面板中 API Key 输入栏

测试用例

T1: JSON 模式 — 正常流程

步骤 预期结果
选中一个场景节点 NodeEditor 显示该场景 JSON
打开 AIPanel输入"给这个场景添加一个 QTE" 显示 loading 状态
opencode 返回修改后 JSON NodeEditor textarea 被填充,出现 [接受] [撤销]
点击 [接受] JSON.parse 成功store.updateScene 执行autoSave 写磁盘
点击 [撤销] textarea 回滚到 AI 修改前的值

T2: JSON 模式 — opencode 返回无效 JSON

步骤 预期结果
AI 返回非法 JSON 中间件返回 500 { error }
AIPanel 显示红色错误提示 "AI 返回格式异常,请重试"textarea 不被填充

T3: 代码模式 — 正常流程

步骤 预期结果
未选中任何节点 AIPanel 显示 "代码模式" 标签
输入"把按钮改成圆角 20px" 显示 loading
opencode 修改 src/components/ChoicePanel.vue 的 CSS 文件写磁盘成功
Vite HMR 触发 预览窗按钮圆角即时可见

T4: 会话管理

步骤 预期结果
首次对话 localStorage 无 editor_ai_session → AIPanel 自动生成 UUID
第二次对话 带上同一 sessionId → opencode --session <id> 恢复上下文
点击 "新建对话" 新 UUID 写入 localStorage
刷新页面 sessionId 不丢失,历史列表可查

T5: 错误处理

步骤 预期结果
DeepSeek Key 无效 AIPanel 显示 "API 认证失败,请检查 Key"
opencode 进程被 kill 中间件 child.on('exit', 137) → 500
请求 15s 无响应 中间件 kill 子进程 → 504
同一 userMessage 3s 内发送两次 去重,不重复 spawn

T6: 路径白名单

步骤 预期结果
代码模式让 AI 修改 /etc/passwd 中间件拒绝或 opencode 写盘失败
JSON 模式让 AI 修改 ../secret.json 路径不在 public/scenes/*.json 范围内, 被拒绝

T7: API Key 管理

步骤 预期结果
未填 Key 就发送请求 AIPanel 提示 "请先在设置中输入 DeepSeek API Key"
填入 Key → 发送请求 Key 从 localStorage 读取POST body 传给中间件
刷新页面 Key 仍存在

T8: NodeEditor 覆盖保护

步骤 预期结果
用户正在手动编辑 textarea → AI 返回结果 AI 结果填充到 textarea覆盖手动编辑可 [撤销]
AI 返回后用户手动再编辑 → 失焦 手动编辑内容通过 blur 保存(不是 AI 内容)