Procházet zdrojové kódy

feat(speaking): extract _runGreeting; retry reuses turnId for idempotency

Public generateGreeting() allocates a fresh UUID per fresh attempt.
retryGreeting() reuses the failed greeting's turnId so the backend's
idempotency lookup hits and the user doesn't get charged for two
LLM calls when the first greeting succeeded server-side but the
response was lost in transit.
jimmylee před 1 týdnem
rodič
revize
ea0e67bcf9

+ 15 - 7
src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts

@@ -40,25 +40,26 @@ export function useDialogueEngine() {
     if (info.expiresAt) startCountdown(info.expiresAt)
   }
 
-  /** 触发开场白生成:push loading 占位 AI 消息 → 请求 /greeting → 成功填内容,失败标 error。 */
-  async function generateGreeting() {
+  /** 执行开场白生成的私有帮手:push loading 占位 AI 消息 → 请求 /greeting → 成功填内容,失败标 error。
+   *  @param turnId 本次生成的轮次 ID,用于后端幂等性查找。
+   */
+  async function _runGreeting(turnId: string) {
     if (!sessionId.value || greetingInflight.value) return
     greetingInflight.value = true
     greetingAbortController = new AbortController()
 
-    const greetingTurnId = crypto.randomUUID()
     const aiMsg = reactive<PreviewChatMessage>({
       id: crypto.randomUUID(),
       role: 'ai',
       content: '',
       timestamp: new Date(),
       status: 'loading',
-      turnId: greetingTurnId,
+      turnId,
     })
     messages.value.push(aiMsg)
 
     try {
-      const { aiMessage } = await api.generateGreeting(sessionId.value, greetingTurnId, greetingAbortController.signal)
+      const { aiMessage } = await api.generateGreeting(sessionId.value, turnId, greetingAbortController.signal)
       aiMsg.content = aiMessage
       aiMsg.status = 'done'
     } catch (err: unknown) {
@@ -74,13 +75,20 @@ export function useDialogueEngine() {
     }
   }
 
-  /** 重试失败的开场白:移除 error 消息,再走一遍 generateGreeting(同 sessionId)。 */
+  /** 触发开场白生成:分配新的 turnId,调用 _runGreeting。 */
+  async function generateGreeting() {
+    await _runGreeting(crypto.randomUUID())
+  }
+
+  /** 重试失败的开场白:移除 error 消息,复用失败消息的 turnId 再走一遍(后端幂等性查找)。
+   *  如果老消息没有 turnId(不应该发生,但防守性),分配新的。 */
   async function retryGreeting() {
     if (greetingInflight.value) return
     const firstAi = messages.value.find(m => m.role === 'ai')
     if (firstAi?.status !== 'error' || firstAi.recovery === 'restart') return
+    const reuseTurnId = firstAi.turnId ?? crypto.randomUUID()
     messages.value = messages.value.filter(m => m.id !== firstAi.id)
-    await generateGreeting()
+    await _runGreeting(reuseTurnId)
   }
 
   // ==================== Send Message ====================