Files
OJ2/docs/spikes/extract-endpoints.ts
yuetsh ae5eea7250 fix(阶段0): 端点提取器漏抓 5 个端点,补反向对账
提取器按文件名白名单扫 <app>/urls/{oj,admin}.py,漏掉两类真实挂载:
tutorial/urls/tutorial.py(文件名不在白名单)和 utils/urls.py(没有
urls/ 目录,被 statSync 的 catch 吞掉),共 5 个端点,其中 4 个前端在用。

改为以 OnlineJudge/oj/urls.py 为唯一入口解析 26 条 include,side 与路径
前缀直接取挂载前缀,app 取 Python 模块名首段。127(oj 77 / admin 50),
DEPRECATED 17,与 cat */urls/*.py utils/urls.py | grep -c "path(" 一致。

reconcile.ts 补上反向对账:前端调用了但后端查无此端点的路径会告警并写进
产出的 markdown,让同类系统性盲区不再依赖人眼评审。修完后 orphan 为 0。

另:生成日期改用本地时区(toISOString 是 UTC,本地 UTC+8 晚间会写成前一天);
补记盲点 3(src/utils/download.ts 的独立 axios 实例,提取器抓不到)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:08:53 -06:00

99 lines
4.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 { readFileSync } 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 }
// 唯一入口是 oj/urls.py —— 它才是 Django 真实的挂载表。
//
// 不要按文件名白名单oj.py / admin.py去猜 urls 文件:那样会漏掉两类真实存在的形态,
// 1. urls/ 目录里文件名不叫 oj/admin 的,如 tutorial/urls/tutorial.py3 个端点,前端在用)
// 2. 根本没有 urls/ 目录、直接是模块文件的,如 utils/urls.py2 个端点,其一前端在用)
// 历史上这两类共 5 个端点被静默漏掉,其中 4 个前端在用。
// 现在改为解析 oj/urls.py 的 include 清单,挂载前缀直接取自这里,不再从文件名反推 side。
const INCLUDE_RE = /path\(\s*["'](api\/(?:admin\/)?)["']\s*,\s*include\(\s*["']([\w.]+)["']\s*\)/g
type Mount = { prefix: string; module: string; file: string; app: string; side: "oj" | "admin" }
function findMounts(root: string): Mount[] {
const src = readFileSync(join(root, "oj", "urls.py"), "utf8")
const mounts: Mount[] = []
INCLUDE_RE.lastIndex = 0
let m: RegExpExecArray | null
while ((m = INCLUDE_RE.exec(src)) !== null) {
const prefix = m[1]
const mod = m[2]
// Python 模块名 → 文件路径。"tutorial.urls.tutorial" → tutorial/urls/tutorial.py
// "utils.urls" → utils/urls.py。app 一律取模块名首段,不依赖目录层数。
mounts.push({
prefix,
module: mod,
file: join(root, ...mod.split(".")) + ".py",
app: mod.split(".")[0],
side: prefix === "api/admin/" ? "admin" : "oj",
})
}
return mounts
}
// 必须跨行匹配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[] = []
const mounts = findMounts(ROOT)
for (const { file, app, side, prefix } of mounts) {
const src = readFileSync(file, "utf8")
// 按括号深度找出每个 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(`挂载点 ${mounts.length} 个(来自 oj/urls.py 的 include`)
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")