refactor(契约): 判题状态码收进契约唯一一份,并收紧 18 处 as any
- 状态码常量与 judgeStatusSchema 移到 packages/contract/src/judge-status.ts, 后端 judge/status.ts 只再导出;前端 SubmissionStatus 枚举加编译期断言对齐契约 (实测改坏一个码会当场类型检查失败) - 类型逃逸 22 处降到 4 处:collab/handler、pagination、configUpdate、 ExerciseManager、ProblemSubmission、pk.vue tooltip;剩下的是词云插件无类型、 生成的 .d.ts、skulpt 和 TextEditor Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -198,6 +198,7 @@ export async function handleCollabMessage(ws: CollabSocket, raw: string) {
|
||||
studentId?: unknown
|
||||
language?: unknown
|
||||
reason?: unknown
|
||||
timestamp?: unknown
|
||||
}
|
||||
try {
|
||||
message = JSON.parse(raw) as typeof message
|
||||
@@ -213,9 +214,7 @@ export async function handleCollabMessage(ws: CollabSocket, raw: string) {
|
||||
|
||||
// 心跳不查库,和 /ws/submissions 的处理一致
|
||||
if (message.type === "ping") {
|
||||
ws.send(
|
||||
JSON.stringify({ type: "pong", timestamp: (message as any).timestamp }),
|
||||
)
|
||||
ws.send(JSON.stringify({ type: "pong", timestamp: message.timestamp }))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
export const JudgeStatus = {
|
||||
COMPILE_ERROR: -2,
|
||||
WRONG_ANSWER: -1,
|
||||
ACCEPTED: 0,
|
||||
CPU_TIME_LIMIT_EXCEEDED: 1,
|
||||
REAL_TIME_LIMIT_EXCEEDED: 2,
|
||||
MEMORY_LIMIT_EXCEEDED: 3,
|
||||
RUNTIME_ERROR: 4,
|
||||
SYSTEM_ERROR: 5,
|
||||
PENDING: 6,
|
||||
JUDGING: 7,
|
||||
PARTIALLY_ACCEPTED: 8,
|
||||
AST_CHECK_FAILED: 10,
|
||||
} as const
|
||||
import { JudgeStatus, type JudgeStatusValue } from "@oj2/contract"
|
||||
|
||||
export type JudgeStatusValue = (typeof JudgeStatus)[keyof typeof JudgeStatus]
|
||||
// 状态码的唯一一份在 packages/contract/src/judge-status.ts,这里只再导出,
|
||||
// 省得二十几处 import 一起改
|
||||
export { JudgeStatus, type JudgeStatusValue }
|
||||
|
||||
export function isAccepted(result: number) {
|
||||
return (
|
||||
@@ -22,7 +11,7 @@ export function isAccepted(result: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 判题状态的中文名,和前端 `utils/constants.ts` 的 `JUDGE_STATUS` 一致,两边必须同步。
|
||||
* 判题状态的中文名,和前端 `utils/constants.ts` 的 `JUDGE_STATUS` 措辞对应(状态码本身已收进契约,名字仍是两份)。
|
||||
* 目前只用在喂给模型的 prompt 里 —— 原来那里拼的是裸状态码(`结果:-1`),
|
||||
* 模型根本不知道 -1 是「答案错误」还是别的什么,等于白给一条信息。
|
||||
*/
|
||||
|
||||
@@ -391,7 +391,7 @@ function typeTagType(type: ExerciseType) {
|
||||
{{ typeName(ex.type) }}
|
||||
</n-tag>
|
||||
<n-text style="margin-left: 10px">
|
||||
{{ (ex.data as any).question }}
|
||||
{{ (ex.data as { question?: string }).question }}
|
||||
</n-text>
|
||||
</div>
|
||||
<n-space :size="8">
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Legend,
|
||||
Colors,
|
||||
Filler,
|
||||
type TooltipItem,
|
||||
} from "chart.js"
|
||||
|
||||
// 注册Chart.js组件
|
||||
@@ -673,9 +674,12 @@ const radarChartOptions = {
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context: any) {
|
||||
const dataset = context.dataset as any
|
||||
const rawValue = dataset?.rawData?.[context.dataIndex]
|
||||
label: function (context: TooltipItem<"radar">) {
|
||||
// rawData 是我们自己塞进 dataset 的扩展字段,chart.js 的类型里没有
|
||||
const dataset = context.dataset as typeof context.dataset & {
|
||||
rawData?: (number | null)[]
|
||||
}
|
||||
const rawValue = dataset.rawData?.[context.dataIndex]
|
||||
const metric = context.label || ""
|
||||
const isRate = context.dataIndex >= 3
|
||||
if (rawValue === undefined || rawValue === null) {
|
||||
|
||||
@@ -298,7 +298,7 @@ watch(query, listSubmissions)
|
||||
<n-tag
|
||||
v-for="item in statusDistribution"
|
||||
:key="item.result"
|
||||
:type="item.type as any"
|
||||
:type="item.type"
|
||||
size="small"
|
||||
round
|
||||
>
|
||||
|
||||
@@ -19,7 +19,8 @@ export function useConfigUpdate() {
|
||||
const handleConfigUpdate = (data: ConfigUpdate) => {
|
||||
// 认不出来的键直接忽略:后端将来多推一个字段,不该把 store 撑出个野字段
|
||||
if (!(data.key in configStore.config)) return
|
||||
;(configStore.config as any)[data.key] = data.value
|
||||
;(configStore.config as unknown as Record<string, unknown>)[data.key] =
|
||||
data.value
|
||||
// getConfig() 里也是这么设的,站点改名后标签页跟着变,别只更新页面里那份
|
||||
if (data.key === "websiteName") document.title = data.value
|
||||
}
|
||||
|
||||
@@ -42,11 +42,13 @@ export function usePagination<T extends Record<string, any>>(
|
||||
limit: parseInt(<string>route.query.limit) || defaultLimit,
|
||||
...initialQuery,
|
||||
}) as unknown as T & PaginationQuery
|
||||
// 键是运行时按 initialQuery 枚举出来的,静态类型写不出来;写入统一走这一个口子
|
||||
const writable = query as Record<string, unknown>
|
||||
|
||||
// 同步 URL 查询参数到本地状态
|
||||
function syncFromRoute() {
|
||||
;(query as any).page = parseInt(<string>route.query.page) || defaultPage
|
||||
;(query as any).limit = parseInt(<string>route.query.limit) || defaultLimit
|
||||
writable.page = parseInt(<string>route.query.page) || defaultPage
|
||||
writable.limit = parseInt(<string>route.query.limit) || defaultLimit
|
||||
|
||||
// 同步其他查询参数
|
||||
Object.keys(initialQuery).forEach((key) => {
|
||||
@@ -54,11 +56,11 @@ export function usePagination<T extends Record<string, any>>(
|
||||
if (value !== undefined) {
|
||||
// 处理不同类型的参数
|
||||
if (typeof initialQuery[key] === "boolean") {
|
||||
;(query as any)[key] = value === "1" || value === "true"
|
||||
writable[key] = value === "1" || value === "true"
|
||||
} else if (typeof initialQuery[key] === "number") {
|
||||
;(query as any)[key] = parseInt(<string>value) || initialQuery[key]
|
||||
writable[key] = parseInt(<string>value) || initialQuery[key]
|
||||
} else {
|
||||
;(query as any)[key] = <string>value || initialQuery[key]
|
||||
writable[key] = <string>value || initialQuery[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -75,7 +77,7 @@ export function usePagination<T extends Record<string, any>>(
|
||||
|
||||
// 重置页码到第一页
|
||||
function resetPage() {
|
||||
;(query as any).page = defaultPage
|
||||
writable.page = defaultPage
|
||||
}
|
||||
|
||||
// 清空所有查询条件(除了分页参数)
|
||||
@@ -83,13 +85,13 @@ export function usePagination<T extends Record<string, any>>(
|
||||
Object.keys(initialQuery).forEach((key) => {
|
||||
const initialValue = initialQuery[key]
|
||||
if (typeof initialValue === "string") {
|
||||
;(query as any)[key] = ""
|
||||
writable[key] = ""
|
||||
} else if (typeof initialValue === "boolean") {
|
||||
;(query as any)[key] = false
|
||||
writable[key] = false
|
||||
} else if (typeof initialValue === "number") {
|
||||
;(query as any)[key] = 0
|
||||
writable[key] = 0
|
||||
} else {
|
||||
;(query as any)[key] = initialValue
|
||||
writable[key] = initialValue
|
||||
}
|
||||
})
|
||||
resetPage()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JudgeStatusValue } from "@oj2/contract"
|
||||
import type { AchievementRarity, SUBMISSION_RESULT, ReactionKey } from "./types"
|
||||
|
||||
// 与后端 judge/status.ts 的 JudgeStatus 逐条对齐(submitting 除外,见下)。
|
||||
@@ -21,6 +22,18 @@ export enum SubmissionStatus {
|
||||
ast_check_failed = 10,
|
||||
}
|
||||
|
||||
// 编译期对齐契约:契约加/改一个码而这里没跟,下面两行会当场编译不过。
|
||||
type SyncedWithContract =
|
||||
Exclude<
|
||||
`${SubmissionStatus}`,
|
||||
`${SubmissionStatus.submitting}`
|
||||
> extends `${JudgeStatusValue}`
|
||||
? `${JudgeStatusValue}` extends `${Exclude<SubmissionStatus, SubmissionStatus.submitting>}`
|
||||
? true
|
||||
: never
|
||||
: never
|
||||
export const _submissionStatusSynced: SyncedWithContract = true
|
||||
|
||||
export enum ContestStatus {
|
||||
initial = "2", // 这里不需要传入到后端,只是为了一开始加载数据的时候,做一个初始位
|
||||
not_started = "1",
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from "./common"
|
||||
export * from "./content"
|
||||
export * from "./contest"
|
||||
export * from "./flowchart"
|
||||
export * from "./judge-status"
|
||||
export * from "./language"
|
||||
export * from "./problem"
|
||||
export * from "./problemset"
|
||||
|
||||
32
packages/contract/src/judge-status.ts
Normal file
32
packages/contract/src/judge-status.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* 判题状态码 —— **前后端唯一的一份**。
|
||||
*
|
||||
* 这些整数是落库的值:12 万条历史提交的 `submission.result` 就是它们,判题沙箱回的
|
||||
* 也是这套编码,所以只能新增、不能改已有的含义。后端 `judge/status.ts` 从这里再导出,
|
||||
* 前端 `utils/constants.ts` 的 `SubmissionStatus` 用类型断言逐条对齐这里。
|
||||
*/
|
||||
export const JudgeStatus = {
|
||||
COMPILE_ERROR: -2,
|
||||
WRONG_ANSWER: -1,
|
||||
ACCEPTED: 0,
|
||||
CPU_TIME_LIMIT_EXCEEDED: 1,
|
||||
REAL_TIME_LIMIT_EXCEEDED: 2,
|
||||
MEMORY_LIMIT_EXCEEDED: 3,
|
||||
RUNTIME_ERROR: 4,
|
||||
SYSTEM_ERROR: 5,
|
||||
PENDING: 6,
|
||||
JUDGING: 7,
|
||||
PARTIALLY_ACCEPTED: 8,
|
||||
AST_CHECK_FAILED: 10,
|
||||
} as const
|
||||
|
||||
export type JudgeStatusValue = (typeof JudgeStatus)[keyof typeof JudgeStatus]
|
||||
|
||||
// 同名的类型:原来契约里就有 `type JudgeStatus`(各处按类型引用),值与类型同名合并
|
||||
export type JudgeStatus = JudgeStatusValue
|
||||
|
||||
export const judgeStatusSchema = z.literal(
|
||||
Object.values(JudgeStatus) as [JudgeStatusValue, ...JudgeStatusValue[]],
|
||||
)
|
||||
@@ -1,23 +1,9 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { paginatedSchema } from "./common"
|
||||
import { judgeStatusSchema, type JudgeStatus } from "./judge-status"
|
||||
import { problemLanguageSchema } from "./language"
|
||||
|
||||
export const judgeStatusSchema = z.union([
|
||||
z.literal(-2),
|
||||
z.literal(-1),
|
||||
z.literal(0),
|
||||
z.literal(1),
|
||||
z.literal(2),
|
||||
z.literal(3),
|
||||
z.literal(4),
|
||||
z.literal(5),
|
||||
z.literal(6),
|
||||
z.literal(7),
|
||||
z.literal(8),
|
||||
z.literal(10),
|
||||
])
|
||||
|
||||
/**
|
||||
* 判题机原始输出(`submission.info` 的 JSONB 原文)。**只是类型,不作运行时校验。**
|
||||
*
|
||||
@@ -397,7 +383,6 @@ export const formatCodeRequestSchema = z.object({
|
||||
|
||||
export const formatCodeResponseSchema = z.object({ code: z.string() })
|
||||
|
||||
export type JudgeStatus = z.infer<typeof judgeStatusSchema>
|
||||
export type StatisticInfo = z.infer<typeof statisticInfoSchema>
|
||||
export type CreateSubmissionRequest = z.infer<
|
||||
typeof createSubmissionRequestSchema
|
||||
|
||||
Reference in New Issue
Block a user