style(格式化): 改成单引号、一行最长 160,全仓重新格式化
Deploy / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 13:52:24 +08:00
1 parent 530e1a3d5d
commit 195069450f
480 files changed
+15507 -24650

No files matched your search

+8 -8
View File
@@ -20,17 +20,17 @@
* 这种,语法里两个都存在,机器看不出来)。语义那一层还是得靠实跑。
*/
import { AST_NODE_TARGETS_BY_LANGUAGE } from "@oj2/contract"
import { Language, Parser } from "web-tree-sitter"
import { AST_NODE_TARGETS_BY_LANGUAGE } from '@oj2/contract'
import { Language, Parser } from 'web-tree-sitter'
import cWasmPath from "tree-sitter-c/tree-sitter-c.wasm" with { type: "file" }
import cppWasmPath from "tree-sitter-cpp/tree-sitter-cpp.wasm" with { type: "file" }
import pythonWasmPath from "tree-sitter-python/tree-sitter-python.wasm" with { type: "file" }
import treeSitterWasmPath from "web-tree-sitter/web-tree-sitter.wasm" with { type: "file" }
import cWasmPath from 'tree-sitter-c/tree-sitter-c.wasm' with { type: 'file' }
import cppWasmPath from 'tree-sitter-cpp/tree-sitter-cpp.wasm' with { type: 'file' }
import pythonWasmPath from 'tree-sitter-python/tree-sitter-python.wasm' with { type: 'file' }
import treeSitterWasmPath from 'web-tree-sitter/web-tree-sitter.wasm' with { type: 'file' }
const WASM_BY_LANGUAGE: Record<string, string> = {
C: cWasmPath,
"C++": cppWasmPath,
'C++': cppWasmPath,
Python: pythonWasmPath,
}
@@ -61,7 +61,7 @@ for (const [language, table] of Object.entries(AST_NODE_TARGETS_BY_LANGUAGE)) {
console.log(`检查了 ${checked} 个 AST target 的节点类型`)
if (missing.length === 0) {
console.log("✓ 每个 target 的 node 都在对应语言的语法里真实存在")
console.log('✓ 每个 target 的 node 都在对应语言的语法里真实存在')
process.exit(0)
}
for (const { language, target, node } of missing) {
+17 -20
View File
@@ -21,10 +21,10 @@
* 动态拼出来的路径看不见 —— 但本仓库没有那种写法,加的时候请保持。
*/
import { readFileSync, readdirSync, statSync } from "node:fs"
import { join, resolve } from "node:path"
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join, resolve } from 'node:path'
const SRC = resolve(import.meta.dir, "..")
const SRC = resolve(import.meta.dir, '..')
interface Route {
method: string
@@ -36,23 +36,23 @@ function walk(dir: string, out: string[] = []) {
for (const entry of readdirSync(dir)) {
const path = join(dir, entry)
if (statSync(path).isDirectory()) walk(path, out)
else if (entry.endsWith(".ts")) out.push(path)
else if (entry.endsWith('.ts')) out.push(path)
}
return out
}
/** 先注册的 pattern 会不会把后注册的 target 吃掉 */
export function shadows(pattern: string, target: string) {
const a = pattern.split("/").filter(Boolean)
const b = target.split("/").filter(Boolean)
const a = pattern.split('/').filter(Boolean)
const b = target.split('/').filter(Boolean)
if (a.length !== b.length) return false
let usedParam = false
for (let i = 0; i < a.length; i++) {
const seg = a[i]!
const other = b[i]!
if (seg.startsWith(":")) {
if (seg.startsWith(':')) {
// 参数段吃得掉任何字面量段;两边都是参数说明本来就是同一条,不算遮蔽
if (other.startsWith(":")) continue
if (other.startsWith(':')) continue
usedParam = true
continue
}
@@ -64,7 +64,7 @@ export function shadows(pattern: string, target: string) {
function collect(): Route[] {
const routerFile = new Map<string, string>()
for (const file of walk(SRC)) {
for (const m of readFileSync(file, "utf8").matchAll(/export const (\w+) = new Hono/g)) {
for (const m of readFileSync(file, 'utf8').matchAll(/export const (\w+) = new Hono/g)) {
routerFile.set(m[1]!, file)
}
}
@@ -72,34 +72,31 @@ function collect(): Route[] {
const routesOf = (router: string, prefix: string): Route[] => {
const file = routerFile.get(router)
if (!file) return []
const text = readFileSync(file, "utf8")
const text = readFileSync(file, 'utf8')
// 直接注册的路由和嵌套挂载(`router.route("/", child)`)放在一起按出现位置排序:
// 子路由挂在哪个位置,它的路由就在哪个位置参与匹配
const pattern = new RegExp(
`${router}\\.(get|post|put|delete|patch)\\(\\s*"([^"]+)"|${router}\\.route\\(\\s*"([^"]*)"\\s*,\\s*(\\w+)\\s*\\)`,
"g",
)
const pattern = new RegExp(`${router}\\.(get|post|put|delete|patch)\\(\\s*"([^"]+)"|${router}\\.route\\(\\s*"([^"]*)"\\s*,\\s*(\\w+)\\s*\\)`, 'g')
return [...text.matchAll(pattern)].flatMap((m) => {
if (m[4]) return routesOf(m[4], prefix + m[3]!)
return [
{
method: m[1]!.toUpperCase(),
path: (prefix + m[2]!).replace(/\/+/g, "/").replace(/\/$/, "") || "/",
file: file.replace(SRC + "/", ""),
path: (prefix + m[2]!).replace(/\/+/g, '/').replace(/\/$/, '') || '/',
file: file.replace(SRC + '/', ''),
},
]
})
}
// 挂载顺序就是匹配顺序,所以必须按 index.ts 里出现的先后来摊平
const index = readFileSync(join(SRC, "index.ts"), "utf8")
const adminIndex = readFileSync(join(SRC, "routes/admin/index.ts"), "utf8")
const index = readFileSync(join(SRC, 'index.ts'), 'utf8')
const adminIndex = readFileSync(join(SRC, 'routes/admin/index.ts'), 'utf8')
const adminMounts = [...adminIndex.matchAll(/\.route\(\s*"([^"]*)"\s*,\s*(\w+)\s*\)/g)]
const all: Route[] = []
for (const m of index.matchAll(/app\.route\(\s*"([^"]+)"\s*,\s*(\w+)\s*\)/g)) {
const [, prefix, router] = m
if (router === "adminRoutes") {
if (router === 'adminRoutes') {
for (const a of adminMounts) all.push(...routesOf(a[2]!, prefix! + a[1]!))
} else {
all.push(...routesOf(router!, prefix!))
@@ -119,7 +116,7 @@ for (let i = 0; i < routes.length; i++) {
console.log(`按注册顺序检查了 ${routes.length} 条路由`)
if (hits.length === 0) {
console.log("✓ 没有路由被遮蔽")
console.log('✓ 没有路由被遮蔽')
process.exit(0)
}
for (const [first, second] of hits) {
+26 -34
View File
@@ -1,9 +1,9 @@
import { eq, sql } from "drizzle-orm"
import { eq, sql } from 'drizzle-orm'
import { db, schema } from "../db"
import { JudgeStatus, isAccepted } from "../judge/status"
import { asRecord } from "../routes/helpers"
import { metaAchievements, refreshUnlockedCount, rescanAchievement } from "../services/achievements"
import { db, schema } from '../db'
import { JudgeStatus, isAccepted } from '../judge/status'
import { asRecord } from '../routes/helpers'
import { metaAchievements, refreshUnlockedCount, rescanAchievement } from '../services/achievements'
/**
* 把反范式的计数列重算回与 submission 表一致。
@@ -144,7 +144,7 @@ async function expectedProfiles() {
}
for (const row of perProblem) {
const current = expected.get(row.user_id) ?? blank()
const bucket = row.is_public ? "problems" : "contest_problems"
const bucket = row.is_public ? 'problems' : 'contest_problems'
current.status[bucket] ??= {}
current.status[bucket]![String(row.problem_id)] = {
status: row.ever_accepted ? JudgeStatus.ACCEPTED : row.last_result,
@@ -157,14 +157,12 @@ async function expectedProfiles() {
/** 稳定序列化,用来比对 jsonb —— 键序不同不该被当成差异 */
function stable(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`
if (value && typeof value === "object") {
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) =>
a < b ? -1 : 1,
)
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stable(v)}`).join(",")}}`
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`
if (value && typeof value === 'object') {
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : 1))
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stable(v)}`).join(',')}}`
}
return JSON.stringify(value) ?? "null"
return JSON.stringify(value) ?? 'null'
}
type Diff = { label: string; field: string; before: unknown; after: unknown }
@@ -216,21 +214,20 @@ async function unlockedCountPlan(plan: Plan) {
if (row.counter !== row.actual) {
plan.diffs.push({
label,
field: "achievement_unlocked_count",
field: 'achievement_unlocked_count',
before: row.counter ?? null,
after: row.actual,
})
plan.unlockedCountFixes.push(row.user_id)
}
for (const meta of metas) {
const met =
meta.operator === "gte" ? row.actual >= meta.threshold : row.actual <= meta.threshold
const met = meta.operator === 'gte' ? row.actual >= meta.threshold : row.actual <= meta.threshold
if (!met || held.has(`${row.user_id}:${meta.id}`)) continue
plan.diffs.push({
label,
field: `成就「${meta.name}」`,
before: "未发",
after: "补发",
before: '未发',
after: '补发',
})
plan.metaGrants.push({ userId: row.user_id, achievementId: meta.id })
}
@@ -281,7 +278,7 @@ async function computePlan(): Promise<Plan> {
if (problem.submissionNumber !== want.submissionNumber) {
rows.push({
label,
field: "submission_number",
field: 'submission_number',
before: problem.submissionNumber,
after: want.submissionNumber,
})
@@ -289,7 +286,7 @@ async function computePlan(): Promise<Plan> {
if (problem.acceptedNumber !== want.acceptedNumber) {
rows.push({
label,
field: "accepted_number",
field: 'accepted_number',
before: problem.acceptedNumber,
after: want.acceptedNumber,
})
@@ -297,7 +294,7 @@ async function computePlan(): Promise<Plan> {
if (stable(asRecord(problem.statisticInfo)) !== stable(want.statisticInfo)) {
rows.push({
label,
field: "statistic_info",
field: 'statistic_info',
before: problem.statisticInfo,
after: want.statisticInfo,
})
@@ -327,7 +324,7 @@ async function computePlan(): Promise<Plan> {
if (profile.submissionNumber !== want.submissionNumber) {
rows.push({
label,
field: "submission_number",
field: 'submission_number',
before: profile.submissionNumber,
after: want.submissionNumber,
})
@@ -335,19 +332,16 @@ async function computePlan(): Promise<Plan> {
if (profile.acceptedNumber !== want.acceptedNumber) {
rows.push({
label,
field: "accepted_number",
field: 'accepted_number',
before: profile.acceptedNumber,
after: want.acceptedNumber,
})
}
if (stable(existing) !== stable(merged)) {
const keys = new Set([
...Object.keys(asRecord(existing.problems)),
...Object.keys(want.status.problems ?? {}),
])
const keys = new Set([...Object.keys(asRecord(existing.problems)), ...Object.keys(want.status.problems ?? {})])
rows.push({
label,
field: "acm_problems_status",
field: 'acm_problems_status',
before: `${Object.keys(asRecord(existing.problems)).length} 题`,
after: `${keys.size} 题(含比赛桶重建)`,
})
@@ -366,9 +360,7 @@ function report(plan: Plan) {
`发现 ${plan.diffs.length} 处不一致(题目 ${plan.problemFixes.length} 道 / 用户 ${plan.profileFixes.length} 人 / 已解锁数 ${plan.unlockedCountFixes.length} 人 / 元成就补发 ${plan.metaGrants.length} 条):`,
)
for (const diff of plan.diffs.slice(0, 40)) {
console.log(
` ${diff.label} ${diff.field}: ${JSON.stringify(diff.before)} → ${JSON.stringify(diff.after)}`,
)
console.log(` ${diff.label} ${diff.field}: ${JSON.stringify(diff.before)} → ${JSON.stringify(diff.after)}`)
}
if (plan.diffs.length > 40) console.log(` ……另有 ${plan.diffs.length - 40} 处`)
}
@@ -377,13 +369,13 @@ function report(plan: Plan) {
export async function recount(options: { apply: boolean }) {
const plan = await computePlan()
if (plan.diffs.length === 0) {
console.log("计数列与 submission / user_achievement 一致,没有要订正的。")
console.log('计数列与 submission / user_achievement 一致,没有要订正的。')
return 0
}
report(plan)
if (!options.apply) {
console.log("\n以上为预演,没有写库。确认无误后加 --apply 落库。")
console.log('\n以上为预演,没有写库。确认无误后加 --apply 落库。')
return 0
}
@@ -423,7 +415,7 @@ export async function recount(options: { apply: boolean }) {
// 必须让部署脚本看见非零退出码,而不是打一行字了事。
const after = await computePlan()
if (after.diffs.length === 0) {
console.log("复核通过:计数列与 submission / user_achievement 一致")
console.log('复核通过:计数列与 submission / user_achievement 一致')
return 0
}
console.error(`复核未通过,仍有 ${after.diffs.length} 处差异:`)
+15 -23
View File
@@ -1,10 +1,10 @@
import { and, eq, inArray, isNull, sql } from "drizzle-orm"
import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
import { db, schema } from "../db"
import { diagnoseRuntimeError, templateForLanguage, type JudgeCase } from "../judge/run"
import { JudgeStatus } from "../judge/status"
import { parseProblemTemplate } from "../judge/template"
import { asRecord } from "../routes/helpers"
import { db, schema } from '../db'
import { diagnoseRuntimeError, templateForLanguage, type JudgeCase } from '../judge/run'
import { JudgeStatus } from '../judge/status'
import { parseProblemTemplate } from '../judge/template'
import { asRecord } from '../routes/helpers'
/**
* 把库里运行时错误的诊断(`statistic_info.runtime_error`)按现在的规则重做一遍。
@@ -30,7 +30,7 @@ import { asRecord } from "../routes/helpers"
/** jsonb 读回来的键顺序和新拼的对象不一样,按排好序的键比。诊断是一层的平对象 */
function sameKeys(value: unknown) {
const record = asRecord(value)
return value ? JSON.stringify(record, Object.keys(record).sort()) : "null"
return value ? JSON.stringify(record, Object.keys(record).sort()) : 'null'
}
export async function rediagnose({ apply }: { apply: boolean }) {
@@ -42,13 +42,11 @@ export async function rediagnose({ apply }: { apply: boolean }) {
and(
eq(schema.submission.result, JudgeStatus.RUNTIME_ERROR),
isNull(schema.submission.contestId),
inArray(schema.submission.language, ["Python", "C", "C++"]),
inArray(schema.submission.language, ['Python', 'C', 'C++']),
sql`(${schema.submission.language} <> 'Python' or ${schema.submission.statisticInfo} ? 'runtime_error')`,
),
)
console.log(
`${rows.length} 条运行时错误要重新诊断${apply ? "" : "(只读预演,加 --apply 才写)"}`,
)
console.log(`${rows.length} 条运行时错误要重新诊断${apply ? '' : '(只读预演,加 --apply 才写)'}`)
const changes = new Map<string, number>()
let changed = 0
@@ -61,14 +59,10 @@ export async function rediagnose({ apply }: { apply: boolean }) {
const rawTemplate = templateForLanguage(row.problem.template, row.submission.language)
const template = rawTemplate ? parseProblemTemplate(rawTemplate) : null
const source = template
? `${template.prepend}\n${row.submission.code}\n${template.append}`
: row.submission.code
const prependLines = template ? template.prepend.split("\n").length : 0
const source = template ? `${template.prepend}\n${row.submission.code}\n${template.append}` : row.submission.code
const prependLines = template ? template.prepend.split('\n').length : 0
const next = failure
? await diagnoseRuntimeError(row, source, prependLines, failure).catch(() => null)
: null
const next = failure ? await diagnoseRuntimeError(row, source, prependLines, failure).catch(() => null) : null
const previous = asRecord(row.submission.statisticInfo).runtime_error
if (!next) {
failed++
@@ -76,8 +70,8 @@ export async function rediagnose({ apply }: { apply: boolean }) {
changed++
const label = (value: unknown) => {
const info = asRecord(value)
if (!value) return "(无)"
if (info.type) return `${String(info.type)}/${String(info.kind ?? "-")}`
if (!value) return '(无)'
if (info.type) return `${String(info.type)}/${String(info.kind ?? '-')}`
return `信号 ${String(info.signal ?? 0)} 退出码 ${String(info.exit_code ?? 0)}`
}
const key = `${row.submission.language} ${label(previous)} → ${label(next)}`
@@ -97,8 +91,6 @@ export async function rediagnose({ apply }: { apply: boolean }) {
for (const [key, count] of [...changes].sort((left, right) => right[1] - left[1])) {
console.log(`${String(count).padStart(5)} ${key}`)
}
console.log(
`变了 ${changed} 条${apply ? ",已写入" : ""};没变 ${rows.length - changed - failed} 条;诊断不出来、保留原样 ${failed} 条`,
)
console.log(`变了 ${changed} 条${apply ? ',已写入' : ''};没变 ${rows.length - changed - failed} 条;诊断不出来、保留原样 ${failed} 条`)
return 0
}
+23 -30
View File
@@ -1,24 +1,23 @@
import { eq, sql } from "drizzle-orm"
import { eq, sql } from 'drizzle-orm'
import { hashPassword } from "../auth/password"
import { db, schema } from "../db"
import { hashPassword } from '../auth/password'
import { db, schema } from '../db'
/**
* 本机开发用的账号。**只能对本地库跑** —— 它会重置账号密码并把明文写进
* raw_password,其中一个还是超管。对着生产库跑一次就是把超管密码改掉,
* 所以这里按 DATABASE_URL 的主机名拦一道,需要绕过时显式设 OJ2_SEED_FORCE=true。
*/
const url =
process.env.DATABASE_URL ?? "postgres://onlinejudge:onlinejudge@localhost:5433/onlinejudge"
const url = process.env.DATABASE_URL ?? 'postgres://onlinejudge:onlinejudge@localhost:5433/onlinejudge'
const host = (() => {
try {
return new URL(url).hostname
} catch {
return ""
return ''
}
})()
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", ""])
if (!LOCAL_HOSTS.has(host) && process.env.OJ2_SEED_FORCE !== "true") {
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', ''])
if (!LOCAL_HOSTS.has(host) && process.env.OJ2_SEED_FORCE !== 'true') {
console.error(
`[seed:dev] DATABASE_URL 指向 ${host},不是本机。这个脚本会重置账号密码` +
`(含一个超管)并写明文 raw_password,拒绝执行。\n` +
@@ -30,15 +29,13 @@ if (!LOCAL_HOSTS.has(host) && process.env.OJ2_SEED_FORCE !== "true") {
const now = new Date().toISOString()
// 阶段 1 是带着显式 id 导入的,PostgreSQL 的序列没跟着走,先对齐
await db.execute(
sql`select setval(pg_get_serial_sequence('"user"', 'id'), coalesce(max(${schema.user.id}), 1), true) from ${schema.user}`,
)
await db.execute(sql`select setval(pg_get_serial_sequence('"user"', 'id'), coalesce(max(${schema.user.id}), 1), true) from ${schema.user}`)
interface SeedAccount {
username: string
password: string
adminType: "Regular User" | "Super Admin"
problemPermission: "None" | "All"
adminType: 'Regular User' | 'Super Admin'
problemPermission: 'None' | 'All'
realName: string
}
@@ -79,17 +76,13 @@ async function seed(account: SeedAccount) {
if (!user) throw new Error(`Failed to seed ${account.username}`)
const [profile] = await db
.select({ id: schema.userProfile.id })
.from(schema.userProfile)
.where(eq(schema.userProfile.userId, user.id))
.limit(1)
const [profile] = await db.select({ id: schema.userProfile.id }).from(schema.userProfile).where(eq(schema.userProfile.userId, user.id)).limit(1)
if (!profile) {
await db.insert(schema.userProfile).values({
userId: user.id,
acmProblemsStatus: { problems: {}, contest_problems: {} },
avatar: "/public/avatar/default.png",
avatar: '/public/avatar/default.png',
realName: account.realName,
})
}
@@ -97,19 +90,19 @@ async function seed(account: SeedAccount) {
console.log(` ${account.adminType.padEnd(13)} ${user.username} / ${account.password}`)
}
console.log("Seeded development logins:")
console.log('Seeded development logins:')
await seed({
username: process.env.OJ2_DEV_USERNAME ?? "student",
password: process.env.OJ2_DEV_PASSWORD ?? "student123",
adminType: "Regular User",
problemPermission: "None",
realName: "开发用学生",
username: process.env.OJ2_DEV_USERNAME ?? 'student',
password: process.env.OJ2_DEV_PASSWORD ?? 'student123',
adminType: 'Regular User',
problemPermission: 'None',
realName: '开发用学生',
})
await seed({
username: process.env.OJ2_DEV_ADMIN_USERNAME ?? "devadmin",
password: process.env.OJ2_DEV_ADMIN_PASSWORD ?? "devadmin123",
adminType: "Super Admin",
problemPermission: "All",
realName: "开发用超管",
username: process.env.OJ2_DEV_ADMIN_USERNAME ?? 'devadmin',
password: process.env.OJ2_DEV_ADMIN_PASSWORD ?? 'devadmin123',
adminType: 'Super Admin',
problemPermission: 'All',
realName: '开发用超管',
})
process.exit(0)