Files
tianshu-engine/docs/E17_AI_ASSISTANT_PROPOSAL.md

235 lines
15 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.

# 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`,作为 `devDependencies``npm install``node_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/**/*.vue``src/**/*.ts``src/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/sessions``opencode 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` | 新增 `deepseekKey`localStorage`showAIPanel``aiResult``sessionId` |
| `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 内容) |