修复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("Cache-Control", "no-cache")
|
||||||
w.Header().Set("Connection", "keep-alive")
|
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)
|
log.Printf("[ai] stream error: %v", err)
|
||||||
sendSSE(w, flusher, "error", err.Error())
|
sendSSE(w, flusher, "error", err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ func (r *statusRecorder) WriteHeader(code int) {
|
|||||||
r.ResponseWriter.WriteHeader(code)
|
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) {
|
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async function askAI() {
|
|||||||
aiContent.value = ''
|
aiContent.value = ''
|
||||||
aiError.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 = new EventSource(url)
|
||||||
es.addEventListener('token', (e) => {
|
es.addEventListener('token', (e) => {
|
||||||
aiContent.value += e.data
|
aiContent.value += e.data
|
||||||
@@ -44,7 +44,7 @@ async function askAI() {
|
|||||||
closeAI()
|
closeAI()
|
||||||
})
|
})
|
||||||
es.onerror = () => {
|
es.onerror = () => {
|
||||||
if (!aiContent.value) aiError.value = '连接中断'
|
if (!aiContent.value && !aiError.value) aiError.value = '连接中断'
|
||||||
closeAI()
|
closeAI()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ const quadrantLabel = (q: string) => {
|
|||||||
🤖 AI 分析当前打分
|
🤖 AI 分析当前打分
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="aiLoading || aiContent" class="ai-card">
|
<div v-if="aiLoading || aiContent || aiError" class="ai-card">
|
||||||
<div class="ai-header">
|
<div class="ai-header">
|
||||||
<span>🤖 AI 分析</span>
|
<span>🤖 AI 分析</span>
|
||||||
<el-button text size="small" @click="closeAI" v-if="aiLoading">取消</el-button>
|
<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 ts = encodeURIComponent(scoreResult.value.ts_code)
|
||||||
const td = encodeURIComponent(scoreResult.value.trade_date)
|
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('token', (e) => { aiContent.value += e.data })
|
||||||
aiES.addEventListener('error', (e) => {
|
aiES.addEventListener('error', (e) => {
|
||||||
aiError.value = (e as any)?.data || '请求失败'
|
aiError.value = (e as any)?.data || '请求失败'
|
||||||
@@ -103,7 +103,7 @@ async function askAI() {
|
|||||||
})
|
})
|
||||||
aiES.addEventListener('done', () => closeAI())
|
aiES.addEventListener('done', () => closeAI())
|
||||||
aiES.onerror = () => {
|
aiES.onerror = () => {
|
||||||
if (!aiContent.value) aiError.value = '连接中断'
|
if (!aiContent.value && !aiError.value) aiError.value = '连接中断'
|
||||||
closeAI()
|
closeAI()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,6 +232,28 @@ onMounted(async () => {
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</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>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user