Skip to content

Latest commit

 

History

History
2157 lines (1649 loc) · 78.9 KB

File metadata and controls

2157 lines (1649 loc) · 78.9 KB

技术分析与问题解决文档

本文档整合了项目开发过程中的技术分析和问题解决方案

目录

  1. IndexedDB 数据库问题分析
  2. 图像存储解决方案
  3. 模式术语迁移总结
  4. 代码审查提示词评估

1. IndexedDB 数据库问题分析

1.1 为什么数据会累积?

📊 为什么 IndexedDB 会无限累积到 2.4 GB?

🔍 问题根源分析

1. 自动保存机制(每次变化都保存)

代码中有三个地方会自动触发保存:

// PromptOptimizerApp.vue:1954-1960
window.addEventListener('pagehide', handlePagehide)          // ① 页面卸载时
document.addEventListener('visibilitychange', handleVisibilityChange)  // ② 标签页切换时
// 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)

// 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 包含:

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. 没有任何大小限制

// 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. 没有清理机制

搜索整个代码库:

# 搜索清理相关代码
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 ⚠️⚠️

但实际情况更糟糕!

如果用户测试了一个超长输出:

// 用户让 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 模式的多轮对话:

// 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. 缺少自动清理策略

其他应用的常见做法:

// ✅ 示例:自动清理 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️⃣ 代码层面:确实是覆盖操作

// dexieStorageProvider.ts:91-95
async setItem(key: string, value: string): Promise<void> {
  await this.db.storage.put({    // ✅ put() 是覆盖操作
    key,                          // 主键相同
    value,                        // 新值
    timestamp: Date.now()
  });
}
// 数据库定义: 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️⃣ 你的数据库状态分析

$ 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 应用

// ✅ 其他应用通常这样做
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 能及时清理

你的应用

// ❌ 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. 限制单次写入大小

    if (testResults.originalResult.length > 50000) {
      testResults.originalResult = testResults.originalResult.slice(0, 50000) + '...'
    }
  2. 减少写入频率

    // 使用 debounce,每 5 秒最多保存一次
    const debouncedSave = debounce(saveSession, 5000)
  3. 定期清理旧数据

    // 只保留最近一次的测试结果
    state.value.testResults = latestResults
  4. 分离大数据存储

    // 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 为什么分离存储有效?

🔍 为什么分离存储能解决问题?

核心问题:保存频率不同

当前模式(问题)

// ❌ 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 ❌

分离模式(解决)

// ✅ 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):

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 可能包含超大对象

问题场景:

// useBasicSystemSession.ts
state.value.testResults = results  // 可能包含完整的输出文本

如果用户测试了很长的输出(如 GPT-4 的长回答):

  • 单个 testResult 可能 1-5 MB
  • 多次测试累积可能达到 50-100 MB
  • JSON.stringify 时内存暴涨

4. 没有数据清理机制

  • 历史测试数据一直累积
  • 没有大小上限检查
  • 没有定期清理旧数据

🛠️ 解决方案

立即修复 (高优先级)

  1. 添加安全的序列化函数
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;
  }
}
  1. 限制 testResults 大小
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;
}
  1. 添加保存前的验证
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);
  }
}
  1. 添加定期清理
// 清理超过 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,没有过滤

    // useBasicSystemSession.ts:219
    const snapshot = JSON.stringify(state.value)  // ❌ 包含所有 testResults
  3. 没有任何大小限制

    • ❌ 没有检查 testResults 的大小
    • ❌ 没有截断超长文本
    • ❌ 没有限制字段长度
  4. 没有清理机制

    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. 定期清理旧的图像

代码结构

// 1. 创建 ImageStorageService
class ImageStorageService {
  private readonly IMAGE_STORE = 'images'

  // 保存图像,返回 ID
  async saveImage(imageResult: ImageResult): Promise<string> {
    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<ImageResult | null> {
    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<void> {
    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. 总大小可控

代码实现

class ImageSessionManager {
  private readonly MAX_IMAGES = 3  // 最多保留3张图像
  private readonly MAX_IMAGE_SIZE = 500 * 1024  // 单张最大 500KB

  async updateImageResult(
    session: ImageText2ImageSessionState,
    newResult: ImageResult
  ): Promise<void> {
    // 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. 如果需要持久化,让用户手动下载

代码实现

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 <img src={imageRef.url} />
  } else if (imageRef.b64) {
    // 如果有 base64(旧数据兼容)
    return <img src={imageRef.b64} />
  } else {
    // 都没有,提示重新生成
    return <div>图像已过期,请重新生成</div>
  }
}

优点

✅ 实现最简单 ✅ Session 数据最小 ✅ 完全避免大文件问题

缺点

❌ URL 会过期(OpenAI 的 URL 1小时后失效) ❌ 用户关闭页面后,图像丢失 ❌ 用户体验差


✅ 方案4:File System Access API(仅桌面版)

实现思路

1. 使用 File System Access API
2. 让用户选择保存位置
3. 图像直接保存到用户磁盘
4. Session 只保存文件路径

代码实现

// 仅在 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. 模式术语迁移总结

模式术语统一迁移总结

📋 迁移概述

本次迁移旨在统一项目中的模式术语,将过时或语义不清的 optimizationModecontextModeselectedOptimizationMode 等表达,逐步对齐到 functionMode(一级功能模式)与 subMode(二级子模式)的设计,并确保各模式的子模式状态独立持久化。

说明:该文档保留在 docs/workspace/ 作为“模式术语与迁移现状”的单点入口;其中的“待办/清理项”只保留仍然有效的内容,避免对过往实现阶段产生误导。

🎯 统一设计架构

核心概念

  • functionMode: 一级功能模式 (basic | pro | image)
  • subMode: 二级子模式,根据 functionMode 而定
    • 基础模式子模式 (system | user)
    • 上下文模式子模式 (system | user)
    • 图像模式子模式 (text2image | image2image)

统一管理函数

所有模式状态应使用 packages/ui/src/composables/mode/ 下的函数:

// 功能模式管理
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.vuepackages/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: selectedOptimizationModeoptimizationMode
  • usePromptTester: selectedOptimizationModeoptimizationMode
  • useContextManagement: 添加 @deprecated 标记

2. 内部变量名统一

  • usePromptTester.ts 中所有 selectedOptimizationMode.valueoptimizationMode.value

3. 文档和注释更新

  • 为迁移的参数添加 @deprecated 标记
  • 更新 JSDoc 注释,说明统一使用 subMode 概念
  • PromptOptimizerApp.vue 中保留必要的兼容性注释(以反映真实装配位置)

🔍 仍需迁移的区域

高优先级(清理与一致性)

  1. 逐步移除命名上的“误导”

    • selectedOptimizationMode 虽已是 computed,但命名仍容易让人误以为它是“用户选择的优化模式状态源”。
    • 建议方向:
      • 对外仍可维持 optimizationMode props(避免大范围破坏性改动)
      • 内部逐步改为更准确的命名(例如 currentOptimizationMode / derivedOptimizationMode),并集中在 UI 层统一出口
  2. 组件/模板中的旧名收敛

    • 将零散的 optimizationMode/contextMode/selectedOptimizationMode 相关命名逐步统一为“functionMode/subMode 派生值”的表达(不强求一次性替换,但要避免继续引入新旧混用)

中优先级

  1. 类型定义中的过时术语

    • 检查 packages/ui/src/types/components.ts
    • 检查 packages/core/src/types/ 相关文件
  2. 测试文件中的术语

    • 更新测试用例中的变量名和断言

低优先级

  1. 国际化文件
    • 检查 packages/ui/src/i18n/locales/ 中的键名
    • 确保文档和帮助文本使用统一术语

🚀 迁移建议

重点方向:以“不破坏行为”为前提做术语清理

  1. 将“状态源”限定为 functionMode + 各自 subMode(已完成)
  2. 将“对外接口/props”保持可用,同时逐步减少旧命名在内部传播(进行中)
  3. 等调用方与文档一致后,再移除 @deprecated 标记(后续清理)

📝 迁移检查清单

  • 更新 usePromptOptimizer 参数
  • 更新 usePromptTester 参数
  • 更新 useContextManagement 接口
  • 统一内部变量名
  • 添加 @deprecated 标记
  • 模式管理收敛到 PromptOptimizerApp(由 subMode 推导 optimizationMode)
  • 更新所有 Vue 模板/props 命名(逐步收敛,避免引入新旧混用)
  • 更新类型定义
  • 更新测试文件
  • 验证功能完整性
  • 更新文档

🎯 预期收益

  1. 术语统一: 消除混淆,提高代码可读性
  2. 架构清晰: 明确的层级关系(functionMode → subMode)
  3. 状态隔离: 不同功能模式的子模式独立持久化
  4. 开发体验: 统一的 API 和清晰的使用模式

🔗 相关文档


文档版本: 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-onlyprompt-iteratepackages/core/src/services/evaluation/types.ts:14)。
  • 新增请求类型:
    • PromptOnlyEvaluationRequest:要求 optimizedPrompt,不要求 testResultpackages/core/src/services/evaluation/types.ts:145
    • PromptIterateEvaluationRequest:要求 optimizedPrompt + iterateRequirementpackages/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 + iterateRequirementpackages/core/src/services/evaluation/service.ts:270)。
  • 多处错误文案由中文改为英文(例如校验/解析错误)(packages/core/src/services/evaluation/service.ts:160packages/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-onlypackages/core/src/services/template/default-templates/evaluation/basic/system/evaluation-prompt-only.ts
    • evaluation-pro-system-prompt-iteratepackages/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 校验规则(包括不要求 testResultiterateRequirement 必填)
    • 模板 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 类型由联合改为 EvaluationRequestpackages/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.noOptimizedPromptpackages/ui/src/i18n/locales/zh-CN.ts:1131packages/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-onlypackages/ui/src/components/PromptPanel.vue:371)。
    • 入口 UI:
      • 若有结果或正在评估:展示 EvaluationScoreBadge
      • 否则:展示「分析」按钮(packages/ui/src/components/PromptPanel.vue:122)。
    • 点击「分析」:
      • optimizedPrompt 为空,toast prompt.error.noOptimizedPrompt
      • 否则按是否有 iterationNote 调用 evaluation.evaluatePromptOnly/Iteratepackages/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.activeDetailTypeevaluation.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:420packages/ui/src/components/PromptPanel.vue:363packages/ui/src/components/PromptPanel.vue:489)。
  • 输出结构被统一规范化:模板可返回不同 dimensions[],但最终都会被规范化为统一的 EvaluationResponse 结构,UI 可复用同一渲染组件(packages/core/src/services/evaluation/service.ts:394packages/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-onlyprompt-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”)

现象

  • ContextSystemWorkspaceContextUserWorkspace 监听 @analyze="handleAnalyze",并在 handleAnalyze 中调用 evaluation.evaluatePromptOnly/Iterate 且传入 proContextpackages/ui/src/components/context-mode/ContextSystemWorkspace.vue:518packages/ui/src/components/context-mode/ContextUserWorkspace.vue:769)。
  • PromptPanel 并未定义/emit analyze 事件(packages/ui/src/components/PromptPanel.vue:413),点击「分析」走的是 handleEvaluate() 直接调用 evaluation.evaluatePromptOnly/Iterate,且未传 proContextpackages/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-iteratepackages/ui/src/components/app-layout/PromptOptimizerApp.vue:1340)。

风险点

  • Context 模式下 PromptPaneloptimizedPrompt 来自 displayAdapter.displayedOptimizedPromptpackages/ui/src/components/context-mode/ContextSystemWorkspace.vue:102),不一定会触发上述 watch;
  • 即使触发,PromptPanel 内部也没有基于 currentVersionIdselectedMessage 的精确清理逻辑。

建议

  • PromptPanel 内部针对 optimizedPromptcurrentVersionIdversions(或等价“内容标识”)做 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 声明存在冗余/误导

现象

  • PromptPaneldefineEmits 新增了 "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. 统一“分析”入口架构:删除死代码或补齐 PromptPanelanalyze 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.tshandleEvaluate() 中添加了对 prompt-onlyprompt-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.vueContextUserWorkspace.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,监听 optimizedPromptcurrentVersionId 的变化
  • 当内容或版本变化时,自动清除 prompt-onlyprompt-iterate 评估结果
  • 避免切换版本/消息后旧分数残留的问题

涉及文件

  • packages/ui/src/components/PromptPanel.vue

📋 P1-1:isOptimizedBetter 字段语义(设计决策)

决策 保持当前行为,作为已知的设计取舍:

  • prompt-onlyprompt-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 对外只保留必要事件:iterateswitchVersionsave-favoriteapply-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 内的 <EvaluationPanel>
    • 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 目录搜索 <EvaluationPanel 应无匹配
  • 搜索 externalEvaluation 应能找到两个 Workspace 的使用

✅ P1:Context Workspaces 的 prompt-iterate re-evaluate 缺少 iterateRequirement(已修复)

原始问题

  • Workspace 内部的 useEvaluationHandler() 未传 currentIterateRequirement,可能导致 prompt-iterate 的 re-evaluate 校验失败。

修复方案(已实施)

  • 在两个 Workspace 中新增 currentIterateRequirement 计算属性:
    • ContextSystemWorkspace.vue:425-432 - 从 displayAdapter.displayedVersions / displayedCurrentVersionId 获取(确保与 UI“当前显示版本”一致)
    • ContextUserWorkspace.vue:531-538 - 从 contextUserOptimization.currentVersions 获取
  • 将其传入 useEvaluationHandler
    • ContextSystemWorkspace.vue:445 - currentIterateRequirement,
    • ContextUserWorkspace.vue:551 - currentIterateRequirement,

✅ P1:应用改进建议仅负责“打开迭代弹窗 + 预填文本”,不依赖预选模板(已修复)

背景/场景

  • 用户在评估详情点击“应用改进建议”,预期行为是:直接打开迭代弹窗,并把建议文本放进输入框;模板在弹窗内再选择(不同模式可选模板不同)。

修复方案(已实施)

  • PromptPanel.vue 的迭代弹窗内已包含 TemplateSelect(可在弹窗内选择模板)。
  • PromptPanel.vuehandleIterate() 不再要求 selectedIterateTemplate 已预选;直接打开弹窗。
  • PromptPanel.vue 暴露 openIterateDialog(input?):用于“应用改进建议”路径预填充输入并打开弹窗。

验证方式

  • 不预选迭代模板,点击“继续优化”按钮:应能打开迭代弹窗并在弹窗内选择模板。
  • 从评估详情点击“应用改进建议”:应打开迭代弹窗并预填建议文本;未选择模板时点击确认应提示“请先选择迭代提示词”(允许)。

✅ P1:模式/子模式切换时关闭并清理评估状态(已修复)

背景/场景

  • “评估”永远针对当前显示内容;当切换功能模式(basic/pro/image)或切换子模式(system/user 等)时,旧的评估详情和分数不应残留。

修复方案(已实施)

  • PromptOptimizerApp.vue 在以下入口统一执行:
    • evaluation.closePanel()(关闭详情面板)
    • evaluation.clearAllResults()(清空所有评估结果)
  • 覆盖:
    • 功能模式切换 handleModeSelect(...)
    • Context 子模式切换 watch(contextManagement.contextMode
    • 子模式切换处理器:handleBasicSubModeChange(...) / handleProSubModeChange(...) / handleImageSubModeChange(...)

验证方式

  • 任意模式下完成评估后切换模式/子模式:评估面板应关闭,评分徽章/详情应清空。

📋 P2:已知取舍(非阻塞,列入优化 backlog)

  • isOptimizedBetter 在 prompt-only/prompt-iterate 中不落库:模板要求输出该字段但服务端只在 compare 保留;建议要么删模板字段节省 token,要么扩展服务与 UI 一致消费(packages/core/src/services/evaluation/service.ts:468)。
  • 错误文案语言不统一:Core 报错英文,UI 透传英文;后续可引入"错误类型/错误码 → i18n key"的映射(packages/core/src/services/evaluation/service.ts:159packages/ui/src/composables/prompt/useEvaluation.ts:410)。

✅ P0:全局 EvaluationPanel 在 Context 模式下的 re-evaluate / apply-improvement 逻辑仍可能不正确(已修复)

该问题是"全局面板事件处理器绑定到基础模式数据源"导致的模式耦合。尽管 Context Workspace 已通过 externalEvaluation 复用了全局 evaluation,并移除了本地面板,但 App 顶层面板的交互仍需要进一步解耦。

代码事实

  • App 顶层唯一 EvaluationPanel@re-evaluate 绑定到 handleReEvaluatepackages/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():内部转发到 PromptPanelopenIterateDialog,用于应用改进建议时打开迭代弹窗。
  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/compareprompt-only/prompt-iterate 会落到 evaluation.title.defaultpackages/ui/src/components/evaluation/EvaluationPanel.vue:185)。

修复方案(已实施)

  1. EvaluationPanel.vue 添加新类型的 case(第 188-191 行):

    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-onlyprompt-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 实例。