Explorar el Código

refactor(speaking): extract parseSSEStream into shared module

jimmylee hace 1 mes
padre
commit
0b7d5d930d

+ 1 - 58
src/views/Editor/EnglishSpeaking/services/llmService.ts

@@ -11,6 +11,7 @@ import type {
   WordAnalysisItem,
 } from '@/types/englishSpeaking'
 import { SPEAKING_DIALOGUE_API_BASE_URL } from './speakingApiConfig'
+import { parseSSEStream } from './sseStream'
 
 const API_BASE = SPEAKING_DIALOGUE_API_BASE_URL
 
@@ -23,64 +24,6 @@ export class DialogueApiError extends Error {
   }
 }
 
-// ==================== SSE 解析 ====================
-
-async function* parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<SSEEvent> {
-  const decoder = new TextDecoder()
-  let buffer = ''
-
-  try {
-    while (true) {
-      const { done, value } = await reader.read()
-      if (done) break
-
-      buffer += decoder.decode(value, { stream: true })
-      const lines = buffer.split('\n')
-      buffer = lines.pop() || ''
-
-      let eventType = ''
-      let data = ''
-
-      for (const line of lines) {
-        if (line.startsWith('event:')) {
-          eventType = line.slice(6).trim()
-        } else if (line.startsWith('data:')) {
-          data = line.slice(5).trim()
-        } else if (line === '' && eventType && data) {
-          try {
-            const parsed = JSON.parse(data)
-            if (eventType === 'transcript') {
-              yield {
-                type: 'transcript',
-                text: parsed.text,
-                audioDuration: parsed.audioDuration ?? null,
-              }
-            } else if (eventType === 'token') {
-              yield { type: 'token', text: parsed.content ?? parsed.text }
-            } else if (eventType === 'done') {
-              yield { type: 'done', isComplete: parsed.isComplete }
-            } else if (eventType === 'image') {
-              yield {
-                type: 'image',
-                url: parsed.url,
-                round: parsed.round,
-              }
-            } else if (eventType === 'error') {
-              yield { type: 'error', message: parsed.message }
-            }
-          } catch {
-            // skip malformed JSON
-          }
-          eventType = ''
-          data = ''
-        }
-      }
-    }
-  } finally {
-    reader.releaseLock()
-  }
-}
-
 // ==================== Backend shape types ====================
 
 interface BackendWordAnalysisItemRaw {

+ 57 - 0
src/views/Editor/EnglishSpeaking/services/sseStream.ts

@@ -0,0 +1,57 @@
+import type { SSEEvent } from '@/types/englishSpeaking'
+
+export async function* parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<SSEEvent> {
+  const decoder = new TextDecoder()
+  let buffer = ''
+
+  try {
+    while (true) {
+      const { done, value } = await reader.read()
+      if (done) break
+
+      buffer += decoder.decode(value, { stream: true })
+      const lines = buffer.split('\n')
+      buffer = lines.pop() || ''
+
+      let eventType = ''
+      let data = ''
+
+      for (const line of lines) {
+        if (line.startsWith('event:')) {
+          eventType = line.slice(6).trim()
+        } else if (line.startsWith('data:')) {
+          data = line.slice(5).trim()
+        } else if (line === '' && eventType && data) {
+          try {
+            const parsed = JSON.parse(data)
+            if (eventType === 'transcript') {
+              yield {
+                type: 'transcript',
+                text: parsed.text,
+                audioDuration: parsed.audioDuration ?? null,
+              }
+            } else if (eventType === 'token') {
+              yield { type: 'token', text: parsed.content ?? parsed.text }
+            } else if (eventType === 'done') {
+              yield { type: 'done', isComplete: parsed.isComplete }
+            } else if (eventType === 'image') {
+              yield {
+                type: 'image',
+                url: parsed.url,
+                round: parsed.round,
+              }
+            } else if (eventType === 'error') {
+              yield { type: 'error', message: parsed.message }
+            }
+          } catch {
+            // skip malformed JSON
+          }
+          eventType = ''
+          data = ''
+        }
+      }
+    }
+  } finally {
+    reader.releaseLock()
+  }
+}