Files
OJ2/packages/contract/src/ai.ts
yuetsh d3b05b8629 refactor(契约): 补齐 80 个类型导出,前端不再手抄形状
契约原来有 184 个 schema 但只导出了 104 个类型,缺的那 80 个前端只能照着
手抄一遍 —— 这是「表格列静默空白」那一类 bug 的根因(d3348f9、2edb8cf,
以及上一个 commit 修的 Top100 两列)。现在 186 个 schema 对 186 个类型,
一一对应,下次要用直接 import。

补导出是机械的(fooSchema → Foo),零命名冲突。真正有价值的是换的过程中
契约逼出来的 5 处分歧 —— 手抄那份在说谎,而 vue-tsc 拦不住,因为类型说它是对的:

- Tutorial.createdBy 手抄成了可选的 `User`(即 AdminUser,带 email、
  rawPassword),后端下发的是必有的 SampleUser。读 createdBy.email 会拿到
  undefined。列表页因此被迫写 `row.createdBy?.username` 和 `row.createdAt!`,
  换成契约类型后两处断言都不需要了。
- TutorialListItem 手抄成 `Omit<Tutorial, "content">`,但后端列表接口连 code
  一起省了 —— 类型声称 code 在。
- Testcase 手抄成 `{input_name, output_name, score}`:响应实际有 5 个字段,
  且**没有 score**。score 是上传完成后前端按测试点数量平分补上去的,手抄那份
  把本地字段说成了响应字段。现在写成 `TestCaseEntry & { score: string }`。
- Tag 手抄成 `{id, name}`,契约是 `{id, name, problemCount}` —— shared/api.ts
  只好用 `Tag & { problemCount: number }` 把丢掉的补回来。
- CreateMessage 是旧后端按名字投递的形状(sender/recipient/submission),
  契约要的是 recipientId/submissionId。全仓零引用,删掉。

同时删掉另外两个零引用的手写类型:LANGUAGE_SHOW_LABEL、UserAdminType;
本地重复的 SampleUser 换成契约的;oj/problem/list.vue 里本地第三份 Tag 改成
`ContractTag & { checked: boolean }`。

需要收窄的一律**从契约派生再收窄**,字段名跟着契约走,只有真正本地的那一两个
键是自己的:

    export type Exercise = Omit<AdminExercise, "data"> & { data: 七种题型的联合 }
    export type SubmitCodePayload =
      Omit<CreateSubmissionRequest, "language"> & { language: LANGUAGE }

保留不动的窄化:StatisticInfo / SubmissionInfo(判题 JSONB 原文,snake_case)、
SQLDisplay*、ProblemFiltered(视图模型)、Exercise*Data(契约里 data 就是
Record<string, unknown>,七种题型结构不同,后端本来也不校验)。
types.ts 的手写 interface 从 31 个降到 22 个。

顺带修的代码:admin/tutorial/detail.vue 新建教程的表单对象缺三个后端产出的
字段,加了 TutorialEdit(对齐 BlankProblem / BlankContest 的写法);三处测试点
上传原来是拿响应对象原地塞 score,改成 map 出新对象,分数算法一字未改。

验证:apps/api tsc(7.0.2) 0 error、check:routes 168 条无遮蔽、
apps/web vue-tsc 0 error、vite build 通过。改动绝大部分在类型层,运行时只有
测试点上传那三处(等价替换)—— **那条路径要传 zip 才能实跑,没有实打**,
只做了代码等价性核对。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 18:07:01 -06:00

117 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod"
/**
* 评级。`grade()` 返回 S/A/B/C`averageGrade()` 在没有可用数据时返回空串 ——
* 空串是真会下发的值,别把它从这里去掉,前端要按「无评级」处理。
*/
export const gradeSchema = z.enum(["S", "A", "B", "C", ""])
export const durationDataSchema = z.object({
unit: z.string(),
index: z.number().int(),
start: z.string(),
end: z.string(),
grade: gradeSchema,
problemCount: z.number().int(),
submissionCount: z.number().int(),
})
export const solvedProblemSchema = z.object({
problem: z.object({
title: z.string(),
displayId: z.string(),
contestTitle: z.string(),
contestId: z.number().int().nullable(),
}),
acTime: z.string(),
rank: z.number().int().nullable(),
acCount: z.number().int(),
grade: gradeSchema,
periodRank: z.number().int().nullable(),
periodAcCount: z.number().int(),
difficulty: z.string(),
})
export const flowchartSummarySchema = z.object({
problemId: z.string(),
problemTitle: z.string(),
submissionCount: z.number().int(),
bestScore: z.number(),
bestGrade: z.string(),
latestSubmissionTime: z.string(),
avgScore: z.number(),
})
export const aiDetailSchema = z.object({
user: z.string(),
className: z.string().nullable(),
start: z.string(),
end: z.string(),
solved: z.array(solvedProblemSchema),
flowcharts: z.array(flowchartSummarySchema),
grade: gradeSchema,
tags: z.record(z.string(), z.number().int()),
difficulty: z.record(z.string(), z.number().int()),
contestCount: z.number().int(),
})
export const aiAnalysisRequestSchema = z.object({
details: z.unknown(),
duration: z.unknown(),
})
export const aiHintRequestSchema = z.object({ submissionId: z.string().min(1) })
export const classAnalysisRequestSchema = z.object({
comparison: z.record(z.string(), z.unknown()),
})
export const classPkAnalysisRequestSchema = z.object({
comparisons: z.array(z.record(z.string(), z.unknown())).min(2),
timeRangeLabel: z.string().default("全部时间"),
})
export const heatmapItemSchema = z.object({
timestamp: z.number(),
value: z.number().int(),
})
export const aiAnalysisRecordSchema = z.object({
id: z.number().int(),
provider: z.string(),
model: z.string(),
data: z.record(z.string(), z.unknown()),
analysis: z.string(),
createTime: z.string(),
isPinned: z.boolean(),
username: z.string().optional(),
})
export const loginSummarySchema = z.object({
summary: z.object({
start: z.string(),
end: z.string(),
newProblemCount: z.number().int(),
submissionCount: z.number().int(),
acceptedCount: z.number().int(),
solvedCount: z.number().int(),
flowchartSubmissionCount: z.number().int(),
}),
analysis: z.string(),
analysisError: z.string().optional(),
})
export type Grade = z.infer<typeof gradeSchema>
export type DurationData = z.infer<typeof durationDataSchema>
export type SolvedProblem = z.infer<typeof solvedProblemSchema>
export type FlowchartSummary = z.infer<typeof flowchartSummarySchema>
export type AiDetail = z.infer<typeof aiDetailSchema>
export type HeatmapItem = z.infer<typeof heatmapItemSchema>
export type AiAnalysisRecord = z.infer<typeof aiAnalysisRecordSchema>
export type LoginSummary = z.infer<typeof loginSummarySchema>
export type AiAnalysisRequest = z.infer<typeof aiAnalysisRequestSchema>
export type AiHintRequest = z.infer<typeof aiHintRequestSchema>
export type ClassAnalysisRequest = z.infer<typeof classAnalysisRequestSchema>
export type ClassPkAnalysisRequest = z.infer<typeof classPkAnalysisRequestSchema>