docs/archives/129-session-store-single-source-refactor/bug-fix-testresults-display.md
日期: 2025-01-08
分支: hapi-var-extract
影响范围: Basic 模式(基础模式-系统/用户提示词)测试功能
严重程度: P0(核心功能失效)
在 Basic 模式下执行测试功能时:
testResults 数据确实存在,但 UI 不显示BasicSystemWorkspace.vue(系统提示词模式)BasicUserWorkspace.vue(用户提示词模式)假设 1: Session Store 数据被清空
// 检查 Session Store 的 updateTestResults 方法
const updateTestResults = (results: TestResults | null) => {
// 添加调试日志
console.log('[updateTestResults] called with:', results)
testResults.value = results
}
结论: 数据没有被清空,Session Store 的 testResults 值正确
假设 2: 响应式追踪失效
// 检查 useBasicWorkspaceLogic.ts 的 computed getter
const testResults = computed({
get: () => {
const result = sessionStore.testResults || {
originalResult: '',
originalReasoning: '',
optimizedResult: '',
optimizedReasoning: ''
}
console.log('[testResults getter] returning:', result)
return result // ❌ 返回临时对象
}
})
发现: getter 返回临时默认对象,破坏响应式追踪
将问题交给 Codex 深入调查后,发现了真正的根本原因:
// ❌ BasicSystemWorkspace.vue 中的错误写法
const hasOriginalResult = computed(() => !!logic.testResults?.originalResult)
// ^^^^^^ 缺少 .value
// ✅ 正确写法
const hasOriginalResult = computed(() => !!logic.testResults.value?.originalResult)
核心发现:
logic.testResults 是 ComputedRef<TestResults | null><script setup> 中,ComputedRef 不会自动解包.value 访问实际值.value 导致布尔值始终为 false// <template> 中:computed 自动解包 ✅
<template>
<div v-if="testResults?.originalResult">...</div>
</template>
// <script setup> 中:computed 不自动解包 ❌
<script setup>
const testResults = computed(() => sessionStore.testResults)
console.log(testResults?.originalResult) // undefined!
console.log(testResults.value?.originalResult) // 正确
</script>
关键规则:
<script setup> 中自动解包ComputedRef 是 ref 的一种,遵循相同规则Session Store (Pinia)
↓ testResults: Ref<TestResults | null>
Logic Layer (Composable)
↓ testResults: ComputedRef<TestResults | null> ← 双重包装
Component
↓ hasOriginalResult = computed(() => !!logic.testResults?.originalResult)
↑ ^^^^ 忘记 .value
问题:
.value 解包.value 导致 bug// ❌ 修复前的代码
const testResults = computed({
get: () => {
return sessionStore.testResults || {
originalResult: '',
originalReasoning: '',
optimizedResult: '',
optimizedReasoning: ''
}
// ^^^^ 每次都返回新的临时对象,Vue 无法追踪!
}
})
问题:
sessionStore.testResults 为 null 时,返回临时对象文件: packages/ui/src/composables/workspaces/useBasicWorkspaceLogic.ts
// ❌ 修复前
const testResults = computed<BasicSessionStore['testResults']>({
get: () => {
const result = sessionStore.testResults || {
originalResult: '',
originalReasoning: '',
optimizedResult: '',
optimizedReasoning: ''
}
console.log('[testResults getter]', result)
return result
},
set: (value) => {
console.log('[testResults setter]', value)
sessionStore.updateTestResults(value)
}
})
// ✅ 修复后
const testResults = computed<BasicSessionStore['testResults']>({
get: () => {
// ✅ 始终返回 sessionStore.testResults(即使是 null)
// 避免返回临时对象导致响应式追踪失效
return sessionStore.testResults
},
set: (value) => {
sessionStore.updateTestResults(value)
}
})
关键改进:
sessionStore.testResults文件: packages/ui/src/components/basic-mode/BasicSystemWorkspace.vue
// ❌ 修复前
const hasOriginalResult = computed(() => !!logic.testResults?.originalResult)
// ✅ 修复后
const hasOriginalResult = computed(() => !!logic.testResults.value?.originalResult)
const hasOptimizedResult = computed(() => !!logic.testResults.value?.optimizedResult)
// ✅ 解包 logic 中的 ref,用于传递给子组件
const unwrappedLogicProps = computed(() => ({
isOptimizing: logic.isOptimizing.value,
isTestingOriginal: logic.isTestingOriginal.value,
optimizedReasoning: logic.optimizedReasoning.value,
// ✅ 处理 testResults 可能为 null 的情况
testResultsOriginalResult: logic.testResults.value?.originalResult || '',
testResultsOriginalReasoning: logic.testResults.value?.originalReasoning || '',
testResultsOptimizedResult: logic.testResults.value?.optimizedResult || '',
testResultsOptimizedReasoning: logic.testResults.value?.optimizedReasoning || ''
}))
// ✅ 评估处理器
const testResultsComputed = computed(() => ({
originalResult: logic.testResults.value?.originalResult || undefined,
optimizedResult: logic.testResults.value?.optimizedResult || undefined
}))
文件: packages/ui/src/components/basic-mode/BasicUserWorkspace.vue
应用与 BasicSystemWorkspace.vue 相同的修复模式。
访问: http://localhost:18181/#/basic/system
输入: "你是一个诗人"
优化: ✅ 成功
测试: ✅ 流式更新正常显示
✅ 测试完成后结果保持显示
✅ 不再回到"暂无内容"
访问: http://localhost:18181/#/basic/user
输入: "你是一个诗人"
优化: ✅ 成功
测试: ✅ 流式更新正常显示
✅ 测试完成后结果保持显示
┌─────────────────────────────────────────────────────────────┐
│ BasicSystemWorkspace.vue │
│ (BasicUserWorkspace.vue) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ useBasicWorkspaceLogic.ts │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 状态代理(Session Store 的包装) │ │
│ │ - prompt, optimizedPrompt, testResults │ │
│ │ - 添加默认值处理(|| '') │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ 2. 过程态管理(不持久化的 UI 状态) │ │
│ │ - isOptimizing, isTestingOriginal, isIterating │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ 3. 历史管理(不持久化的历史数据) │ │
│ │ - currentVersions, currentChainId │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ 4. 业务逻辑(共享的核心操作) │ │
│ │ - handleOptimize, handleTest, handleIterate │ │
│ │ - handleSwitchVersion, loadVersions │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ useBasicSystemSession.ts │
│ (useBasicUserSession.ts) │
│ ┌──────────────────────────────────┐ │
│ │ 持久化状态(Session Store) │ │
│ │ - prompt, optimizedPrompt │ │
│ │ - testResults, chainId, versionId │ │
│ │ - selectedModelKey, templateId │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| 职责 | 价值 | 代价 |
|---|---|---|
| 代码复用 | BasicSystem 和 BasicUser 共享 99% 的业务逻辑 | 无 |
| 状态代理 | 统一处理空值默认值(` | |
| 过程态管理 | 避免 Session Store 被临时状态污染 | 增加一层抽象 |
| 历史管理 | 不持久化大型历史数据 | 增加状态管理复杂度 |
| 错误处理 | 统一的 toast 提示和错误处理 | 无 |
// ❌ 当前实现
const prompt = computed<string>({
get: () => sessionStore.prompt || '',
set: (value) => sessionStore.updatePrompt(value || '')
})
问题:
state → view → actions → state// Logic 层返回对象包装的 ref
return {
testResults, // ComputedRef<TestResults | null>
isOptimizing // Ref<boolean>
}
// ❌ 组件中必须使用 .value
const hasResult = computed(() => !!logic.testResults.value?.originalResult)
// ^^^^^^ 容易遗漏
// 需要创建解包版本传递给子组件
const unwrappedLogicProps = computed(() => ({
testResultsOriginalResult: logic.testResults.value?.originalResult || '',
isOptimizing: logic.isOptimizing.value
// ... 大量样板代码
}))
问题:
<script setup> 中自动解包// ✅ TypeScript 类型检查通过
const hasResult = computed(() => !!logic.testResults?.originalResult)
// ❌ 运行时行为错误
// logic.testResults 是 ComputedRef 对象,没有 originalResult 属性
// 应该是 logic.testResults.value?.originalResult
问题:
.value 缺失logic.testResults?.originalResult 在类型上是合法的// Logic 层只是在转发 Store 的操作
const prompt = computed<string>({
get: () => sessionStore.prompt || '',
set: (value) => sessionStore.updatePrompt(value || '')
})
const optimizedPrompt = computed<string>({
get: () => sessionStore.optimizedPrompt || '',
set: (value) => {
sessionStore.updateOptimizedResult({
optimizedPrompt: value || '',
reasoning: sessionStore.reasoning || '',
chainId: sessionStore.chainId || '',
versionId: sessionStore.versionId || ''
})
}
})
问题:
toRefs 自动解包(最小改动)适用场景: 短期内快速修复,减少类似 bug
// ✅ 改进后的 useBasicWorkspaceLogic.ts
import { toRefs } from 'vue'
export function useBasicWorkspaceLogic(...) {
// ... 现有代码 ...
return {
// ✅ 使用 toRefs 自动解包所有 refs
...toRefs({
prompt,
optimizedPrompt,
optimizedReasoning,
testResults,
selectedOptimizeModelKey,
selectedTestModelKey,
isOptimizing,
isIterating,
isTestingOriginal,
isTestingOptimized,
currentVersions,
currentVersionId
}),
// 方法直接返回
handleOptimize,
handleTest,
handleIterate,
handleSwitchVersion,
loadVersions
}
}
// ✅ 组件中无需 .value
const hasOriginalResult = computed(() => !!logic.testResults?.originalResult)
// ^^^^^^ 不再需要 .value!
优点:
.value缺点:
适用场景: 长期重构,符合 Vue 3 最佳实践
// ✅ BasicSystemWorkspace.vue(重构后)
<script setup>
import { storeToRefs } from 'pinia'
import { useBasicSystemSession } from '../../stores/session/useBasicSystemSession'
import { useBasicWorkspaceOperations } from '../../composables/workspaces/useBasicWorkspaceOperations'
// 1. 状态:直接使用 Store
const sessionStore = useBasicSystemSession()
const { prompt, testResults, optimizedPrompt } = storeToRefs(sessionStore)
// 2. 派生状态:在组件内定义
const hasOriginalResult = computed(() =>
!!testResults.value?.originalResult
)
const hasOptimizedResult = computed(() =>
!!testResults.value?.optimizedResult
)
// 3. 业务逻辑:从专门的 composable 获取
const { handleOptimize, handleTest, handleIterate } = useBasicWorkspaceOperations({
sessionStore,
services,
optimizationMode: 'system'
})
</script>
// ✅ useBasicWorkspaceOperations.ts(新的 composable)
export function useBasicWorkspaceOperations(options: {
sessionStore: BasicSessionStore
services: Ref<AppServices | null>
optimizationMode: 'system' | 'user'
}) {
const { sessionStore, services, optimizationMode } = options
const toast = useToast()
const { t } = useI18n()
// UI 过程态(不持久化)
const isOptimizing = ref(false)
const isTestingOriginal = ref(false)
const isTestingOptimized = ref(false)
// ✅ 只包含操作逻辑,不包含状态代理
const handleOptimize = async () => {
if (!sessionStore.prompt?.trim()) {
toast.error(t('prompt.error.noPrompt'))
return
}
const promptService = services.value?.promptService
if (!promptService) {
toast.error(t('toast.error.serviceInit'))
return
}
isOptimizing.value = true
try {
const request: OptimizationRequest = {
optimizationMode,
targetPrompt: sessionStore.prompt,
templateId: sessionStore.selectedTemplateId || '',
modelKey: sessionStore.selectedOptimizeModelKey
}
// 清理历史绑定
sessionStore.updateOptimizedResult({
optimizedPrompt: '',
reasoning: '',
chainId: '',
versionId: ''
})
await promptService.optimizePromptStream(request, {
onToken: (token: string) => {
// ✅ 直接更新 store
sessionStore.updateOptimizedResult({
optimizedPrompt: (sessionStore.optimizedPrompt || '') + token,
reasoning: sessionStore.reasoning || '',
chainId: sessionStore.chainId || '',
versionId: sessionStore.versionId || ''
})
},
onComplete: async () => {
// 处理历史记录
const historyManager = services.value?.historyManager
if (historyManager) {
const recordData = {
id: uuidv4(),
originalPrompt: sessionStore.prompt,
optimizedPrompt: sessionStore.optimizedPrompt,
type: optimizationMode === 'system' ? 'system-optimize' : 'user-optimize',
modelKey: sessionStore.selectedOptimizeModelKey,
templateId: sessionStore.selectedTemplateId || '',
timestamp: Date.now()
}
const chain = await historyManager.createNewChain(recordData)
sessionStore.updateOptimizedResult({
optimizedPrompt: sessionStore.optimizedPrompt,
reasoning: sessionStore.reasoning || '',
chainId: chain.chainId,
versionId: chain.currentRecord.id
})
toast.success(t('toast.success.optimizeSuccess'))
}
},
onError: (error: Error) => {
throw error
}
})
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
toast.error(t('toast.error.optimizeFailed') + ': ' + err.message)
} finally {
isOptimizing.value = false
}
}
const handleTest = async () => {
// ... 类似的实现
}
const handleIterate = async () => {
// ... 类似的实现
}
return {
// 过程态
isOptimizing,
isTestingOriginal,
isTestingOptimized,
// 业务逻辑
handleOptimize,
handleTest,
handleIterate
}
}
优点:
.value 解包对象属性缺点:
适用场景: 想保留 Logic 层的代码复用,但符合 Vue 3 最佳实践
// ✅ useBasicWorkspace.ts(重构后)
export function useBasicWorkspace(options: {
mode: 'system' | 'user'
}) {
const { mode } = options
const sessionStore = mode === 'system'
? useBasicSystemSession()
: useBasicUserSession()
const toast = useToast()
const { t } = useI18n()
// ✅ UI 过程态(不持久化)
const isOptimizing = ref(false)
const isTestingOriginal = ref(false)
const isTestingOptimized = ref(false)
// ✅ 历史管理(不持久化)
const currentVersions = ref<PromptRecordChain['versions']>([])
const currentChainId = ref('')
const currentVersionId = ref('')
// ✅ 派生状态(在 composable 内定义)
const hasOriginalResult = computed(() =>
!!sessionStore.testResults?.originalResult
)
const hasOptimizedResult = computed(() =>
!!sessionStore.testResults?.optimizedResult
)
// ✅ 业务逻辑
const handleTest = async () => {
if (!sessionStore.optimizedPrompt) {
toast.error(t('prompt.error.noOptimizedPrompt'))
return
}
const promptService = services.value?.promptService
if (!promptService) return
const isCompareMode = !!sessionStore.isCompareMode
const testInput = sessionStore.testContent || ''
if (mode === 'system' && !testInput.trim()) {
toast.error(t('test.simpleMode.help'))
return
}
// 先清空 session store 的 testResults
sessionStore.updateTestResults(null)
// 初始化测试结果
sessionStore.updateTestResults({
originalResult: '',
originalReasoning: '',
optimizedResult: '',
optimizedReasoning: ''
})
try {
// 对比模式:先测试原始提示词
if (isCompareMode) {
isTestingOriginal.value = true
const systemPrompt = mode === 'system' ? sessionStore.prompt : ''
const userPrompt = mode === 'system' ? testInput : sessionStore.prompt
await promptService.testPromptStream(
systemPrompt,
userPrompt,
sessionStore.selectedTestModelKey,
{
onToken: (token: string) => {
const results = sessionStore.testResults
sessionStore.updateTestResults({
...results,
originalResult: (results?.originalResult || '') + token
})
},
onComplete: () => {
isTestingOriginal.value = false
},
onError: (error: Error) => {
throw error
}
}
)
}
// 测试优化后的提示词
isTestingOptimized.value = true
const optimizedSystemPrompt = mode === 'system' ? sessionStore.optimizedPrompt : ''
const optimizedUserPrompt = mode === 'system' ? testInput : sessionStore.optimizedPrompt
await promptService.testPromptStream(
optimizedSystemPrompt,
optimizedUserPrompt,
sessionStore.selectedTestModelKey,
{
onToken: (token: string) => {
const results = sessionStore.testResults
sessionStore.updateTestResults({
...results,
optimizedResult: (results?.optimizedResult || '') + token
})
},
onComplete: () => {
toast.success(t('toast.success.testComplete'))
},
onError: (error: Error) => {
throw error
}
}
)
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
toast.error(t('toast.error.testFailed') + ': ' + err.message)
} finally {
isTestingOriginal.value = false
isTestingOptimized.value = false
}
}
const handleOptimize = async () => {
// ... 类似的实现
}
const handleIterate = async () => {
// ... 类似的实现
}
// ✅ 返回独立的 ref(直接返回,不用对象包装)
return {
// 派生状态
hasOriginalResult, // ComputedRef<boolean>
hasOptimizedResult, // ComputedRef<boolean>
// 过程态
isOptimizing, // Ref<boolean>
isTestingOriginal, // Ref<boolean>
isTestingOptimized, // Ref<boolean>
// 历史管理
currentVersions, // Ref<PromptRecord[]>
currentChainId, // Ref<string>
currentVersionId, // Ref<string>
// Actions
handleTest,
handleOptimize,
handleIterate,
handleSwitchVersion,
loadVersions
}
}
// ✅ 组件中使用
<script setup>
import { useBasicWorkspace } from '../../composables/workspaces/useBasicWorkspace'
const {
hasOriginalResult, // ComputedRef - 自动解包
hasOptimizedResult, // ComputedRef - 自动解包
isOptimizing, // Ref - 自动解包
handleTest // Function
} = useBasicWorkspace({ mode: 'system' })
// ✅ 在模板中直接使用,无需 .value
</script>
<template>
<div v-if="hasOriginalResult">{{ testResults }}</div>
<button :disabled="isOptimizing" @click="handleTest">测试</button>
</template>
优点:
<script setup> 中自动解包缺点:
| 方面 | 当前架构 | 方案 A: toRefs | 方案 B: 移除 Logic | 方案 C: 重构 Logic |
|---|---|---|---|---|
| 改动成本 | - | 小 | 大 | 中 |
| Vue 3 最佳实践 | ❌ | ⚠️ 部分符合 | ✅ 完全符合 | ✅ 完全符合 |
| 数据流清晰度 | ❌ 双向 | ⚠️ 双向 | ✅ 单向 | ✅ 单向 |
| 组件代码量 | 中 | 中 | 少 | 少 |
| 是否需要 .value | 是(对象属性) | 否 | 否 | 否 |
| 类型安全 | ⚠️ 运行时错误 | ✅ | ✅ | ✅ |
| 代码复用 | ✅ | ✅ | ⚠️ 需手动提取 | ✅ |
| 可测试性 | ⚠️ | ⚠️ | ✅ | ✅ |
| 推荐指数 | - | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
.value 遗漏<template> 中自动解包,但在 <script setup> 中不自动解包.value 缺失的错误.value.value 遗漏文档维护: 请在后续重构后更新此文档,记录最终的实施方案和结果。