修复AI分析SSE流断开:中间件透传Flusher、DB Key生效、错误提示不覆盖

This commit is contained in:
fish
2026-05-10 17:11:51 +08:00
parent 1094e82e88
commit 4cdc542291
4 changed files with 35 additions and 6 deletions

View File

@@ -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())
}

View File

@@ -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)

View File

@@ -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 分析当前打分
</el-button>
</div>
<div v-if="aiLoading || aiContent" class="ai-card">
<div v-if="aiLoading || aiContent || aiError" class="ai-card">
<div class="ai-header">
<span>🤖 AI 分析</span>
<el-button text size="small" @click="closeAI" v-if="aiLoading">取消</el-button>

View File

@@ -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 () => {
</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="ai-section">
<el-button
v-if="!aiLoading && !aiContent && !aiError"
type="primary"
:loading="aiLoading"
@click="askAI"
>
🤖 AI 分析当前打分
</el-button>
<div v-if="aiLoading || aiContent || aiError" class="ai-card">
<div class="ai-header">
<span>🤖 AI 分析</span>
<el-button v-if="aiLoading" text size="small" @click="closeAI">取消</el-button>
</div>
<div class="ai-body">
<div v-if="aiContent" class="ai-text" v-html="aiContent.replace(/\n/g, '<br>')" />
<div v-if="aiError" class="ai-error">{{ aiError }}</div>
<div v-if="aiLoading && !aiContent" class="ai-loading"> 正在分析...</div>
</div>
</div>
</div>
</el-card>
</div>