refactor(AI): 四份 SSE 请求代码合成 streamAI + useAIStream
学情分析、班级分析、班级 PK、AI 提示四处原来各有一份几乎一样的 fetch + SSE 解析, 错误处理各漏各的: - 裸 fetch 绕过了 axios 拦截器,未登录时只显示「AI 分析生成失败」而不弹登录框。 拦截器里的全站错误处理抽成 handleGlobalApiError,两条路共用 - 班级分析、班级 PK 离开页面不中止流。useAIStream 在作用域销毁时自动 abort - AI 提示换了一次提交不掐上一条,旧提示会写进新提交的面板 utils/stream.ts 的 streamAI 负责请求 + 解析 + 错误翻译;组件用 useAIStream (waiting = 第一个字还没到,running = 流没结束);学情分析在 store 里直接用 streamAI。 浏览器实跑核过:学情分析逐字流出、provider 500 时显示后端的固定中文文案、 未登录调用弹出登录框。班级 PK / 班级分析卸载即中止这条没有在浏览器里点过 (本机库里没有班级数据),逻辑在 useAIStream 的 onScopeDispose。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
09030240b8
commit
c43da01ff2
7 files changed
+202
-205
No files matched your search
@@ -0,0 +1,62 @@
|
||||
import { streamAI } from "utils/stream"
|
||||
|
||||
/**
|
||||
* 组件里发 AI 流式请求:同一时刻只跑一条(再点一次会先掐掉上一条),组件卸载时
|
||||
* 自动中止 —— 原来班级分析、班级 PK 两处离开页面后流还在跑,白占一个 AI 名额。
|
||||
*
|
||||
* - `waiting`:请求发出、第一个字还没到。按钮 loading、占位转圈用它
|
||||
* - `running`:整条流还没结束
|
||||
*
|
||||
* `run` 失败时抛 Error(message 可直接给用户看);被中止时静默返回,不抛。
|
||||
*/
|
||||
export function useAIStream() {
|
||||
const waiting = ref(false)
|
||||
const running = ref(false)
|
||||
let controller: AbortController | null = null
|
||||
|
||||
function abort() {
|
||||
controller?.abort()
|
||||
controller = null
|
||||
waiting.value = false
|
||||
running.value = false
|
||||
}
|
||||
|
||||
async function run<Done = Record<string, unknown>>(
|
||||
path: string,
|
||||
body: unknown,
|
||||
handlers: {
|
||||
onDelta: (content: string) => void
|
||||
onDone?: (data: Done) => void
|
||||
},
|
||||
) {
|
||||
abort()
|
||||
const own = new AbortController()
|
||||
controller = own
|
||||
waiting.value = true
|
||||
running.value = true
|
||||
try {
|
||||
await streamAI<Done>(path, body, {
|
||||
...handlers,
|
||||
signal: own.signal,
|
||||
onDelta(content) {
|
||||
waiting.value = false
|
||||
handlers.onDelta(content)
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (own.signal.aborted) return
|
||||
throw error
|
||||
} finally {
|
||||
// 被下一次 run 顶掉的那条不碰状态,状态归新的那条管
|
||||
if (controller === own) {
|
||||
controller = null
|
||||
waiting.value = false
|
||||
running.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getCurrentScope()) onScopeDispose(abort)
|
||||
|
||||
return { waiting, running, run, abort }
|
||||
}
|
||||
Reference in new issue
Block a user