feat(AI 提示): 提示落库并收集学生评价,落进 ai_hint
Some checks failed
Deploy / deploy (push) Has been cancelled

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:
2026-09-19 06:00:10 -06:00
parent a0ef204bd2
commit 47d8f46bdb
9 changed files with 4220 additions and 21 deletions

View File

@@ -991,6 +991,60 @@ export const submissionTrace = pgTable(
],
)
/**
* 每一次「让 AI 分析我的代码」POST /ai/hint的留痕连同学生的评价。
*
* 在此之前提示一条都没落库,用了多少次、在哪些题上用、用完有没有做出来都无从知道;
* 之后要做的提示分级、错误归因都得拿这张表做对照。**生成失败的也记**content 为空、
* error 有值),失败率是 AI 功能悄悄变差时最先动的那个数。
*
* - 不存 prompt 原文:学生代码在 submission 里、题面在 problem 里,重复存一遍没有意义。
* 存的是 `prompt_version` —— 改 system / prompt 的拼法时在 routes/ai.ts 里加一,
* 事后才分得清哪批提示是按哪版生成的。
* - 人和题顺着 submission 查submission_id 上有索引)。和 submission_trace 一样
* CASCADE 挂在提交上:提交没了,这条提示也就没有上下文了。
* - 同一条提交可以有多条:刷新页面之后按钮会重新出现。
*/
export const aiHint = pgTable(
"ai_hint",
{
id: bigint({ mode: "number" }).primaryKey().generatedByDefaultAsIdentity({
name: "ai_hint_id_seq",
startWith: 1,
increment: 1,
minValue: 1,
maxValue: "9223372036854775807",
cache: 1,
}),
submissionId: text("submission_id").notNull(),
model: text().notNull(),
promptVersion: integer("prompt_version").notNull(),
// 生成失败时为空串,失败原因在 error
content: text().notNull(),
error: text(),
// 从收到请求到生成结束(或失败)的毫秒数
durationMs: integer("duration_ms").notNull(),
// 学生的评价null = 没评
helpful: boolean(),
feedbackTime: timestamp("feedback_time", {
withTimezone: true,
mode: "string",
}),
createTime: timestamp("create_time", {
withTimezone: true,
mode: "string",
}).notNull(),
},
(table) => [
index("ai_hint_submission_id_idx").on(table.submissionId),
foreignKey({
columns: [table.submissionId],
foreignColumns: [submission.id],
name: "ai_hint_submission_id_fk_submission_id",
}).onDelete("cascade"),
],
)
export const tutorial = pgTable(
"tutorial",
{