# 技术分析与问题解决文档 > 本文档整合了项目开发过程中的技术分析和问题解决方案 ## 目录 1. [IndexedDB 数据库问题分析](#1-indexedb-数据库问题分析) 2. [图像存储解决方案](#2-图像存储解决方案) 3. [模式术语迁移总结](#3-模式术语迁移总结) 4. [代码审查提示词评估](#4-代码审查提示词评估) --- ## 1. IndexedDB 数据库问题分析 ### 1.1 为什么数据会累积? # 📊 为什么 IndexedDB 会无限累积到 2.4 GB? ## 🔍 问题根源分析 ### 1. **自动保存机制(每次变化都保存)** 代码中有三个地方会自动触发保存: ```typescript // PromptOptimizerApp.vue:1954-1960 window.addEventListener('pagehide', handlePagehide) // ① 页面卸载时 document.addEventListener('visibilitychange', handleVisibilityChange) // ② 标签页切换时 ``` ```typescript // PromptOptimizerApp.vue:1105-1128 watch( () => promptTester.testResults, (newTestResults) => { // 每次测试结果变化,都会自动同步到 session store (session as any).updateTestResults(stableResults); }, { deep: true } // ⚠️ 深度监听,任何字段变化都会触发 ); ``` **这意味着:** - ✅ 每次测试完成 → `updateTestResults` 被调用 → `lastActiveAt = Date.now()` - ✅ 切换标签页 → 触发 `visibilitychange` → 调用 `saveAllSessions()` - ✅ 关闭页面 → 触发 `pagehide` → 调用 `saveAllSessions()` ### 2. **保存的是完整的 state(包括超大的 testResults)** ```typescript // useBasicSystemSession.ts:211-227 const saveSession = async () => { // ❌ 直接序列化整个 state,没有任何过滤或截断 const snapshot = JSON.stringify(state.value) await $services.preferenceService.set('session/v1/basic-system', snapshot) } ``` **state.value 包含:** ```typescript interface BasicSystemSessionState { prompt: string optimizedPrompt: string reasoning: string testContent: string testResults: TestResults | null // ⚠️ 这个可以无限大! // ...其他字段 } interface TestResults { originalResult: string // ⚠️ 可能几十 KB originalReasoning: string // ⚠️ 可能几十 KB optimizedResult: string // ⚠️ 可能几十 KB optimizedReasoning: string // ⚠️ 可能几十 KB } ``` ### 3. **没有任何大小限制** ```typescript // useBasicSystemSession.ts:128-143 const updateTestResults = (results: TestResults | null) => { // ❌ 没有检查 results 的大小 // ❌ 没有截断超长文本 // ❌ 没有限制历史记录数量 state.value.testResults = results state.value.lastActiveAt = Date.now() } ``` **对比:没有任何防护代码:** - ❌ 没有 `if (size > MAX_SIZE) { truncate() }` - ❌ 没有 `if (text.length > 50000) { text = text.slice(0, 50000) }` - ❌ 没有 `cleanupOldResults()` ### 4. **没有清理机制** 搜索整个代码库: ```bash # 搜索清理相关代码 grep -r "清理\|cleanup\|clean\|delete.*test\|remove.*test" packages/ui/src/stores/session # 结果:No matches found ❌ ``` **这意味着:** - ❌ 测试结果永远不会被删除 - ❌ 旧数据永远不会过期 - ❌ 数据库只会越来越大 ## 📈 累积过程示例 假设用户的使用场景: ### Day 1: 正常使用 ``` 测试 1: GPT-4 输出 5 KB → 保存到 IndexedDB (5 KB) 测试 2: Claude 输出 8 KB → 保存到 IndexedDB (8 KB) 测试 3: Gemini 输出 6 KB → 保存到 IndexedDB (6 KB) 总计: 19 KB ✅ ``` ### Day 2: 继续测试 ``` 测试 4-10: 每次 5-10 KB 总计: 19 KB + 70 KB = 89 KB ✅ ``` ### Day 30: 一个月后 ``` 测试 1-300: 平均每次 7 KB 总计: 300 * 7 KB = 2.1 MB ⚠️ ``` ### Day 90: 三个月后 ``` 测试 1-900: 平均每次 7 KB 总计: 900 * 7 KB = 6.3 MB ⚠️⚠️ ``` ### 但实际情况更糟糕! **如果用户测试了一个超长输出:** ```typescript // 用户让 GPT-4 写了一篇长文章 testResults = { originalResult: "很长的文章...", // 100 KB originalReasoning: "详细的思考...", // 50 KB optimizedResult: "优化后的长文章...", // 120 KB optimizedReasoning: "优化思路...", // 60 KB } // 单次测试 = 330 KB! ``` **如果用户频繁切换标签页:** ``` 用户打开 10 个标签页 → 每个标签页都有自己的 session 每个 session 都累积测试结果 10 * 6.3 MB = 63 MB ⚠️⚠️⚠️ ``` **如果用户使用了 Pro 模式的多轮对话:** ```typescript // Pro-多消息模式 messages = [ { role: 'user', content: '...' }, // 每条可能 10-50 KB { role: 'assistant', content: '...' }, // ... 30 条消息 ] // 单个会话 = 30 * 30 KB = 900 KB ``` ### 最终结果:2.4 GB 的数据库 ``` 6 个 session stores (basic-system, basic-user, pro-system, pro-user, image-text2image, image-image2image) × 每个累积 3 个月的测试结果 × 没有任何清理 × 每次切换标签页都保存一次 = 2.4 GB 💥 ``` ## 🔧 为什么代码没有防护? ### 1. **过度信任用户行为** 开发者假设: - ❌ "用户不会测试超长文本" - ❌ "用户不会频繁切换标签" - ❌ "用户会定期清理数据" 实际情况: - ✅ 用户经常测试 GPT-4 的长回答(5000+ 字) - ✅ 用户会开很多标签页 - ✅ 用户根本不知道需要清理 ### 2. **缺少监控和告警** 没有代码检查: - ❌ IndexedDB 使用量 - ❌ 单个 session 大小 - ❌ 序列化时间(超过 1 秒说明数据太大) ### 3. **缺少自动清理策略** 其他应用的常见做法: ```typescript // ✅ 示例:自动清理 7 天前的数据 const cleanupOldData = () => { const now = Date.now(); const WEEK = 7 * 24 * 60 * 60 * 1000; if (state.value.lastActiveAt && (now - state.value.lastActiveAt) > WEEK) { state.value.testResults = null; state.value.testContent = ''; } } // ✅ 示例:限制单个字段大小 const MAX_RESULT_LENGTH = 50000; // 50 KB if (results.originalResult.length > MAX_RESULT_LENGTH) { results.originalResult = results.originalResult.slice(0, MAX_RESULT_LENGTH) + '...[已截断]'; } // ✅ 示例:数据库大小检查 if (estimatedSize > 100 * 1024 * 1024) { // 100 MB console.warn('数据库过大,建议清理'); showCleanupDialog(); } ``` ## 📊 与你的情况对比 你的备份数据库: ``` 总大小: 2.4 GB 文件数: 40+ 个 .ldb 文件 最大文件: 27 MB ``` 这说明: - ✅ 你长期使用该应用(可能几个月) - ✅ 测试了大量文本(可能包含 GPT-4 的长回答) - ✅ 从未手动清理过数据 - ✅ 数据库达到了浏览器的临界点(打开时崩溃) ## ✅ 总结 **无限累积的三个关键原因:** 1. **自动保存很频繁** - 每次测试、每次切换标签页都保存 2. **保存的数据很大** - 完整的测试结果,没有截断 3. **从不清理** - 代码中没有任何清理逻辑 **解决方案:** - 🔧 立即:删除数据库重置(已提供工具) - 🛡️ 预防:实施数据大小限制和自动清理(下一步任务) ### 1.2 为什么是追加而非覆盖? # 🔍 为什么是新增而不是覆盖?深层原因分析 ## 关键发现 你的 IndexedDB 确实在使用 **覆盖操作** (put),但底层存储机制导致了数据"累积"而非真正的覆盖。 ## 1️⃣ 代码层面:确实是覆盖操作 ```typescript // dexieStorageProvider.ts:91-95 async setItem(key: string, value: string): Promise { await this.db.storage.put({ // ✅ put() 是覆盖操作 key, // 主键相同 value, // 新值 timestamp: Date.now() }); } ``` ```typescript // 数据库定义: dexieStorageProvider.ts:23-25 this.version(1).stores({ storage: 'key, value, timestamp' // ✅ 'key' 是主键 }); ``` **逻辑上:** 每次调用 `setItem('session/v1/basic-system', newData)` **应该覆盖**同一个 key 的旧值。 ## 2️⃣ 底层存储:LevelDB 的 LSM-Tree 架构 但是!Chrome 的 IndexedDB 底层使用 **LevelDB**,而 LevelDB 使用 **LSM-Tree (Log-Structured Merge Tree)** 架构。 ### LSM-Tree 的工作原理 ``` 写入流程(Append-Only): ┌─────────────────────────────────────────────────────────────┐ │ 1. 写入 MemTable (内存) │ │ - key='session/v1/basic-system' │ │ - value='{"prompt":"..."}' (1 KB) │ │ - 不会检查是否存在相同 key │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 2. MemTable 满 → 刷写到 SSTable 文件 (.ldb) │ │ - 001445.ldb (包含这次的写入) │ │ - 不会删除旧文件! │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 3. 再次写入同一个 key │ │ - key='session/v1/basic-system' │ │ - value='{"prompt":"...", "testResults": {...}}' (500 KB)│ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 4. 再次刷写到新的 SSTable 文件 │ │ - 001496.ldb (包含新的值) │ │ - 001445.ldb 仍然存在!(包含旧值) │ └─────────────────────────────────────────────────────────────┘ ``` ### 为什么旧数据没有被删除? **关键:** LSM-Tree 是 **Append-Only(只追加)** 架构: - ❌ 不会就地修改已有的 .ldb 文件 - ❌ 不会立即删除旧版本的数据 - ✅ 每次写入都创建新的记录 - ✅ 旧数据通过 **Compaction(压缩合并)** 清理 ## 3️⃣ Compaction(压缩)未及时执行 ### 正常情况 ``` 写入 100 次 → 积累 100 个版本 → 触发 Compaction ↓ 合并多个 .ldb 文件 ↓ 删除重复的 key,只保留最新版本 ↓ 数据库大小回落 ``` ### 你的情况:Compaction 被延迟或失败 可能原因: 1. **浏览器崩溃或异常退出** - Compaction 是后台任务 - 如果浏览器频繁崩溃,Compaction 无法完成 - 旧数据一直累积 2. **数据写入速度 > Compaction 速度** ``` 每秒写入 10 次 (切换标签页很频繁) vs Compaction 每 10 秒运行一次 → 累积速度快于清理速度 ``` 3. **数据过大导致 Compaction 失败** ``` 单个 .ldb 文件 = 27 MB 合并 10 个文件 = 270 MB → Compaction 需要大量内存 → 浏览器内存不足 → Compaction 失败,旧数据保留 ``` 4. **LevelDB 的 Compaction 策略** ``` Level 0: 新写入的文件(未排序) Level 1-6: 已压缩的文件(排序) Compaction 触发条件: - Level 0 文件数 > 4 - Level N 总大小 > 阈值 你的情况: - 40+ 个 .ldb 文件 → 可能卡在 Level 0 - 没有触发或完成 Compaction ``` ## 4️⃣ 你的数据库状态分析 ```bash $ ls -lhS *.ldb | head -10 -rw-r--r-- 27M 001445.ldb # 第1次大保存 -rw-r--r-- 27M 001481.ldb # 第2次大保存 -rw-r--r-- 26M 001534.ldb # 第3次大保存 ... 共 40+ 个文件 = 2.4 GB ``` **这说明:** - ✅ 每次保存都创建了新的 .ldb 文件 - ✅ 旧的 .ldb 文件从未被清理 - ✅ Compaction **完全没有执行**或**一直失败** ## 5️⃣ 为什么其他应用没有这个问题? ### 对比:正常的 Web 应用 ```typescript // ✅ 其他应用通常这样做 await db.users.put({ id: 1, name: 'Alice' }) // 1 KB await db.users.put({ id: 2, name: 'Bob' }) // 1 KB // ... // 特点: // - 小数据量 (每条 1-10 KB) // - 写入频率低 (每秒 1-2 次) // - Compaction 能及时清理 ``` ### 你的应用 ```typescript // ❌ Prompt Optimizer 的情况 await db.storage.put({ key: 'session/v1/basic-system', value: JSON.stringify({ // ... testResults: { originalResult: '...很长的文本...', // 100 KB optimizedResult: '...更长的文本...', // 120 KB } }) }) // 单次写入 500 KB - 2 MB! // 特点: // - 超大数据量 (每次 500 KB - 2 MB) // - 写入频率高 (每次切换标签页都写) // - Compaction 跟不上,累积成 2.4 GB ``` ## 6️⃣ 根本原因总结 | 层级 | 问题 | 影响 | |------|------|------| | **应用层** | 保存完整的 testResults,没有截断 | 单次写入 500 KB - 2 MB | | **应用层** | 频繁自动保存(每次切换标签页) | 写入频率过高 | | **存储层** | LevelDB 的 LSM-Tree 是追加式写入 | 每次写入创建新记录 | | **存储层** | Compaction 未及时或失败 | 旧数据永不删除 | | **结果** | **2.4 GB 的累积数据** | **浏览器崩溃** | ## 7️⃣ 证据链 ``` 用户行为 IndexedDB 逻辑 LevelDB 物理层 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 测试 10 次 → 10 次 put() 操作 → 10 个新记录追加到 MemTable 切换标签页 → 触发 saveSession → MemTable 刷写到 001445.ldb (27 MB) 再测试 10 次 → 10 次 put() 操作 → 10 个新记录追加到 MemTable 切换标签页 → 触发 saveSession → MemTable 刷写到 001481.ldb (27 MB) ⚠️ 001445.ldb 仍然存在! ⚠️ Compaction 未执行 重复 3 个月 → 1000 次保存 → 40+ 个 .ldb 文件 = 2.4 GB ⚠️ 所有旧文件都保留 ⚠️ Compaction 完全失败 尝试打开页面 → indexedDB.open() → LevelDB 尝试读取所有 .ldb ⚠️ 加载 2.4 GB 到内存 💥 浏览器崩溃 ``` ## 8️⃣ 解决方案 ### 立即修复(治标) 1. 删除整个 IndexedDB 数据库 2. 浏览器重新创建干净的数据库 ### 根本修复(治本) 1. **限制单次写入大小** ```typescript if (testResults.originalResult.length > 50000) { testResults.originalResult = testResults.originalResult.slice(0, 50000) + '...' } ``` 2. **减少写入频率** ```typescript // 使用 debounce,每 5 秒最多保存一次 const debouncedSave = debounce(saveSession, 5000) ``` 3. **定期清理旧数据** ```typescript // 只保留最近一次的测试结果 state.value.testResults = latestResults ``` 4. **分离大数据存储** ```typescript // testResults 单独存储,不放在 session 中 await db.testResults.put({ sessionId, results }) ``` ## 9️⃣ 为什么浏览器不自动修复? Chrome 的 LevelDB Compaction 依赖: - ✅ 浏览器**正常关闭**时触发 - ✅ 数据库**空闲时**自动运行 - ❌ 浏览器**崩溃**时无法完成 - ❌ 数据库**持续高负载**时延迟 你的情况: 1. 应用频繁写入 → 数据库始终繁忙 2. 浏览器可能因数据过大而崩溃 → Compaction 无法完成 3. 形成恶性循环 → 数据只增不减 --- ## ✅ 总结 **问题的本质:** 不是"新增 vs 覆盖"的问题,而是: 1. **逻辑上是覆盖**(put 同一个 key) 2. **物理上是追加**(LSM-Tree 架构) 3. **清理机制失效**(Compaction 未执行) 4. **数据只增不减**(2.4 GB 累积) **类比:** 就像你每天往同一个文件柜(key)里放新文件(value),虽然名字相同,但旧文件没人清理,最后文件柜爆满。 ### 1.3 为什么分离存储有效? # 🔍 为什么分离存储能解决问题? ## 核心问题:保存频率不同 ### 当前模式(问题) ```typescript // ❌ Session 包含完整图像 interface SessionState { originalPrompt: string originalImageResult: { images: [{ b64: "2-3 MB 的 base64..." }] // 包含在 session 中 } } // 保存流程 saveSession() { const snapshot = { prompt: state.prompt, imageResult: state.originalImageResult // ← 包含 26 MB 图像 } await db.put('session', snapshot) // ← 每次都保存 26 MB } // 触发时机 - 用户生成图像 → 保存 - 用户切换标签页 → 保存 ← 包含图像! - 用户切换回来 → 保存 ← 包含图像! - 用户关闭页面 → 保存 ← 包含图像! ``` **问题:** ``` 时间线: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10:00 生成图像1 (26 MB) ↓ saveSession() ↓ 写入 001.ldb (26 MB) 10:05 切换标签页 ↓ saveSession() ↓ 写入 002.ldb (26 MB) ← 重复保存图像1! 10:10 切换回来 ↓ saveSession() ↓ 写入 003.ldb (26 MB) ← 又重复保存图像1! 10:15 关闭页面 ↓ saveSession() ↓ 写入 004.ldb (26 MB) ← 又重复保存图像1! 10:20 重新打开,生成图像2 (26 MB) ↓ saveSession() ↓ 写入 005.ldb (26 MB) ← 图像2 10:25 又切换标签页 ↓ saveSession() ↓ 写入 006.ldb (26 MB) ← 重复保存图像2! ... 重复 42 次 ... 总计:42 个文件 × 26 MB = 1.1 GB ❌ ``` --- ### 分离模式(解决) ```typescript // ✅ Session 只保存引用 interface SessionState { originalPrompt: string originalImageRef: { imageId: "img_123456" // ← 只保存 ID (20 字节) } } // 图像单独存储 interface ImageRecord { id: string data: { images: [{ b64: "2-3 MB 的 base64..." }] } createdAt: number } // 保存流程(分离) saveSession() { const snapshot = { prompt: state.prompt, imageRef: state.originalImageRef // ← 只有 ID,几 KB } await db.put('session', snapshot) // ← 只保存几 KB } saveImage(data) { const imageRecord = { id: `img_${Date.now()}`, data: data, // ← 26 MB createdAt: Date.now() } await db.put('images', imageRecord) // ← 只在生成新图像时调用 } ``` **解决:** ``` 时间线: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10:00 生成图像1 (26 MB) ↓ saveImage() ← 只调用1次 ↓ 写入 images:001.ldb (26 MB) ↓ saveSession() ↓ 写入 session:001.ldb (5 KB) ← 只保存 ID 10:05 切换标签页 ↓ saveSession() ← 没有 saveImage() ↓ 写入 session:002.ldb (5 KB) ← 又是 ID,但很小! 10:10 切换回来 ↓ saveSession() ↓ 写入 session:003.ldb (5 KB) ← 很小! 10:15 关闭页面 ↓ saveSession() ↓ 写入 session:004.ldb (5 KB) ← 很小! 10:20 重新打开,生成图像2 (26 MB) ↓ saveImage() ← 只调用1次 ↓ 写入 images:002.ldb (26 MB) ↓ saveSession() ↓ 写入 session:005.ldb (5 KB) 10:25 又切换标签页 ↓ saveSession() ↓ 写入 session:006.ldb (5 KB) ← 很小! ... 重复 42 次 ... Session 保存:42 个文件 × 5 KB = 210 KB ✅ 图像保存:2 个文件 × 26 MB = 52 MB ✅ 总计:210 KB + 52 MB = 52.2 MB ✅ ``` --- ## 关键对比 ### 保存频率 ``` 当前模式: - Session 保存:42 次 - 每次包含图像:42 次 - 图像数据写入:42 次 分离模式: - Session 保存:42 次 - 每次只包含 ID:42 次 - 图像数据写入:2 次(只在生成新图像时) ``` ### 数据写入量 ``` 当前模式: 42 次 × 26 MB = 1,092 MB (1.1 GB) ❌ 分离模式: Session:42 次 × 5 KB = 210 KB ✅ 图像:2 次 × 26 MB = 52 MB ✅ 总计:52.2 MB ✅ 减少:95%+ 🎉 ``` --- ## Compaction 为什么能成功? ### 当前模式(失败) ``` 尝试合并 42 个 session 文件: ├── 001.ldb (26 MB) ← 包含图像1 ├── 002.ldb (26 MB) ← 包含图像1 ├── 003.ldb (26 MB) ← 包含图像1 ├── ... └── 042.ldb (26 MB) ← 包含图像2 需要读取:42 × 26 MB = 1,092 MB 需要内存:3-4 倍(去重、合并)= 3-4 GB 浏览器限制:~100-500 MB 结果:内存不足/超时 → Compaction 失败 ❌ ``` ### 分离模式(成功) ``` 尝试合并 42 个 session 文件: ├── 001.ldb (5 KB) ← 只有 ID "img_123" ├── 002.ldb (5 KB) ← 只有 ID "img_123" ├── 003.ldb (5 KB) ← 只有 ID "img_123" ├── ... └── 042.ldb (5 KB) ← 只有 ID "img_456" 需要读取:42 × 5 KB = 210 KB 需要内存:3-4 倍 = 1 MB 左右 浏览器限制:~100-500 MB 结果:轻松完成 ✅ Compaction: 1. 快速读取所有文件(210 KB) 2. 合并去重(都在内存中) 3. 写入新文件(5 KB) 4. 删除 42 个旧文件 ✅ 最终:只有 1 个最新的 session 文件(5 KB) ``` --- ## 核心原理 ### 1. 不同的生命周期 ``` Session:高频更新,低频读取 - 每次切换标签页都保存 - 但数据很小(几 KB) - Compaction 轻松处理 Images:低频更新,按需读取 - 只在生成新图像时保存 - 数据较大(26 MB) - 但数量可控(假设 10 张 = 260 MB) ``` ### 2. 独立的 Compaction ``` Session Store: - 小文件,高频率 - Compaction 每秒都在运行 - 随时保持干净状态 Images Store: - 大文件,低频率 - Compaction 偶尔运行 - 总量可控(260 MB) ``` ### 3. 故障隔离 ``` 当前模式: - 图像导致 Session 文件过大 - Session Compaction 失败 - 整个数据库崩溃 ❌ 分离模式: - Session 文件很小 → Compaction 正常 ✅ - 图像文件独立 → 即使 Compaction 慢,也不影响 Session ✅ - 故障隔离 ✅ ``` --- ## 类比 ### 当前模式 ``` 就像:每次搬家都把所有家具打包 - 第1次搬家:打包所有家具(26 MB) - 第2次搬家:又打包所有家具(26 MB) - 第3次搬家:又打包所有家具(26 MB) ... - 第42次搬家:还是打包所有家具(26 MB) 结果:搬家公司崩溃 ❌ ``` ### 分离模式 ``` 就像:只打包"家具清单",家具放在仓库 - 第1次搬家:打包清单(5 KB)+ 家运到仓库(26 MB) - 第2次搬家:打包清单(5 KB)← 清单很小! - 第3次搬家:打包清单(5 KB)← 清单很小! ... - 第42次搬家:打包清单(5 KB)← 清单很小! 结果: - 清单:42 × 5 KB = 210 KB ✅ - 仓库:只有真正搬家的 2 次的家具 = 52 MB ✅ ``` --- ## 总结 ### 为什么分离能解决? | 维度 | 当前模式 | 分离模式 | |------|----------|----------| | Session 保存大小 | 26 MB | 5 KB | | Session 保存次数 | 42 次 | 42 次 | | Session 总写入量 | 1,092 MB | 210 KB | | 图像保存次数 | 42 次(冗余) | 2 次(实际) | | 图像总写入量 | 1,092 MB | 52 MB | | Compaction 内存需求 | 3-4 GB | 1 MB | | Compaction 结果 | 失败 ❌ | 成功 ✅ | ### 核心原理 **不是"分离"本身解决问题,而是:** 1. Session 不再包含大数据 → 文件小 2. 文件小 → Compaction 能成功 3. Compaction 成功 → 旧文件被删除 4. 旧文件被删除 → 数据库不累积 5. 图像单独保存 → 总量可控(260 MB vs 1.1 GB) **关键是:减少写入频率(42次 → 2次),而不是分离!** ### 1.4 IndexedDB 损坏分析 # 🔴 IndexedDB 数据库损坏问题分析报告 ## 问题现象 - 打开 IndexedDB 数据库导致浏览器崩溃 - 应用在有历史数据的情况下无法启动 - 连简单的 `indexedDB.open()` 操作都会触发崩溃 ## 🔍 根本原因分析 ### 1. **危险的序列化操作** **问题代码** (所有 session store): ```typescript const saveSession = async () => { const snapshot = JSON.stringify(state.value) // ❌ 没有错误处理 await $services.preferenceService.set('session/v1/basic-system', snapshot) } ``` **风险点**: - 如果 `state.value` 包含循环引用 → `JSON.stringify` 抛出错误 → 错误被吞掉 → 写入不完整数据 - 如果 `state.value` 包含超大对象(> 100MB)→ 内存溢出 → 浏览器崩溃 - 没有大小限制检查 - 没有循环引用检测 ### 2. **可能的并发写入冲突** 虽然 `useSessionManager.ts` 已修复为顺序保存,但: - 页面卸载时的 `beforeunload` 事件可能触发多次 `saveAllSessions` - 如果用户快速刷新/关闭页面,可能同时触发多个保存操作 - IndexedDB 事务可能冲突,导致数据库损坏 ### 3. **testResults 可能包含超大对象** **问题场景**: ```typescript // useBasicSystemSession.ts state.value.testResults = results // 可能包含完整的输出文本 ``` 如果用户测试了很长的输出(如 GPT-4 的长回答): - 单个 testResult 可能 1-5 MB - 多次测试累积可能达到 50-100 MB - `JSON.stringify` 时内存暴涨 ### 4. **没有数据清理机制** - 历史测试数据一直累积 - 没有大小上限检查 - 没有定期清理旧数据 ## 🛠️ 解决方案 ### 立即修复 (高优先级) 1. **添加安全的序列化函数** ```typescript function safeStringify(obj: any, maxSize: number = 10 * 1024 * 1024): string | null { try { // 检测循环引用 const seen = new WeakSet(); const jsonString = JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]'; } seen.add(value); } return value; }); // 检查大小 if (jsonString.length > maxSize) { console.error(`数据过大: ${jsonString.length} 字节 (限制: ${maxSize})`); return null; } return jsonString; } catch (error) { console.error('序列化失败:', error); return null; } } ``` 2. **限制 testResults 大小** ```typescript const updateTestResults = (results: TestResults | null) => { if (!results) return; // 限制文本长度 const MAX_LENGTH = 50000; // 50KB if (results.originalResult?.length > MAX_LENGTH) { results.originalResult = results.originalResult.slice(0, MAX_LENGTH) + '...[截断]'; } if (results.optimizedResult?.length > MAX_LENGTH) { results.optimizedResult = results.optimizedResult.slice(0, MAX_LENGTH) + '...[截断]'; } state.value.testResults = results; } ``` 3. **添加保存前的验证** ```typescript const saveSession = async () => { const snapshot = safeStringify(state.value); if (!snapshot) { console.error('[BasicSystemSession] 序列化失败,跳过保存'); return; } try { await $services.preferenceService.set('session/v1/basic-system', snapshot); } catch (error) { console.error('[BasicSystemSession] 保存会话失败:', error); } } ``` 4. **添加定期清理** ```typescript // 清理超过 7 天的测试结果 const cleanupOldData = () => { const now = Date.now(); const WEEK = 7 * 24 * 60 * 60 * 1000; if (state.value.lastActiveAt && (now - state.value.lastActiveAt) > WEEK) { state.value.testResults = null; state.value.testContent = ''; } } ``` ### 中期优化 1. **数据分片存储** - 将 testResults 单独存储 - 只保留最近 N 条记录 - 使用 IndexedDB 的 auto-increment key 2. **添加数据压缩** - 使用 LZString 或类似库压缩数据 - 可以减少 50-80% 的存储空间 3. **添加数据库健康检查** - 启动时检查数据大小 - 如果过大,提示用户清理 - 提供清理按钮 ## 📊 诊断结果更新 ### 实际发现(2026-01-07) ✅ **已确认数据库状态:** - 数据库总大小:**2.4 GB**(正常应该 < 100 MB) - 单个 .ldb 文件:26-27 MB(共 40+ 个文件) - 文件结构完整,**不是损坏,而是数据过载** ✅ **根本原因确认:** 1. **自动保存机制过于频繁** - 每次测试结果变化 → 自动保存 - 每次切换标签页(visibilitychange)→ 自动保存 - 每次关闭页面(pagehide)→ 自动保存 2. **保存完整 state,没有过滤** ```typescript // useBasicSystemSession.ts:219 const snapshot = JSON.stringify(state.value) // ❌ 包含所有 testResults ``` 3. **没有任何大小限制** - ❌ 没有检查 testResults 的大小 - ❌ 没有截断超长文本 - ❌ 没有限制字段长度 4. **没有清理机制** ```bash grep -r "清理\|cleanup\|clean" packages/ui/src/stores/session # 结果:No matches found ❌ ``` 详细分析见:`docs/workspace/why-data-accumulates.md` ## 📊 下一步行动 1. ✅ 创建了 `find-db.html` - 帮助用户找到数据库文件 2. ✅ 创建了 `db-repair.html` - 数据库修复和清理工具 3. ✅ 分析了数据累积的根本原因 4. ⏳ 用户使用修复工具清理数据库 5. ⏳ 实施预防措施(数据大小限制、自动清理) ## 🔗 相关文件 - `packages/ui/src/stores/session/useBasicSystemSession.ts:211` - `packages/ui/src/stores/session/useBasicUserSession.ts` - `packages/ui/src/stores/session/useProVariableSession.ts` - `packages/ui/src/stores/session/useProMultiMessageSession.ts` - `packages/ui/src/stores/session/useImageText2ImageSession.ts` - `packages/ui/src/stores/session/useImageImage2ImageSession.ts` - `packages/ui/src/stores/session/useSessionManager.ts:324` (saveAllSessions) ### 1.5 图像存储解决方案 # 📋 图像存储优化方案 ## 问题分析 当前实现:直接在 session 中存储图像的 base64(2-3 MB),导致: - 单次保存 26 MB - 42 次保存 = 1.1 GB - Compaction 失败 - 数据库崩溃 ## 推荐方案 ### ✅ 方案1:图像单独存储(推荐) #### 实现思路 ``` 1. 创建独立的 images object store 2. Session 只保存图像 ID(字符串引用) 3. 图像和 session 分离存储 4. 定期清理旧的图像 ``` #### 代码结构 ```typescript // 1. 创建 ImageStorageService class ImageStorageService { private readonly IMAGE_STORE = 'images' // 保存图像,返回 ID async saveImage(imageResult: ImageResult): Promise { const db = await this.openDB() const id = `img_${Date.now()}_${Math.random().toString(36).slice(2)}` await db.put(this.IMAGE_STORE, { id, data: imageResult, // 完整的图像数据 createdAt: Date.now() }) return id } // 读取图像 async getImage(id: string): Promise { const db = await this.openDB() const record = await db.get(this.IMAGE_STORE, id) return record?.data || null } // 清理旧图像(保留最近 N 张) async cleanupOldImages(keepCount: number = 10): Promise { const db = await this.openDB() const tx = db.transaction(this.IMAGE_STORE, 'readwrite') const store = tx.objectStore(this.IMAGE_STORE) // 获取所有图像,按时间排序 const allImages = await store.getAll() allImages.sort((a, b) => a.createdAt - b.createdAt) // 删除旧的 const toDelete = allImages.slice(0, -keepCount) for (const img of toDelete) { await store.delete(img.id) } } } // 2. 修改 ImageResult 接口 interface ImageResultRef { imageId: string // 只保存 ID thumbnail?: string // 缩略图(可选,10KB以内) } // 3. 修改 Session interface ImageText2ImageSessionState { // ...其他字段 originalImageResult: ImageResultRef | null // 只保存引用 optimizedImageResult: ImageResultRef | null // 只保存引用 } // 4. 保存图像时的流程 async function handleImageGenerated(result: ImageResult) { // 保存到独立的图像存储 const imageId = await imageStorageService.saveImage(result) // Session 只保存引用 session.updateOriginalImageResult({ imageId, thumbnail: result.images[0].b64?.slice(0, 1000) // 只保存前1KB作为预览 }) } ``` #### 优点 ✅ Session 数据小(几 KB) ✅ 图像独立管理,可单独清理 ✅ 不影响 Compaction ✅ 可以实施 LRU 缓存策略 #### 缺点 ⚠️ 需要额外的清理逻辑 ⚠️ 增加复杂度 --- ### ✅ 方案2:限制图像数量 + 覆盖策略 #### 实现思路 ``` 1. Session 中保存图像,但只保留最近 N 张 2. 超过限制时,删除最早的 3. 总大小可控 ``` #### 代码实现 ```typescript class ImageSessionManager { private readonly MAX_IMAGES = 3 // 最多保留3张图像 private readonly MAX_IMAGE_SIZE = 500 * 1024 // 单张最大 500KB async updateImageResult( session: ImageText2ImageSessionState, newResult: ImageResult ): Promise { // 1. 限制单张图像大小 const limitedResult = this.limitImageSize(newResult) // 2. 获取现有图像列表 const imageList = session.imageList || [] // 3. 添加新图像 imageList.push({ ...limitedResult, id: `img_${Date.now()}`, createdAt: Date.now() }) // 4. 只保留最近 N 张 const keepCount = Math.min(imageList.length, this.MAX_IMAGES) const trimmedList = imageList.slice(-keepCount) // 5. 更新 session session.imageList = trimmedList // 6. 如果需要,清理 IndexedDB 中的旧图像 await this.cleanupOldImages(imageList, trimmedList) } private limitImageSize(result: ImageResult): ImageResult { return { ...result, images: result.images.map(img => ({ ...img, // 如果 base64 太大,截断或丢弃 b64: img.b64 && img.b64.length > this.MAX_IMAGE_SIZE ? undefined : img.b64, // 优先使用 URL url: img.url || img.b64 // 如果有 b64,生成 Blob URL })) } } } // 修改 Session 接口 interface ImageText2ImageSessionState { // 不再是 single result,而是 list imageList: ImageResultItem[] currentImageId?: string // 当前选中的图像 } ``` #### 优点 ✅ 实现相对简单 ✅ 总大小可控(3 × 500KB = 1.5 MB) ✅ 不需要额外的 object store #### 缺点 ⚠️ 丢失历史图像 ⚠️ 用户体验可能受影响 --- ### ✅ 方案3:只保存 URL,不保存 base64 #### 实现思路 ``` 1. 图像 API 通常返回 URL(如 OpenAI 的临时 URL) 2. 只保存 URL,不保存 base64 3. 如果需要持久化,让用户手动下载 ``` #### 代码实现 ```typescript interface ImageResultItem { url?: string // 优先使用 URL b64?: string // ⚠️ 不保存 base64 mimeType?: string expiresAt?: number // URL 过期时间 } // 保存时 async function handleImageGenerated(result: ImageResult) { // 只保存 URL,丢弃 base64 const limitedResult = { ...result, images: result.images.map(img => ({ url: img.url, mimeType: img.mimeType, b64: undefined // ❌ 不保存 base64 })) } session.updateOriginalImageResult(limitedResult) } // 显示时 function displayImage(imageRef: ImageResultItem) { if (imageRef.url) { // 使用 URL return } else if (imageRef.b64) { // 如果有 base64(旧数据兼容) return } else { // 都没有,提示重新生成 return
图像已过期,请重新生成
} } ``` #### 优点 ✅ 实现最简单 ✅ Session 数据最小 ✅ 完全避免大文件问题 #### 缺点 ❌ URL 会过期(OpenAI 的 URL 1小时后失效) ❌ 用户关闭页面后,图像丢失 ❌ 用户体验差 --- ### ✅ 方案4:File System Access API(仅桌面版) #### 实现思路 ``` 1. 使用 File System Access API 2. 让用户选择保存位置 3. 图像直接保存到用户磁盘 4. Session 只保存文件路径 ``` #### 代码实现 ```typescript // 仅在 Electron/桌面应用中可用 async function saveImageToFile(result: ImageResult) { // 请求用户选择保存位置 const fileHandle = await window.showSaveFilePicker({ suggestedName: `image-${Date.now()}.png`, types: [{ description: 'PNG Image', accept: {'image/png': ['.png']} }] }) // 保存图像 const blob = await fetch(result.images[0].url).then(r => r.blob()) const writable = await fileHandle.createWritable() await writable.write(blob) await writable.close() // Session 只保存文件路径 session.updateOriginalImageResult({ filePath: fileHandle.name, fileType: 'local' }) } ``` #### 优点 ✅ 不占用浏览器存储 ✅ 图像永久保存 ✅ 大小不受限制 #### 缺点 ❌ 只在支持 File System Access API 的浏览器可用 ❌ 需要用户手动选择 ❌ Web 版不支持 --- ## 🎯 推荐实施步骤 ### 短期(立即修复) **方案2:限制图像数量** - 只保留最近 3 张图像 - 限制单张 500 KB - 总大小 < 1.5 MB - 实施简单,立即见效 ### 中期(优化体验) **方案1:图像单独存储** - 创建独立的 image store - Session 只保存引用 - 实施 LRU 缓存 - 保留最近 10-20 张图像 ### 长期(最佳体验) **方案1 + 方案4 组合** - Web 版:图像单独存储(IndexedDB) - 桌面版:File System Access API - 提供用户选择:自动管理 / 手动保存 ## 📝 代码改动清单 ### 必须修改的文件 1. `packages/core/src/services/image/types.ts` - 添加 `ImageResultRef` 接口 2. `packages/ui/src/stores/session/useImageText2ImageSession.ts` - 修改 `ImageText2ImageSessionState` - 改为只保存引用 3. `packages/ui/src/stores/session/useImageImage2ImageSession.ts` - 同上 4. `packages/core/src/services/storage/` (新增) - 创建 `ImageStorageService` 5. `packages/ui/src/components/.../ImageWorkspace.vue` - 修改保存/读取逻辑 ## 🔧 实施优先级 ### P0(立即) - ✅ 方案2:限制图像数量到 3 张 - ✅ 限制单张图像 500 KB ### P1(本周) - ✅ 方案1:创建 ImageStorageService - ✅ 迁移到引用模式 ### P2(下周) - ✅ 添加清理逻辑 - ✅ 添加用户下载按钮 - ✅ 桌面版集成 File System Access API --- ## 总结 | 方案 | 复杂度 | 效果 | 推荐度 | |------|--------|------|--------| | 方案1:单独存储 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ 强烈推荐 | | 方案2:限制数量 | ⭐⭐ | ⭐⭐⭐⭐ | ✅ 短期推荐 | | 方案3:只保存URL | ⭐ | ⭐⭐ | ❌ 不推荐 | | 方案4:文件系统 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ 桌面推荐 | --- ## 2. 模式术语迁移总结 # 模式术语统一迁移总结 ## 📋 迁移概述 本次迁移旨在统一项目中的模式术语,将过时或语义不清的 `optimizationMode`、`contextMode`、`selectedOptimizationMode` 等表达,逐步对齐到 `functionMode`(一级功能模式)与 `subMode`(二级子模式)的设计,并确保各模式的子模式状态独立持久化。 说明:该文档保留在 `docs/workspace/` 作为“模式术语与迁移现状”的单点入口;其中的“待办/清理项”只保留仍然有效的内容,避免对过往实现阶段产生误导。 ## 🎯 统一设计架构 ### 核心概念 - **functionMode**: 一级功能模式 (`basic` | `pro` | `image`) - **subMode**: 二级子模式,根据 functionMode 而定 - 基础模式子模式 (`system` | `user`) - 上下文模式子模式 (`system` | `user`) - 图像模式子模式 (`text2image` | `image2image`) ### 统一管理函数 所有模式状态应使用 `packages/ui/src/composables/mode/` 下的函数: ```typescript // 功能模式管理 useFunctionMode(services) // { functionMode, setFunctionMode, ... } // 子模式管理(独立持久化) useBasicSubMode(services) // 基础模式子模式 useProSubMode(services) // 上下文模式子模式 useImageSubMode(services) // 图像模式子模式 // 只读访问(无需 services) useCurrentMode() // { functionMode, proSubMode, isBasicMode, ... } ``` ## ✅ 当前实现现状(已落地) ### 1) 主装配位置从 Web/Extension App.vue 收敛到 UI 主组件 - `packages/web/src/App.vue`、`packages/extension/src/App.vue` 目前仅作为壳组件渲染 UI 主应用。 - 真实的模式状态管理与装配已收敛到: - `packages/ui/src/components/app-layout/PromptOptimizerApp.vue` ### 2) 子模式持久化已实现,且 `selectedOptimizationMode` 已改为 computed(非独立状态源) - `PromptOptimizerApp.vue` 使用 `useFunctionMode` + `useBasicSubMode/useProSubMode/useImageSubMode` 管理状态,并做独立持久化。 - `selectedOptimizationMode` 不再是独立 `ref`,而是从 subMode 推导的 `computed`(兼容旧接口/props 形态)。 ## ✅ 已完成的迁移 ### 1. Composables 参数统一 - **usePromptOptimizer**: `selectedOptimizationMode` → `optimizationMode` - **usePromptTester**: `selectedOptimizationMode` → `optimizationMode` - **useContextManagement**: 添加 @deprecated 标记 ### 2. 内部变量名统一 - `usePromptTester.ts` 中所有 `selectedOptimizationMode.value` → `optimizationMode.value` ### 3. 文档和注释更新 - 为迁移的参数添加 @deprecated 标记 - 更新 JSDoc 注释,说明统一使用 subMode 概念 - 在 `PromptOptimizerApp.vue` 中保留必要的兼容性注释(以反映真实装配位置) ## 🔍 仍需迁移的区域 ### 高优先级(清理与一致性) 1. **逐步移除命名上的“误导”** - `selectedOptimizationMode` 虽已是 computed,但命名仍容易让人误以为它是“用户选择的优化模式状态源”。 - 建议方向: - 对外仍可维持 `optimizationMode` props(避免大范围破坏性改动) - 内部逐步改为更准确的命名(例如 `currentOptimizationMode` / `derivedOptimizationMode`),并集中在 UI 层统一出口 2. **组件/模板中的旧名收敛** - 将零散的 `optimizationMode/contextMode/selectedOptimizationMode` 相关命名逐步统一为“functionMode/subMode 派生值”的表达(不强求一次性替换,但要避免继续引入新旧混用) ### 中优先级 3. **类型定义中的过时术语** - 检查 `packages/ui/src/types/components.ts` - 检查 `packages/core/src/types/` 相关文件 4. **测试文件中的术语** - 更新测试用例中的变量名和断言 ### 低优先级 5. **国际化文件** - 检查 `packages/ui/src/i18n/locales/` 中的键名 - 确保文档和帮助文本使用统一术语 ## 🚀 迁移建议 ### 重点方向:以“不破坏行为”为前提做术语清理 1. 将“状态源”限定为 `functionMode + 各自 subMode`(已完成) 2. 将“对外接口/props”保持可用,同时逐步减少旧命名在内部传播(进行中) 3. 等调用方与文档一致后,再移除 `@deprecated` 标记(后续清理) ## 📝 迁移检查清单 - [x] 更新 usePromptOptimizer 参数 - [x] 更新 usePromptTester 参数 - [x] 更新 useContextManagement 接口 - [x] 统一内部变量名 - [x] 添加 @deprecated 标记 - [x] 模式管理收敛到 PromptOptimizerApp(由 subMode 推导 optimizationMode) - [ ] 更新所有 Vue 模板/props 命名(逐步收敛,避免引入新旧混用) - [ ] 更新类型定义 - [ ] 更新测试文件 - [ ] 验证功能完整性 - [ ] 更新文档 ## 🎯 预期收益 1. **术语统一**: 消除混淆,提高代码可读性 2. **架构清晰**: 明确的层级关系(functionMode → subMode) 3. **状态隔离**: 不同功能模式的子模式独立持久化 4. **开发体验**: 统一的 API 和清晰的使用模式 ## 🔗 相关文档 - [功能模式设计文档](../archives/126-submode-persistence/README.md) - [模式管理 API](../../../packages/ui/src/composables/mode/index.ts) - [上下文 UI 改造与变量系统重构(归档)](../archives/128-context-ui-and-variable-system-refactor/README.md) --- **文档版本**: v1.0 **创建时间**: 2025-10-31 **最近更新**: 2025-12-19 **维护者**: 用户 --- ## 3. 提示词文档 # 角色设定 你是一个基于E4-D方法论的智能提示词优化引擎,实现分解→诊断→开发→交付的全流程自动化优化。 # 核心参数配置 **基础参数** - 原始提示词:{{originalPrompt}} - 目标平台:{{targetPlatform}}(GPT-4/Claude-3/Gemini-Pro/通用) - 优化模式:{{optimizationMode}}(DETAIL/BASIC/AUTO) **E4-D专项参数** - 分解深度:{{decomposeDepth}}(浅层/标准/深度) - 诊断维度:{{diagnoseDimensions}}(清晰度/完整性/结构性) - 开发策略:{{developStrategy}}(思维链/少样本/多视角/混合) - 交付标准:{{deliverStandard}}(基础/专业/企业级) **扩展参数** - 任务类型:{{taskType}}(创意生成/技术分析/教育解释/复杂推理) - 输出格式:{{outputFormat}}(Markdown/JSON/纯文本/结构化报告) - 语言风格:{{languageStyle}}(专业/通俗/学术/商务) - 迭代上限:{{maxIterations}}(1-5轮,默认3轮) # E4-D自动化优化流程 **阶段一:分解(Decompose)** - 语义解构:解析{{originalPrompt}}的核心意图与隐含需求 - 要素提取:识别关键实体、操作指令、约束条件 - 边界划定:根据{{decomposeDepth}}确定分析粒度 **阶段二:诊断(Diagnose)** - 多维评估:基于{{diagnoseDimensions}}进行缺陷量化分析 - 问题定位:识别模糊点、歧义项、结构缺陷 - 优先级排序:按影响程度分类处理优化重点 **阶段三:开发(Develop)** - 策略匹配:根据{{developStrategy}}选择最优架构模式 - 模板注入:动态绑定标准化优化模板 - 平台适配:针对{{targetPlatform}}注入兼容层 **阶段四:交付(Deliver)** - 质量验证:按照{{deliverStandard}}进行输出检验 - 格式标准化:确保符合{{outputFormat}}要求 - 迭代判断:未达阈值时自动触发下一轮E4-D循环 # 质量保障机制 **自动评估指标** - 意图达成率(≥95%) - 结构完整性(≥90%) - 平台兼容性(≥95%) - 风格一致性(≥90%) **迭代优化逻辑** - 每轮E4-D流程后重新评估质量得分 - 根据薄弱环节调整下一轮优化策略 - 达到{{qualityThreshold}}或{{maxIterations}}后终止 # 输出交付物 **最终产物包含** - 优化后的提示词(标记E4-D迭代版本) - E4-D流程报告(各阶段执行详情) - 质量认证证书(四维度达标状态) - 参数使用摘要(所有配置参数效果分析) --- ## 4. 代码审查提示词评估 # 代码审查报告:新增提示词评估类型(prompt-only / prompt-iterate) 日期:2025-12-20 分支:`develop` 基线提交:`390545b`(工作区存在未提交变更) ## 1. 范围与目标 本次审查覆盖当前工作区代码变更(未提交),核心目标是: - 在“评估(Evaluation)”能力中新增两类评估: - `prompt-only`:仅根据提示词本身评估质量,不依赖测试结果 - `prompt-iterate`:在“迭代需求(iterationNote)”背景下评估提示词改进程度 - 在 UI 中新增「分析」入口与评分徽章展示,并通过 `provide/inject` 共享评估上下文,减少多层组件传递评估 props。 > 备注:本报告聚焦功能一致性、正确性与可维护性;不包含运行时验证(未执行 pnpm 指令)。 ## 1.1 更新说明(重要) - 第 4 节为“问题清单(含风险)”,记录审查时发现的缺陷与建议。 - 由于后续已有代码修复/解释补充,本报告新增第 8 节“修复状态(更新记录)”。 - 若第 4 节的“建议/风险”与第 8 节内容存在冲突,请以第 8 节的“当前实现状态”为准,并据此做回归验证。 ## 2. 变更摘要(按模块) ### 2.1 Core:评估类型、校验、上下文构建 - 扩展评估类型联合:`EvaluationType` 增加 `prompt-only`、`prompt-iterate`(`packages/core/src/services/evaluation/types.ts:14`)。 - 新增请求类型: - `PromptOnlyEvaluationRequest`:要求 `optimizedPrompt`,不要求 `testResult`(`packages/core/src/services/evaluation/types.ts:145`) - `PromptIterateEvaluationRequest`:要求 `optimizedPrompt` + `iterateRequirement`(`packages/core/src/services/evaluation/types.ts:156`) - `EvaluationService.validateRequest()` 增加上述两种类型的字段校验(`packages/core/src/services/evaluation/service.ts:159`)。 - `EvaluationService.buildTemplateContext()` 为上述两种类型注入模板上下文: - prompt-only:`optimizedPrompt` - prompt-iterate:`optimizedPrompt` + `iterateRequirement`(`packages/core/src/services/evaluation/service.ts:270`)。 - 多处错误文案由中文改为英文(例如校验/解析错误)(`packages/core/src/services/evaluation/service.ts:160`、`packages/core/src/services/evaluation/service.ts:385`)。 ### 2.2 Core:内置模板注册 新增内置评估模板(basic/pro × system/user × zh/en × prompt-only/prompt-iterate),并注册到默认模板集合: - 导出聚合:`packages/core/src/services/template/default-templates/evaluation/index.ts` - 静态模板集合:`packages/core/src/services/template/default-templates/index.ts` - 模板示例: - `evaluation-basic-system-prompt-only`(`packages/core/src/services/template/default-templates/evaluation/basic/system/evaluation-prompt-only.ts`) - `evaluation-pro-system-prompt-iterate`(`packages/core/src/services/template/default-templates/evaluation/pro/system/evaluation-prompt-iterate.ts`) 注意:`TemplateManager.getBuiltinTemplates()` 会根据“当前语言”选择模板集合(`packages/core/src/services/template/manager.ts:208`),因此模板 **ID 必须在不同语言集合中一致**;目前 en 文件的 `id` 与 zh 文件一致(例如 `evaluation-basic-system-original`),符合该机制。 ### 2.3 Core:单元测试 - 新增 `packages/core/tests/unit/evaluation/service.test.ts`,覆盖: - `prompt-only/prompt-iterate` 校验规则(包括不要求 `testResult`、`iterateRequirement` 必填) - 模板 ID 生成与模板拉取是否按预期发生 - `evaluateStream` 回调路径(`packages/core/tests/unit/evaluation/service.test.ts:73`)。 ### 2.4 UI:评估 composable 扩展与上下文注入 - `useEvaluation`: - 扩展状态 `state['prompt-only']`、`state['prompt-iterate']` - 新增计算属性(分数/等级/是否评估中/是否有结果) - 新增方法 `evaluatePromptOnly()`、`evaluatePromptIterate()` - `executeEvaluation()` 的 request 类型由联合改为 `EvaluationRequest`(`packages/ui/src/composables/prompt/useEvaluation.ts:375`)。 - 新增评估上下文: - `provideEvaluation()` / `useEvaluationContext()` / `useEvaluationContextOptional()`(`packages/ui/src/composables/prompt/useEvaluationContext.ts:28`)。 - `PromptOptimizerApp` 提供上下文: - `provideEvaluation(evaluation)`(`packages/ui/src/components/app-layout/PromptOptimizerApp.vue:993`)。 - i18n 增加文案: - `prompt.analyze` - `prompt.error.noOptimizedPrompt`(`packages/ui/src/i18n/locales/zh-CN.ts:1131`、`packages/ui/src/i18n/locales/en-US.ts:1163`)。 ### 2.5 UI:PromptPanel 增加“分析入口”与评分徽章 - `PromptPanel`: - 通过 `useEvaluationContextOptional()` 读取上下文(`packages/ui/src/components/PromptPanel.vue:358`)。 - 计算评估类型:若当前版本存在 `iterationNote`,使用 `prompt-iterate`,否则 `prompt-only`(`packages/ui/src/components/PromptPanel.vue:371`)。 - 入口 UI: - 若有结果或正在评估:展示 `EvaluationScoreBadge` - 否则:展示「分析」按钮(`packages/ui/src/components/PromptPanel.vue:122`)。 - 点击「分析」: - 若 `optimizedPrompt` 为空,toast `prompt.error.noOptimizedPrompt` - 否则按是否有 `iterationNote` 调用 `evaluation.evaluatePromptOnly/Iterate`(`packages/ui/src/components/PromptPanel.vue:489`)。 ## 3. 关键链路梳理(用于定位问题) ### 3.1 Core 评估执行链路 1) UI 组装 `EvaluationRequest` 2) `EvaluationService.validateRequest()` 校验必要字段 3) 根据 `mode` + `type` 组装模板 ID:`evaluation-{functionMode}-{subMode}-{type}`(`packages/core/src/services/evaluation/service.ts:263`) 4) `TemplateManager.getTemplate(id)`:按语言选择内置模板集合,并用相同的 `id` 查找(`packages/core/src/services/template/manager.ts:208`) 5) `buildTemplateContext()` 注入字段(`optimizedPrompt` / `iterateRequirement` 等) 6) 调用 LLM(stream 或非 stream) 7) `parseEvaluationResult()` → `normalizeEvaluationResponse()` 规范化输出(`packages/core/src/services/evaluation/service.ts:331`)。 ### 3.2 UI 展示链路(新类型) - `PromptOptimizerApp`:统一持有 `evaluation` 实例,并通过 `provideEvaluation()` 注入 - `PromptPanel`:直接通过 `inject` 调用评估方法并展示结果徽章 - `EvaluationPanel`:仍由顶层统一展示(依赖 `evaluation.state.activeDetailType`、`evaluation.activeResult` 等)。 ## 3.3 设计说明:为什么“不同模式格式不同”不必导致“多套评估实例” 不同模式(basic/pro、system/user)在“优化对象形态、评估维度、上下文信息”上确实可能不同,但在当前架构下,这些差异主要由“请求参数 + 模板选择 + 上下文注入”解决,不必通过“每个 Workspace 各自一套 evaluation 实例”解决。 - **模板选择天然区分模式**:Core 通过 `evaluation-{functionMode}-{subMode}-{type}` 生成模板 ID,不同模式会命中不同模板(`packages/core/src/services/evaluation/service.ts:263`)。 - **上下文差异通过 `proContext` 注入**:Pro-System 需要多消息上下文,Pro-User 需要变量解析上下文。当前通过 `provideProContext()` 在 Workspace 提供,并在 `PromptPanel` 评估时读取注入(`packages/ui/src/components/context-mode/ContextSystemWorkspace.vue:420`、`packages/ui/src/components/PromptPanel.vue:363`、`packages/ui/src/components/PromptPanel.vue:489`)。 - **输出结构被统一规范化**:模板可返回不同 `dimensions[]`,但最终都会被规范化为统一的 `EvaluationResponse` 结构,UI 可复用同一渲染组件(`packages/core/src/services/evaluation/service.ts:394`、`packages/core/src/services/evaluation/types.ts:206`)。 结论:建议“全局一套 evaluation(App-level)+ provide/inject”,用 `mode/proContext/type` 适配不同模式差异;这样能避免 Context 模式出现“双套评估状态/双面板”的割裂问题(见第 9 节)。 ## 4. 主要问题与风险(按优先级) ### P0:评估面板“重新评估”对新类型无效(功能缺口) **状态**:✅ 已修复(见第 8 节“P0-1”) **现象** - 在 `EvaluationPanel` 中触发 “重新评估(re-evaluate)” 时,若当前详情类型为 `prompt-only` 或 `prompt-iterate`,不会重新发起请求。 **原因定位** - `handleReEvaluate()` 读取 `evaluation.state.activeDetailType` 并调用 `handleEvaluate(currentType)`(`packages/ui/src/composables/prompt/useEvaluationHandler.ts:220`)。 - 但 `handleEvaluate(type)` 只处理 `original/optimized/compare` 三种类型(`packages/ui/src/composables/prompt/useEvaluationHandler.ts:183`),对新类型没有分支,等同于“无操作返回”。 **影响** - 用户从详情面板复评新类型无响应,体验不一致; - 若未来 `EvaluationScoreBadge` 也依赖 `EvaluationPanel` 复评链路,问题将进一步扩大。 **建议** - 在 `useEvaluationHandler.handleEvaluate()` 增加对 `prompt-only/prompt-iterate` 的分支,并考虑从状态或上下文中取得 `iterateRequirement`(或由 UI 提供)。 --- ### P0:Context 模式下 “@analyze” 监听与 proContext 传递存在不一致/死代码 **状态**:✅ 已修复(见第 8 节“P0-2”) **现象** - `ContextSystemWorkspace` 与 `ContextUserWorkspace` 监听 `@analyze="handleAnalyze"`,并在 `handleAnalyze` 中调用 `evaluation.evaluatePromptOnly/Iterate` 且传入 `proContext`(`packages/ui/src/components/context-mode/ContextSystemWorkspace.vue:518`、`packages/ui/src/components/context-mode/ContextUserWorkspace.vue:769`)。 - 但 `PromptPanel` 并未定义/emit `analyze` 事件(`packages/ui/src/components/PromptPanel.vue:413`),点击「分析」走的是 `handleEvaluate()` 直接调用 `evaluation.evaluatePromptOnly/Iterate`,且未传 `proContext`(`packages/ui/src/components/PromptPanel.vue:489`)。 **影响** - `@analyze` 监听逻辑大概率不会触发,成为“死代码”; - Pro 模式模板对 `proContext` 依赖较强(尤其 `pro-system` 场景,用于多消息上下文理解),未传会降低评估质量。 **建议(历史记录)** - 原建议为“事件驱动”或“上下文直连”二选一避免双轨;当前实现已选择“上下文直连”,并通过 `provide/inject` 共享 `proContext`(见第 8 节“P0-2”)。 --- ### P0:新类型评估结果可能与当前展示内容不一致(旧分数残留风险) **状态**:✅ 已修复(见第 8 节“P0-3”) **现象** - `PromptPanel` 徽章展示基于 `evaluation.state['prompt-only'|'prompt-iterate']` 是否已有结果(`packages/ui/src/components/PromptPanel.vue:399`)。 - 当切换版本/切换消息/替换 `optimizedPrompt` 时,如果没有明确清理对应评估状态,徽章可能展示上一条内容的分数与详情。 **当前已有防护** - 顶层仅对 `optimizer.optimizedPrompt` 做了 watch 并清理 `prompt-only/prompt-iterate`(`packages/ui/src/components/app-layout/PromptOptimizerApp.vue:1340`)。 **风险点** - Context 模式下 `PromptPanel` 的 `optimizedPrompt` 来自 `displayAdapter.displayedOptimizedPrompt`(`packages/ui/src/components/context-mode/ContextSystemWorkspace.vue:102`),不一定会触发上述 watch; - 即使触发,`PromptPanel` 内部也没有基于 `currentVersionId` 或 `selectedMessage` 的精确清理逻辑。 **建议** - 在 `PromptPanel` 内部针对 `optimizedPrompt`、`currentVersionId`、`versions`(或等价“内容标识”)做 watch,主动清空对应评估状态,确保“内容-评估结果”一致性。 --- ### P1:模板输出字段与服务规范化逻辑不一致(isOptimizedBetter 被丢弃) **现象** - `prompt-only/prompt-iterate` 模板输出 JSON 中包含 `"isOptimizedBetter"`(例如 `packages/core/src/services/template/default-templates/evaluation/basic/system/evaluation-prompt-only.ts`)。 - 但 `normalizeEvaluationResponse()` 仅在 `type === 'compare'` 时才会把 `isOptimizedBetter` 写入响应(`packages/core/src/services/evaluation/service.ts:468`)。 **影响** - 模板 token 成本增加但信息被丢弃; - 易产生误导:模板要求输出 true/false,但 UI/服务端并不消费该字段。 **建议** - 明确语义:若希望 prompt-only/prompt-iterate 也保留该字段,扩展响应结构与 UI 展示;若不需要,应移除模板中的字段要求(更省 token、更一致)。 --- ### P1:错误文案从中文切换为英文,可能造成中文界面体验割裂 **现象** - Core 抛出的校验/解析错误信息改为英文(`packages/core/src/services/evaluation/service.ts:160` 等)。 - UI toast 使用 `getErrorMessage(error)` 透传(`packages/ui/src/composables/prompt/useEvaluation.ts:410`),在中文界面下可能显示英文错误。 **影响** - 用户体验与 i18n 文案体系不一致; - 单测已锁定英文字符串,后续想恢复中文会引入测试修改成本(`packages/core/tests/unit/evaluation/service.test.ts:100`)。 **建议** - 若希望 i18n 统一:考虑在 UI 层将错误映射到本地化 key(按 error class / error code),而不是依赖错误 message 文案。 --- ### P2:PromptPanel emit 声明存在冗余/误导 **现象** - `PromptPanel` 的 `defineEmits` 新增了 `"apply-improvement"`,但注释中提到“评估相关事件(evaluate 和 show-evaluation-detail 已通过 inject 处理)”(`packages/ui/src/components/PromptPanel.vue:431`)。 - 同时 workspace 中仍出现 `@analyze` 监听(见 P0),但 `PromptPanel` 并未 emit。 **影响** - 组件接口不清晰,调用方难以判断哪些事件仍有效; - 容易引入更多“监听了但永远不触发”的事件绑定。 **建议** - 统一组件契约:对外只保留必要事件(例如 `apply-improvement`),其余通过 context 内部处理即可。 ## 5. 测试与回归关注点 ### 已覆盖 - Core `EvaluationService` 对新类型的校验、模板 ID 生成、`evaluateStream` 回调路径已有单测(`packages/core/tests/unit/evaluation/service.test.ts:73`)。 ### 建议补充(可选) - UI 层至少做一次“切换版本/切换消息后徽章不残留”的用例验证(手测即可,或后续补 e2e/组件测试)。 - Pro 模式下确认 `proContext` 在 prompt-only/prompt-iterate 评估中确实被带入,且模板渲染符合预期。 ## 6. 建议行动清单(可直接转为 TODO) 1) `useEvaluationHandler.handleEvaluate()` 支持 `prompt-only/prompt-iterate`,确保 `EvaluationPanel` 的 re-evaluate 可用。 2) 统一“分析”入口架构:删除死代码或补齐 `PromptPanel` 的 `analyze` emit,并确保 Pro 场景传递 `proContext`。 3) 在 `PromptPanel` 内增加内容变更触发的 `clearResult('prompt-only'|'prompt-iterate')`,避免旧分数残留。 4) 明确并统一 `isOptimizedBetter` 的语义(模板/服务/前端三方一致)。 5) 如需 i18n 统一,考虑“错误码/错误类型 → 文案 key”的映射策略,减少对英文 message 的依赖。 ## 7. 附录:文件变更清单 ### 已修改(M) - `packages/core/src/services/evaluation/service.ts` - `packages/core/src/services/evaluation/types.ts` - `packages/core/src/services/template/default-templates/evaluation/basic/system/index.ts` - `packages/core/src/services/template/default-templates/evaluation/basic/user/index.ts` - `packages/core/src/services/template/default-templates/evaluation/index.ts` - `packages/core/src/services/template/default-templates/evaluation/pro/system/index.ts` - `packages/core/src/services/template/default-templates/evaluation/pro/user/index.ts` - `packages/core/src/services/template/default-templates/index.ts` - `packages/ui/src/components/PromptPanel.vue` - `packages/ui/src/components/app-layout/PromptOptimizerApp.vue` - `packages/ui/src/components/basic-mode/BasicSystemWorkspace.vue` - `packages/ui/src/components/basic-mode/BasicUserWorkspace.vue` - `packages/ui/src/components/context-mode/ContextSystemWorkspace.vue` - `packages/ui/src/components/context-mode/ContextUserWorkspace.vue` - `packages/ui/src/composables/prompt/index.ts` - `packages/ui/src/composables/prompt/useEvaluation.ts` - `packages/ui/src/composables/prompt/useEvaluationHandler.ts` - `packages/ui/src/i18n/locales/en-US.ts` - `packages/ui/src/i18n/locales/zh-CN.ts` - `packages/ui/src/i18n/locales/zh-TW.ts` ### 新增(??) - `packages/core/src/services/template/default-templates/evaluation/**/evaluation-prompt-only*.ts` - `packages/core/src/services/template/default-templates/evaluation/**/evaluation-prompt-iterate*.ts` - `packages/core/tests/unit/evaluation/service.test.ts` - `packages/ui/src/composables/prompt/useEvaluationContext.ts` - `packages/ui/src/composables/prompt/useProContext.ts` --- ## 8. 修复状态(2025-12-20 更新) ### ✅ P0-1:handleReEvaluate 支持新类型(已修复) **修复内容** - 在 `useEvaluationHandler.ts` 的 `handleEvaluate()` 中添加了对 `prompt-only` 和 `prompt-iterate` 类型的处理分支 - 在 `UseEvaluationHandlerOptions` 中新增 `currentIterateRequirement` 可选参数,用于 `prompt-iterate` 类型的重新评估 - 在 `PromptOptimizerApp.vue` 中计算 `currentIterateRequirement`(从当前版本的 `iterationNote` 获取)并传递给 evaluationHandler **涉及文件** - `packages/ui/src/composables/prompt/useEvaluationHandler.ts` - `packages/ui/src/components/app-layout/PromptOptimizerApp.vue` --- ### ✅ P0-2:proContext 注入机制与死代码清理(已修复) **修复方案** 选择了"上下文直连"路径:通过 `provide/inject` 共享 `proContext`,而非事件驱动。 **修复内容** 1. 新增 `useProContext.ts`,提供 `provideProContext()` 和 `useProContextOptional()` 方法 2. 在 `ContextSystemWorkspace.vue` 和 `ContextUserWorkspace.vue` 中调用 `provideProContext(proContext)` 3. 在 `PromptPanel.vue` 中调用 `useProContextOptional()` 获取 proContext,并在评估调用时传入 4. 移除了 workspace 中的 `@analyze` 监听和 `handleAnalyze` 函数(死代码清理) 5. 将 `@analyze` 替换为 `@apply-improvement`(用于应用改进建议) **涉及文件** - `packages/ui/src/composables/prompt/useProContext.ts`(新增) - `packages/ui/src/composables/prompt/index.ts` - `packages/ui/src/components/PromptPanel.vue` - `packages/ui/src/components/context-mode/ContextSystemWorkspace.vue` - `packages/ui/src/components/context-mode/ContextUserWorkspace.vue` --- ### ✅ P0-3:内容变更清除评估结果(已修复) **修复内容** - 在 `PromptPanel.vue` 中新增 watch,监听 `optimizedPrompt` 和 `currentVersionId` 的变化 - 当内容或版本变化时,自动清除 `prompt-only` 和 `prompt-iterate` 评估结果 - 避免切换版本/消息后旧分数残留的问题 **涉及文件** - `packages/ui/src/components/PromptPanel.vue` --- ### 📋 P1-1:isOptimizedBetter 字段语义(设计决策) **决策** 保持当前行为,作为已知的设计取舍: - `prompt-only` 和 `prompt-iterate` 模板中仍输出 `isOptimizedBetter` 字段 - 服务端 `normalizeEvaluationResponse()` 仅在 `compare` 类型时保留该字段 - 前端不消费新类型的 `isOptimizedBetter` **理由** - 新类型的语义是"评估单个提示词质量",`isOptimizedBetter` 字段在此场景下意义有限 - 模板中保留该字段可作为 LLM 输出的校验锚点,不影响功能正确性 - 若后续需要展示,可在服务端和前端同步扩展 --- ### 📋 P1-2:错误文案语言(设计决策) **决策** 保持 Core 层错误使用英文,在 UI 层进行本地化映射(未来改进方向): - 当前 Core 层的校验/解析错误使用英文,便于日志分析和问题定位 - UI 层通过 `getErrorMessage(error)` 透传,中文界面下可能显示英文错误 - 这是一个可接受的临时状态,不影响核心功能 **未来改进方向** - 在 UI 层实现"错误码 → i18n key"的映射机制 - 根据错误类型或错误码选择对应的本地化文案 - 保持 Core 层错误信息稳定,避免因文案变更导致测试频繁修改 --- ### ✅ P2:PromptPanel emit 声明清理(随 P0-2 一并解决) - 移除了 workspace 中的 `@analyze` 监听 - `PromptPanel` 对外只保留必要事件:`iterate`、`switchVersion`、`save-favorite`、`apply-improvement` 等 - 评估相关逻辑通过 `provide/inject` 内部处理,无需对外暴露 ## 9. 现存问题与建议(给后续 AI 的处理指南) 本节聚焦"截至当前代码状态仍存在的问题"(以代码为准),用于指导后续 AI 做收敛与修复。 ### ✅ P0:Context 模式存在"双套 evaluation 实例 + 双面板"(已修复) **原始问题** - App 顶层已提供全局评估上下文,但 ContextSystem/ContextUser 两个 Workspace 各自创建独立 `evaluationHandler` 并渲染本地 `EvaluationPanel`,导致状态不同步。 **修复方案**(已实施) 采纳了"全局一套 evaluation + 顶层唯一 EvaluationPanel"方案: 1. **修改 `useEvaluationHandler.ts`**:新增 `externalEvaluation` 可选参数(第 57 行、第 183-188 行),允许传入外部 evaluation 实例 2. **移除 Workspace 内的 ``**: - `ContextSystemWorkspace.vue:212` - 仅保留注释说明 - `ContextUserWorkspace.vue:247` - 仅保留注释说明 3. **Workspace 使用全局 evaluation**: - `ContextSystemWorkspace.vue:417` - `const globalEvaluation = useEvaluationContext()` - `ContextSystemWorkspace.vue:446` - `externalEvaluation: globalEvaluation` - `ContextUserWorkspace.vue:523` - `const globalEvaluation = useEvaluationContext()` - `ContextUserWorkspace.vue:552` - `externalEvaluation: globalEvaluation` **验证方式** - 在 context-mode 目录搜索 ` 该问题是"全局面板事件处理器绑定到基础模式数据源"导致的模式耦合。尽管 Context Workspace 已通过 `externalEvaluation` 复用了全局 evaluation,并移除了本地面板,但 App 顶层面板的交互仍需要进一步解耦。 **代码事实** - App 顶层唯一 `EvaluationPanel` 的 `@re-evaluate` 绑定到 `handleReEvaluate`(`packages/ui/src/components/app-layout/PromptOptimizerApp.vue:583`),其实现来自 App 内部的 `evaluationHandler.handleReEvaluate()`,而该 handler 使用的数据源是 `optimizer.prompt/optimizer.optimizedPrompt/testResults`(即基础模式优化器与测试结果)。 - 在 Context 模式中,评估请求通常由 `PromptPanel` 直接使用 inject 到的全局 `evaluation` 发起,内容来源是 Context Workspace 传入的 `originalPrompt/optimizedPrompt` props(`packages/ui/src/components/PromptPanel.vue:489`)。 - 因此,当用户在 Context 模式下打开评估详情并点击"重新评估",可能会用基础模式的数据重新评估,覆盖 Context 的评估结果。 **修复方案**(已实施) 本次采用“方案 B:Provider(数据源提供者)路由”,核心原则是: - **重新评估使用最新状态**(当前工作区/当前内容),不保存/重放 lastRequest。 - 全局 `EvaluationPanel` 只做 UI,不再绑定到基础模式数据源;其事件路由到“当前活跃 Workspace”执行。 1. **`useEvaluationHandler.ts` 调整 handleReEvaluate 语义**: - 改为始终使用当前业务状态重新组装请求并执行一次评估(不依赖 lastRequest)。 2. **Context Workspaces 暴露 Provider 能力(defineExpose)**: - `reEvaluateActive()`:内部调用 `evaluationHandler.handleReEvaluate()`,使用当前 Workspace 的数据源(original/optimized/proContext/iterateRequirement 等)重新评估。 - `openIterateDialog()`:内部转发到 `PromptPanel` 的 `openIterateDialog`,用于应用改进建议时打开迭代弹窗。 3. **`PromptOptimizerApp.vue` 全局面板事件路由**: - `@re-evaluate`:根据 `functionMode/contextMode` 选择 `systemWorkspaceRef/userWorkspaceRef`(Context)或使用基础模式 handler,调用对应 provider 的 `reEvaluateActive()`。 - `@apply-improvement`:在 Context 模式下调用对应 Workspace 的 `openIterateDialog(improvement)`;基础模式继续走 `basicModeWorkspaceRef`。 **验证方式** - Context 模式下执行评估后,在全局 `EvaluationPanel` 点击“重新评估”,应重新评估当前选中消息/当前变量提示词(而非基础模式 optimizer 的数据)。 - Context 模式下在全局 `EvaluationPanel` 点击“应用改进”,应打开当前 Workspace 的迭代弹窗并预填改进建议。 --- ### ✅ P2:EvaluationPanel 标题未覆盖新类型(已修复) **原始问题** - `EvaluationPanel.vue` 标题 switch 只覆盖 `original/optimized/compare`,`prompt-only/prompt-iterate` 会落到 `evaluation.title.default`(`packages/ui/src/components/evaluation/EvaluationPanel.vue:185`)。 **修复方案**(已实施) 1. **`EvaluationPanel.vue` 添加新类型的 case**(第 188-191 行): ```typescript case 'prompt-only': return t('evaluation.title.promptOnly') case 'prompt-iterate': return t('evaluation.title.promptIterate') ``` 2. **添加 i18n 标题**: - `zh-CN.ts` - `promptOnly: "提示词质量分析"`, `promptIterate: "迭代优化分析"` - `en-US.ts` - `promptOnly: "Prompt Quality Analysis"`, `promptIterate: "Iteration Optimization Analysis"` - `zh-TW.ts` - `promptOnly: "提示詞品質分析"`, `promptIterate: "迭代優化分析"` ## 10. 使用与设计说明(面向后续维护) ### 10.1 “基础模式(basic)”怎么用(与评估关联) 典型流程(单提示词优化): 1) 输入 `originalPrompt`(原始提示词) 2) 点击“优化”得到 `optimizedPrompt`(当前显示版本) 3) (可选)在测试区运行测试得到 `testResult`(用于 original/optimized/compare 三类评估) 4) 点击“分析”执行 `prompt-only` 或 `prompt-iterate`(不依赖测试结果) 5) 在评估详情中点击“重新评估”会对“当前显示的内容 + 当前模式参数”再评估一次 这里的关键约束:**`originalPrompt` 在产品定义中始终存在**(用于对齐原始需求,避免意图偏离),因此 Core 层校验 `originalPrompt` 不能为空是合理的,不需要为所谓“仅提示词独立评估”放宽。 ### 10.2 为什么 Context 模式的 Context 不一样 Context 模式(pro)本质上不是“单提示词”,而是“带上下文的目标对象”: - **Pro-System**:目标是对话中的某条 message(system/user/assistant/tool),`proContext` 会携带“目标 message + 全对话消息列表”,便于模型理解上下文语义。 - **Pro-User**:目标是“带变量的提示词”,`proContext` 会携带变量解析信息(raw/resolved/variables),便于评估时知道占位符如何被填充。 因此: - 同一个 `EvaluationType`(比如 `prompt-only`)在不同子模式下“模板与上下文输入”可能不同; - 但服务端输出仍应通过 `EvaluationResponse` 规范化,保持 UI 展示一致(分数/建议/原因等)。 ### 10.3 重新评估(re-evaluate)为什么只需要“当前状态”,不需要 lastRequest “重新评估”的产品语义是:**再执行一次评估**,且评估对象永远是“当前 UI 正在展示的版本”。 因此实现上只需要两类信息: - “要评估哪种类型”:来自当前打开的详情类型 `evaluation.state.activeDetailType` - “要评估的输入数据”:来自当前业务状态(当前 prompt / 当前版本 / 当前 proContext / 当前 iterateRequirement 等) 之前的 `lastRequest` 方案容易引入“旧状态回放”与跨模式污染;当前实现已移除 `lastRequest`,并把 re-evaluate 变成“以当前状态重建请求并执行”,更符合产品定义。 ### 10.4 全局评估面板的设计取舍:方案 B(Provider 路由) vs 每个模式自带面板 本次已落地的是 **方案 B:全局唯一 `EvaluationPanel` + Provider 路由**: - 优点:UI 一致、状态唯一(避免双套 evaluation)、跨组件更易共享(`provide/inject`)。 - 风险:顶层需要知道“当前活跃 workspace”,并在能力缺失时按“异常 bug”处理(避免 silently fallback 用错数据源)。 备选方案(回退):每个模式各自渲染一个 `EvaluationPanel`。 - 优点:数据源天然就近,路由简单。 - 缺点:容易出现“双面板/双状态”,并带来更多模式分支与同步问题。 当前结论:在现有 UI 架构下,**优先保持方案 B**;若未来 Provider 接口进一步膨胀或难以维护,再考虑回退为“各模式自带面板”,但需要严格避免重复 evaluation 实例。