AI 时代 OJ 设计的 2a:先把现有提示记下来,为后面的分级和两段式诊断攒对照数据。
提示本身的行为不变,发给模型的 prompt 一字未改。
- 迁移 0017 建 ai_hint:模型、prompt 版本、内容、错误、耗时、学生评价;
成功和失败都记(失败率是 AI 悄悄变差时最先动的数)。挂在 submission 上 CASCADE
- streamChat 第三个参数改成 { onComplete, onError }:onComplete 的返回值并进
done 事件,提示 id 由此带回前端;班级学情分析那处调用同步改写,行为不变
- 落库失败只记日志,不影响学生拿到提示
- 新增 POST /ai/hint/:id/feedback:只能评自己的提示(别人的一律 404),可以改票
- 前端提示生成完出「有帮助 / 没帮助」,选中的高亮;后端没存上时不出按钮
- 实跑:本地假 LLM 验证成功 / provider 断开 / 缺 AI_KEY 三条路径的落库,
评价接口六种请求,浏览器里按钮出现、改票、重复点不发请求
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,16 +51,34 @@ export async function completeChat(system: string, user: string) {
|
||||
return payload.choices?.[0]?.message?.content?.trim() ?? ""
|
||||
}
|
||||
|
||||
export interface StreamChatHooks {
|
||||
/**
|
||||
* 生成完整结束后调,拿到的是全文。**返回的对象会并进 `done` 事件**,
|
||||
* 用来把落库之后才有的东西(比如 ai_hint 的 id)交给前端。
|
||||
*/
|
||||
onComplete?: (value: string) => Promise<Record<string, unknown> | void>
|
||||
/**
|
||||
* 生成失败时调(没配 AI_KEY、provider 报错、流中途断掉)。只用来留痕,
|
||||
* 抛出的异常会被吞掉 —— 记录失败不该再搅乱这条流本身的收尾。
|
||||
*/
|
||||
onError?: (message: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function streamChat(
|
||||
system: string,
|
||||
user: string,
|
||||
onComplete?: (value: string) => Promise<void>,
|
||||
hooks: StreamChatHooks = {},
|
||||
) {
|
||||
const encoder = new TextEncoder()
|
||||
const reportError = (message: string) =>
|
||||
hooks.onError?.(message).catch((error) => {
|
||||
console.error("streamChat onError hook failed", error)
|
||||
})
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
const send = (value: string) => controller.enqueue(encoder.encode(value))
|
||||
if (!config.aiKey) {
|
||||
await reportError("缺少 AI_KEY")
|
||||
send(
|
||||
`data: ${JSON.stringify({ type: "error", message: "缺少 AI_KEY" })}\n\n`,
|
||||
)
|
||||
@@ -127,12 +145,15 @@ export function streamChat(
|
||||
if (done) break
|
||||
}
|
||||
const full = chunks.join("").trim()
|
||||
if (onComplete) await onComplete(full)
|
||||
send(`data: ${JSON.stringify({ type: "done" })}\n\n`)
|
||||
const extra = hooks.onComplete
|
||||
? await hooks.onComplete(full)
|
||||
: undefined
|
||||
send(`data: ${JSON.stringify({ ...extra, type: "done" })}\n\n`)
|
||||
} catch (error) {
|
||||
send(
|
||||
`data: ${JSON.stringify({ type: "error", message: error instanceof Error ? error.message : String(error) })}\n\n`,
|
||||
)
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
// 先留痕再回前端:客户端已经断开的话下面这个 send 自己也会抛
|
||||
await reportError(message)
|
||||
send(`data: ${JSON.stringify({ type: "error", message })}\n\n`)
|
||||
} finally {
|
||||
send("event: end\n\n")
|
||||
controller.close()
|
||||
|
||||
Reference in New Issue
Block a user