Back to Prompt Optimizer

Session Store 测试结果显示 Bug 修复记录

docs/archives/129-session-store-single-source-refactor/bug-fix-testresults-display.md

2.10.228.4 KB
Original Source

Session Store 测试结果显示 Bug 修复记录

日期: 2025-01-08 分支: hapi-var-extract 影响范围: Basic 模式(基础模式-系统/用户提示词)测试功能 严重程度: P0(核心功能失效)


📋 问题描述

症状

在 Basic 模式下执行测试功能时:

  • ✅ 测试执行过程中可以看到流式更新
  • ❌ 测试完成后,结果区域显示"暂无内容"
  • ❌ Session Store 中的 testResults 数据确实存在,但 UI 不显示

复现步骤

  1. 访问 http://localhost:18181/#/basic/system
  2. 输入原始提示词(如"你是一个诗人")
  3. 点击"优化"按钮生成优化后的提示词
  4. 点击"测试"按钮执行测试
  5. 观察结果区域:测试过程中显示内容,测试完成后显示"暂无内容"

影响范围

  • BasicSystemWorkspace.vue(系统提示词模式)
  • BasicUserWorkspace.vue(用户提示词模式)
  • 所有依赖测试结果的功能(评估、对比等)

🔍 排查过程

阶段 1:数据流追踪

假设 1: Session Store 数据被清空

typescript
// 检查 Session Store 的 updateTestResults 方法
const updateTestResults = (results: TestResults | null) => {
  // 添加调试日志
  console.log('[updateTestResults] called with:', results)
  testResults.value = results
}

结论: 数据没有被清空,Session Store 的 testResults 值正确

假设 2: 响应式追踪失效

typescript
// 检查 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 返回临时默认对象,破坏响应式追踪

阶段 2:Codex 协助调查

将问题交给 Codex 深入调查后,发现了真正的根本原因

typescript
// ❌ BasicSystemWorkspace.vue 中的错误写法
const hasOriginalResult = computed(() => !!logic.testResults?.originalResult)
//                                              ^^^^^^ 缺少 .value

// ✅ 正确写法
const hasOriginalResult = computed(() => !!logic.testResults.value?.originalResult)

核心发现:

  • logic.testResultsComputedRef<TestResults | null>
  • <script setup> 中,ComputedRef 不会自动解包
  • 必须使用 .value 访问实际值
  • 没有 .value 导致布尔值始终为 false

🎯 根本原因分析

1. Vue 3 响应式系统的易错性

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

关键规则:

  • 只有顶层变量的 ref 会在 <script setup> 中自动解包
  • 对象属性中的 ref 不会自动解包
  • ComputedRef 是 ref 的一种,遵循相同规则

2. 架构设计的问题

Session Store (Pinia)
    ↓ testResults: Ref<TestResults | null>
Logic Layer (Composable)
    ↓ testResults: ComputedRef<TestResults | null>  ← 双重包装
Component
    ↓ hasOriginalResult = computed(() => !!logic.testResults?.originalResult)
    ↑                                        ^^^^ 忘记 .value

问题:

  • Logic 层返回对象包装的 ref
  • 组件中需要手动 .value 解包
  • TypeScript 无法捕获这种运行时错误
  • 容易遗漏 .value 导致 bug

3. 临时对象破坏响应式

typescript
// ❌ 修复前的代码
const testResults = computed({
  get: () => {
    return sessionStore.testResults || {
      originalResult: '',
      originalReasoning: '',
      optimizedResult: '',
      optimizedReasoning: ''
    }
    // ^^^^ 每次都返回新的临时对象,Vue 无法追踪!
  }
})

问题:

  • sessionStore.testResultsnull 时,返回临时对象
  • 临时对象的引用每次都不同
  • Vue 的响应式系统依赖对象引用追踪变化
  • 导致依赖这个 computed 的组件无法正确更新

🔧 当前修复方案

修复 1: useBasicWorkspaceLogic.ts

文件: packages/ui/src/composables/workspaces/useBasicWorkspaceLogic.ts

typescript
// ❌ 修复前
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)
  }
})

关键改进:

  1. 移除临时默认对象,始终返回 sessionStore.testResults
  2. 移除所有调试日志
  3. 简化代码逻辑

修复 2: BasicSystemWorkspace.vue

文件: packages/ui/src/components/basic-mode/BasicSystemWorkspace.vue

typescript
// ❌ 修复前
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
}))

修复 3: BasicUserWorkspace.vue

文件: packages/ui/src/components/basic-mode/BasicUserWorkspace.vue

应用与 BasicSystemWorkspace.vue 相同的修复模式。


✅ 验证结果

测试场景 1: Basic-System 模式

访问: http://localhost:18181/#/basic/system
输入: "你是一个诗人"
优化: ✅ 成功
测试: ✅ 流式更新正常显示
      ✅ 测试完成后结果保持显示
      ✅ 不再回到"暂无内容"

测试场景 2: Basic-User 模式

访问: http://localhost:18181/#/basic/user
输入: "你是一个诗人"
优化: ✅ 成功
测试: ✅ 流式更新正常显示
      ✅ 测试完成后结果保持显示

Console 日志

  • ✅ 无调试日志残留
  • ✅ 无错误或警告
  • ✅ 响应式更新正常触发

🏗️ 架构分析

当前架构:Logic 层的作用

┌─────────────────────────────────────────────────────────────┐
│                    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    │              │
│            └──────────────────────────────────┘              │
└─────────────────────────────────────────────────────────────┘

Logic 层存在的价值

职责价值代价
代码复用BasicSystem 和 BasicUser 共享 99% 的业务逻辑
状态代理统一处理空值默认值(`
过程态管理避免 Session Store 被临时状态污染增加一层抽象
历史管理不持久化大型历史数据增加状态管理复杂度
错误处理统一的 toast 提示和错误处理

当前架构的痛点

痛点 1: 双向 Computed 违背单向数据流

typescript
// ❌ 当前实现
const prompt = computed<string>({
  get: () => sessionStore.prompt || '',
  set: (value) => sessionStore.updatePrompt(value || '')
})

问题:

  • Vue 3 推崇单向数据流:state → view → actions → state
  • 双向 computed 打破了数据流向的清晰性
  • 组件无法控制何时触发更新

痛点 2: 对象属性中的 Ref 需要手动解包

typescript
// 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
  // ... 大量样板代码
}))

问题:

  • 违背了 Composition API 的设计理念:ref 应该在 <script setup> 中自动解包
  • 只有当 ref 是顶层变量时才会自动解包
  • 对象属性中的 ref 不会自动解包

痛点 3: TypeScript 无法捕获运行时错误

typescript
// ✅ TypeScript 类型检查通过
const hasResult = computed(() => !!logic.testResults?.originalResult)

// ❌ 运行时行为错误
// logic.testResults 是 ComputedRef 对象,没有 originalResult 属性
// 应该是 logic.testResults.value?.originalResult

问题:

  • TypeScript 编译器无法捕获 .value 缺失
  • logic.testResults?.originalResult 在类型上是合法的
  • 但实际访问的是 ComputedRef 对象,而不是 TestResults

痛点 4: 过度抽象

typescript
// 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 || ''
    })
  }
})

问题:

  • Logic 层没有真正的业务逻辑,只是在做数据转发
  • 这不是抽象,这是间接层(Indirection)
  • 增加了代码复杂度,没有带来价值

💡 改进方案

方案 A: 使用 toRefs 自动解包(最小改动)

适用场景: 短期内快速修复,减少类似 bug

typescript
// ✅ 改进后的 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
  • ✅ 保持响应式
  • ✅ 类型安全
  • 最小改动

缺点:

  • ⚠️ 仍然保留双向 computed(违背单向数据流)
  • ⚠️ Logic 层仍然是间接层

方案 B: 移除 Logic 层,直接使用 Store(推荐)

适用场景: 长期重构,符合 Vue 3 最佳实践

typescript
// ✅ 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
  }
}

优点:

  • ✅ 符合 Vue 3 单向数据流原则
  • ✅ 组件直接使用 Store,清晰明了
  • ✅ Composable 只包含业务逻辑和 UI 过程态,职责单一
  • ✅ 不需要 .value 解包对象属性
  • ✅ 易于测试、易于维护

缺点:

  • ⚠️ 需要重构多个组件
  • ⚠️ 需要拆分 Logic 层的职责

方案 C: 保留 Logic 层,但重构为真正的 Composable

适用场景: 想保留 Logic 层的代码复用,但符合 Vue 3 最佳实践

typescript
// ✅ 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>

优点:

  • ✅ 返回独立的 ref,在 <script setup> 中自动解包
  • ✅ 派生状态在 composable 内定义,组件无需关心
  • ✅ UI 过程态和业务逻辑封装在一起
  • ✅ 组件代码极度简洁
  • ✅ 保留了代码复用价值

缺点:

  • ⚠️ 需要重构 Logic 层
  • ⚠️ Composable 变得更复杂(但也更完整)

📊 方案对比

方面当前架构方案 A: toRefs方案 B: 移除 Logic方案 C: 重构 Logic
改动成本-
Vue 3 最佳实践⚠️ 部分符合✅ 完全符合✅ 完全符合
数据流清晰度❌ 双向⚠️ 双向✅ 单向✅ 单向
组件代码量
是否需要 .value是(对象属性)
类型安全⚠️ 运行时错误
代码复用⚠️ 需手动提取
可测试性⚠️⚠️
推荐指数-⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

🎯 建议

短期(当前阶段)

  • ✅ 使用**方案 A(toRefs)**快速修复
  • ✅ 添加 ESLint 规则检测常见的 .value 遗漏
  • ✅ 添加单元测试覆盖响应式更新

长期(架构重构)

  • ✅ 考虑方案 B(移除 Logic 层)方案 C(重构 Logic)
  • ✅ 统一使用单向数据流
  • ✅ 将 Logic 层拆分为更小的、职责单一的 composables

📝 经验总结

1. Vue 3 响应式系统的陷阱

  • ⚠️ Computed 在 <template> 中自动解包,但在 <script setup> 中不自动解包
  • ⚠️ 只有顶层变量的 ref 会自动解包,对象属性的 ref 不会
  • ⚠️ TypeScript 无法捕获 .value 缺失的错误

2. 架构设计原则

  • ✅ 避免双向 computed,使用单向数据流
  • ✅ Composable 应该返回独立的 ref,而不是对象包装的 ref
  • ✅ 优先考虑 Vue 官方推荐的模式,而不是自创模式
  • ✅ 过度抽象会增加复杂度,降低可维护性

3. 调试技巧

  • ✅ 添加详细的日志追踪数据流
  • ✅ 检查响应式依赖是否正确建立
  • ✅ 验证临时对象是否破坏响应式
  • ✅ 使用 Codex 等 AI 助手进行深度分析

4. 代码审查要点

  • ⚠️ 检查所有 ComputedRef 访问是否使用了 .value
  • ⚠️ 检查是否有返回临时对象的 computed getter
  • ⚠️ 检查是否有违背单向数据流的双向绑定
  • ⚠️ 检查 Logic 层是否有真正的价值,还是只是间接层

🔗 相关资源


📌 TODO

  • 选择最终的重构方案(A/B/C)
  • 添加 ESLint 规则检测 .value 遗漏
  • 添加单元测试覆盖响应式更新
  • 重构 Logic 层(如果选择方案 B 或 C)
  • 更新相关文档和注释

文档维护: 请在后续重构后更新此文档,记录最终的实施方案和结果。