diff --git a/web/backend/internal/handlers/ai.go b/web/backend/internal/handlers/ai.go index f346c7a..3fa5aaf 100644 --- a/web/backend/internal/handlers/ai.go +++ b/web/backend/internal/handlers/ai.go @@ -88,7 +88,7 @@ func (d *Deps) Analyze(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") - if err := streamLLM(d.AIConfig, prompt, w, flusher); err != nil { + if err := streamLLM(llmCfg, prompt, w, flusher); err != nil { log.Printf("[ai] stream error: %v", err) sendSSE(w, flusher, "error", err.Error()) } diff --git a/web/backend/internal/middleware/logger.go b/web/backend/internal/middleware/logger.go index 790c10e..1221126 100644 --- a/web/backend/internal/middleware/logger.go +++ b/web/backend/internal/middleware/logger.go @@ -39,6 +39,13 @@ func (r *statusRecorder) WriteHeader(code int) { r.ResponseWriter.WriteHeader(code) } +// Flush 透传 http.Flusher,避免 SSE 流式响应被中间件阻断。 +func (r *statusRecorder) Flush() { + if f, ok := r.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/web/frontend/src/components/ScoreDetailDrawer.vue b/web/frontend/src/components/ScoreDetailDrawer.vue index c3e1022..4fdbeab 100644 --- a/web/frontend/src/components/ScoreDetailDrawer.vue +++ b/web/frontend/src/components/ScoreDetailDrawer.vue @@ -31,7 +31,7 @@ async function askAI() { aiContent.value = '' aiError.value = '' - const url = `/api/v1/ai/analyze?ts_code=${encodeURIComponent(score.value.ts_code)}&trade_date=${encodeURIComponent(score.value.trade_date)}` + const url = `/api/ai/analyze?ts_code=${encodeURIComponent(score.value.ts_code)}&trade_date=${encodeURIComponent(score.value.trade_date)}` es = new EventSource(url) es.addEventListener('token', (e) => { aiContent.value += e.data @@ -44,7 +44,7 @@ async function askAI() { closeAI() }) es.onerror = () => { - if (!aiContent.value) aiError.value = '连接中断' + if (!aiContent.value && !aiError.value) aiError.value = '连接中断' closeAI() } } @@ -267,7 +267,7 @@ const quadrantLabel = (q: string) => { 🤖 AI 分析当前打分 -
+
🤖 AI 分析 取消 diff --git a/web/frontend/src/views/ScoresView.vue b/web/frontend/src/views/ScoresView.vue index 2e88785..d81a84c 100644 --- a/web/frontend/src/views/ScoresView.vue +++ b/web/frontend/src/views/ScoresView.vue @@ -95,7 +95,7 @@ async function askAI() { const ts = encodeURIComponent(scoreResult.value.ts_code) const td = encodeURIComponent(scoreResult.value.trade_date) - aiES = new EventSource(`/api/v1/ai/analyze?ts_code=${ts}&trade_date=${td}`) + aiES = new EventSource(`/api/ai/analyze?ts_code=${ts}&trade_date=${td}`) aiES.addEventListener('token', (e) => { aiContent.value += e.data }) aiES.addEventListener('error', (e) => { aiError.value = (e as any)?.data || '请求失败' @@ -103,7 +103,7 @@ async function askAI() { }) aiES.addEventListener('done', () => closeAI()) aiES.onerror = () => { - if (!aiContent.value) aiError.value = '连接中断' + if (!aiContent.value && !aiError.value) aiError.value = '连接中断' closeAI() } } @@ -232,6 +232,28 @@ onMounted(async () => { + +
+ + 🤖 AI 分析当前打分 + +
+
+ 🤖 AI 分析 + 取消 +
+
+
+
{{ aiError }}
+
⏳ 正在分析...
+
+
+