修复AI分析SSE流断开:中间件透传Flusher、DB Key生效、错误提示不覆盖
This commit is contained in:
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user