Files
OJ2/docs/spikes/extract-endpoints.ts
yuetsh 3da7482741 docs: 阶段 0 实施计划 + 端点提取器
计划覆盖端点盘点、人工裁决、jieba 验证、取回生产库 schema 四件事。
相对设计文档有两处偏离,已在计划内说明:阶段 0 不删旧代码只产出清单
(与"旧仓库全程冻结"一致);不写测试(遵循项目既定策略)。

盘点中发现两处事实,已修正设计文档:
- /api/sessions 前端从不调用,User.session_keys 只写不读,选 opaque
  token 而非 JWT 的原始理由不成立,理由已换成账号封禁需即时生效
- SessionRecordMiddleware 每个已登录请求都写库,新后端不要复刻

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:24:41 -06:00

85 lines
3.0 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.
#!/usr/bin/env bun
// 从 Django urls/*.py 提取后端端点全集
import { readdirSync, readFileSync, statSync } from "node:fs"
import { join } from "node:path"
const ROOT = process.argv[2] ?? "../../OnlineJudge"
type Endpoint = { app: string; side: "oj" | "admin"; pattern: string; view: string; name: string; deprecated: boolean }
function findUrlFiles(root: string): string[] {
const out: string[] = []
for (const app of readdirSync(root)) {
const dir = join(root, app, "urls")
try {
if (!statSync(dir).isDirectory()) continue
} catch {
continue
}
for (const f of readdirSync(dir)) {
if (f === "oj.py" || f === "admin.py") out.push(join(dir, f))
}
}
return out.sort()
}
// 必须跨行匹配18 处 path( 的参数换行写,按行扫会漏
// path("problem/", ProblemAPI.as_view(), name="problem_api"),
// path(
// "problemset/visible",
// ProblemsetVisibleAPI.as_view(),
// ),
const PATH_RE = /path\(\s*r?["']([^"']*)["']\s*,\s*([A-Za-z_][\w.]*)/g
const NAME_RE = /name\s*=\s*["']([^"']+)["']/
const endpoints: Endpoint[] = []
for (const file of findUrlFiles(ROOT)) {
const parts = file.split("/")
const app = parts[parts.length - 3]
const side = parts[parts.length - 1] === "oj.py" ? "oj" : "admin"
const src = readFileSync(file, "utf8")
const prefix = side === "oj" ? "/api/" : "/api/admin/"
// 按括号深度找出每个 path(...) 的完整片段,再连同该行剩余部分(行尾注释可能带
// DEPRECATED 标记)一起分析。单纯用正则切片会在 .as_view() 的右括号处截断。
for (let i = src.indexOf("path("); i !== -1; i = src.indexOf("path(", i + 1)) {
if (i > 0 && /[\w.]/.test(src[i - 1])) continue // 跳过 re_path( 等
let depth = 0
let end = i
for (let j = i + 4; j < src.length; j++) {
if (src[j] === "(") depth++
else if (src[j] === ")" && --depth === 0) {
end = j + 1
break
}
}
const lineEnd = src.indexOf("\n", end)
const chunk = src.slice(i, lineEnd === -1 ? end : lineEnd)
// 注释可能出现在 path( 之后、字符串之后、逗号之后的任意位置,先整体剥掉再匹配;
// DEPRECATED 的判定仍用未剥离的 chunk。
const stripped = chunk.replace(/#[^\n]*/g, "")
PATH_RE.lastIndex = 0
const m = PATH_RE.exec(stripped)
if (!m) {
console.warn(` ⚠ 无法解析: ${chunk.split("\n")[0].trim()}`)
continue
}
endpoints.push({
app,
side,
pattern: prefix + m[1],
view: m[2],
name: chunk.match(NAME_RE)?.[1] ?? "",
deprecated: /DEPRECATED/.test(chunk),
})
}
}
const oj = endpoints.filter((e) => e.side === "oj").length
const dep = endpoints.filter((e) => e.deprecated).length
console.log(`后端端点合计 ${endpoints.length} (oj ${oj} / admin ${endpoints.length - oj})`)
console.log(`其中已标 DEPRECATED: ${dep}`)
await Bun.write("endpoints-backend.json", JSON.stringify(endpoints, null, 2))
console.log("→ endpoints-backend.json")