diff --git a/docs/specs/endpoint-inventory.md b/docs/specs/endpoint-inventory.md index f70b1fd..84772bf 100644 --- a/docs/specs/endpoint-inventory.md +++ b/docs/specs/endpoint-inventory.md @@ -1,7 +1,7 @@ # 端点清单(机器初判) -生成时间:2026-08-07 -合计 122 个端点 —— KEEP 100、CUT 16、REVIEW 6 +生成时间:2026-08-06 +合计 127 个端点 —— KEEP 104、CUT 17、REVIEW 6 > REVIEW 项需人工裁决,裁决后把本行的 REVIEW 改成 KEEP 或 CUT,并在末列写明理由。 @@ -9,28 +9,31 @@ > 已知盲点 2:`ojnext` 里有 4 处用原生 `fetch("/api/...")` 而非 `http.get/post(...)` 发起请求(AI 流式响应场景:`src/oj/store/ai.ts`、`src/oj/problem/components/SubmissionResult.vue`、`src/oj/rank/list.vue`、`src/oj/class/pk.vue`),提取脚本只认 `get/post/put/delete(...)` 调用形式,完全抓不到 `fetch(...)`。本轮 REVIEW 里的 `/api/ai/analysis`、`/api/ai/hint`、`/api/ai/class_pk`、`/api/ai/class_single` 经人工核实均属此类,实际都在用。 -> 上述两类盲点都是“前端有调用=否”但实际有调用,人工裁决时不要仅凭本表这一列就判 CUT;REVIEW 里唯一不属于此类的是 `/api/judge_server_heartbeat/`——它是判题机而非前端调用的接口,不受提取脚本盲点影响,是否保留需按后端间调用来判断。 +> 已知盲点 3:`ojnext/src/utils/download.ts` 是一个独立的 axios 实例(`baseURL: "/api/admin"`,与 `src/utils/http.ts` 那个共用实例无关),对外只暴露 `download(url)` 一个函数,内部走 `http.get(url)`。提取脚本既不认 `download(...)` 这种调用名,也抓不到内部那个变量 `url`,所以这条通道上的调用一律是假阴性。当前两个调用点(`src/admin/problem/components/Actions.vue:46`、`src/admin/problem/detail.vue:316`)都指向 `admin/test_case`,而该端点已因别处的字面量调用被判成 KEEP,**本轮结论不受影响**。但日后新增的 `download(...)` 调用会静默变成假阴性 CUT,裁决时留意。 + +> 盲点 1、2、3 都是“前端有调用=否”但实际有调用,人工裁决时不要仅凭本表这一列就判 CUT;REVIEW 里唯一不属于此类的是 `/api/judge_server_heartbeat/`——它是判题机而非前端调用的接口,不受提取脚本盲点影响,是否保留需按后端间调用来判断。 + +> 反向对账(前端调用了、后端却查无此端点):**0 条**,前端全部调用路径都能在后端端点全集里找到对应。 | 裁决 | app | 侧 | 路径 | 视图 | 前端有调用 | 已标 DEPRECATED | 理由 | |---|---|---|---|---|---|---|---| -| CUT | account | admin | `/api/admin/generate_user` | GenerateUserAPI.as_view | 否 | 是 | | | CUT | account | oj | `/api/change_password` | UserChangePasswordAPI.as_view | 否 | 是 | | | CUT | account | oj | `/api/change_email` | UserChangeEmailAPI.as_view | 否 | 是 | | | CUT | account | oj | `/api/check_username_or_email` | UsernameOrEmailCheck.as_view | 否 | 是 | | | CUT | account | oj | `/api/sessions` | SessionManagementAPI.as_view | 否 | 是 | | | CUT | account | oj | `/api/open_api_appkey` | OpenAPIAppkeyAPI.as_view | 否 | 是 | | | CUT | account | oj | `/api/sso` | SSOAPI.as_view | 否 | 是 | | +| CUT | account | admin | `/api/admin/generate_user` | GenerateUserAPI.as_view | 否 | 是 | | | CUT | conf | oj | `/api/languages` | LanguagesAPI.as_view | 否 | 是 | | +| CUT | contest | oj | `/api/contest/announcement` | ContestAnnouncementListAPI.as_view | 否 | 是 | | | CUT | contest | admin | `/api/admin/contest/announcement` | ContestAnnouncementAPI.as_view | 否 | 是 | | | CUT | contest | admin | `/api/admin/download_submissions` | DownloadContestSubmissions.as_view | 否 | 是 | | -| CUT | contest | oj | `/api/contest/announcement` | ContestAnnouncementListAPI.as_view | 否 | 是 | | -| CUT | problemset | admin | `/api/admin/problemset//sync` | ProblemSetSyncAPI.as_view | 否 | 是 | | | CUT | problemset | oj | `/api/problemset//problems/` | ProblemSetProblemAPI.as_view | 否 | 是 | | | CUT | problemset | oj | `/api/problemset//progress` | ProblemSetProgressAPI.as_view | 否 | 是 | | | CUT | problemset | oj | `/api/user/progress` | UserProgressAPI.as_view | 否 | 是 | | +| CUT | problemset | admin | `/api/admin/problemset//sync` | ProblemSetSyncAPI.as_view | 否 | 是 | | | CUT | submission | oj | `/api/submission_exists` | SubmissionExistsAPI.as_view | 否 | 是 | | -| KEEP | account | admin | `/api/admin/user` | UserAdminAPI.as_view | 是 | 否 | | -| KEEP | account | admin | `/api/admin/reset_password` | ResetUserPasswordAPI.as_view | 是 | 否 | | +| CUT | utils | admin | `/api/admin/upload_file` | SimditorFileUploadAPIView.as_view | 否 | 是 | | | KEEP | account | oj | `/api/login` | UserLoginAPI.as_view | 是 | 否 | | | KEEP | account | oj | `/api/logout` | UserLogoutAPI.as_view | 是 | 否 | | | KEEP | account | oj | `/api/register` | UserRegisterAPI.as_view | 是 | 否 | | @@ -41,45 +44,55 @@ | KEEP | account | oj | `/api/user_rank` | UserRankAPI.as_view | 是 | 否 | | | KEEP | account | oj | `/api/user_activity_rank` | UserActivityRankAPI.as_view | 是 | 否 | | | KEEP | account | oj | `/api/user_problem_rank` | UserProblemRankAPI.as_view | 是 | 否 | | -| KEEP | achievement | admin | `/api/admin/achievement` | AchievementAdminAPI.as_view | 是 | 否 | | -| KEEP | achievement | admin | `/api/admin/achievement/metrics` | AchievementMetricAdminAPI.as_view | 是 | 否 | | +| KEEP | account | admin | `/api/admin/user` | UserAdminAPI.as_view | 是 | 否 | | +| KEEP | account | admin | `/api/admin/reset_password` | ResetUserPasswordAPI.as_view | 是 | 否 | | | KEEP | achievement | oj | `/api/achievements` | AchievementListAPI.as_view | 是 | 否 | | | KEEP | achievement | oj | `/api/achievements/summary` | AchievementSummaryAPI.as_view | 是 | 否 | | | KEEP | achievement | oj | `/api/achievements/pending` | AchievementPendingAPI.as_view | 是 | 否 | | -| KEEP | ai | admin | `/api/admin/ai/reports` | AIAnalysisAdminAPI.as_view | 是 | 否 | | +| KEEP | achievement | admin | `/api/admin/achievement` | AchievementAdminAPI.as_view | 是 | 否 | | +| KEEP | achievement | admin | `/api/admin/achievement/metrics` | AchievementMetricAdminAPI.as_view | 是 | 否 | | | KEEP | ai | oj | `/api/ai/detail` | AIDetailDataAPI.as_view | 是 | 否 | | | KEEP | ai | oj | `/api/ai/duration` | AIDurationDataAPI.as_view | 是 | 否 | | | KEEP | ai | oj | `/api/ai/heatmap` | AIHeatmapDataAPI.as_view | 是 | 否 | | | KEEP | ai | oj | `/api/ai/login_summary` | AILoginSummaryAPI.as_view | 是 | 否 | | | KEEP | ai | oj | `/api/ai/pinned` | AIPinnedReportAPI.as_view | 是 | 否 | | -| KEEP | announcement | admin | `/api/admin/announcement` | AnnouncementAdminAPI.as_view | 是 | 否 | | +| KEEP | ai | admin | `/api/admin/ai/reports` | AIAnalysisAdminAPI.as_view | 是 | 否 | | | KEEP | announcement | oj | `/api/announcement` | AnnouncementAPI.as_view | 是 | 否 | | +| KEEP | announcement | admin | `/api/admin/announcement` | AnnouncementAdminAPI.as_view | 是 | 否 | | | KEEP | class_pk | oj | `/api/class_rank` | ClassRankAPI.as_view | 是 | 否 | | | KEEP | class_pk | oj | `/api/user_class_rank` | UserClassRankAPI.as_view | 是 | 否 | | | KEEP | class_pk | oj | `/api/class_pk` | ClassPKAPI.as_view | 是 | 否 | | +| KEEP | conf | oj | `/api/website` | WebsiteConfigAPI.as_view | 是 | 否 | | +| KEEP | conf | oj | `/api/hitokoto` | HitokotoAPI.as_view | 是 | 否 | | +| KEEP | conf | oj | `/api/class_usernames` | ClassUsernamesAPI.as_view | 是 | 否 | | | KEEP | conf | admin | `/api/admin/website` | WebsiteConfigAPI.as_view | 是 | 否 | | | KEEP | conf | admin | `/api/admin/random_user` | RandomUsernameAPI.as_view | 是 | 否 | | | KEEP | conf | admin | `/api/admin/judge_server` | JudgeServerAPI.as_view | 是 | 否 | | | KEEP | conf | admin | `/api/admin/prune_test_case` | TestCasePruneAPI.as_view | 是 | 否 | | | KEEP | conf | admin | `/api/admin/dashboard_info` | DashboardInfoAPI.as_view | 是 | 否 | | -| KEEP | conf | oj | `/api/website` | WebsiteConfigAPI.as_view | 是 | 否 | | -| KEEP | conf | oj | `/api/hitokoto` | HitokotoAPI.as_view | 是 | 否 | | -| KEEP | conf | oj | `/api/class_usernames` | ClassUsernamesAPI.as_view | 是 | 否 | | -| KEEP | contest | admin | `/api/admin/contest` | ContestAPI.as_view | 是 | 否 | | -| KEEP | contest | admin | `/api/admin/contest/clone` | ContestCloneAPI.as_view | 是 | 否 | | -| KEEP | contest | admin | `/api/admin/contest/acm_helper` | ACMContestHelper.as_view | 是 | 否 | | | KEEP | contest | oj | `/api/contests` | ContestListAPI.as_view | 是 | 否 | | | KEEP | contest | oj | `/api/contest` | ContestAPI.as_view | 是 | 否 | | | KEEP | contest | oj | `/api/contest/password` | ContestPasswordVerifyAPI.as_view | 是 | 否 | | | KEEP | contest | oj | `/api/contest/access` | ContestAccessAPI.as_view | 是 | 否 | | | KEEP | contest | oj | `/api/contest_rank` | ContestRankAPI.as_view | 是 | 否 | | -| KEEP | flowchart | admin | `/api/admin/flowchart/statistics` | FlowchartStatisticsAPI.as_view | 是 | 否 | | +| KEEP | contest | admin | `/api/admin/contest` | ContestAPI.as_view | 是 | 否 | | +| KEEP | contest | admin | `/api/admin/contest/clone` | ContestCloneAPI.as_view | 是 | 否 | | +| KEEP | contest | admin | `/api/admin/contest/acm_helper` | ACMContestHelper.as_view | 是 | 否 | | | KEEP | flowchart | oj | `/api/flowchart/submission` | FlowchartSubmissionAPI.as_view | 是 | 否 | | | KEEP | flowchart | oj | `/api/flowchart/submissions` | FlowchartSubmissionListAPI.as_view | 是 | 否 | | | KEEP | flowchart | oj | `/api/flowchart/submission/retry` | FlowchartSubmissionRetryAPI.as_view | 是 | 否 | | | KEEP | flowchart | oj | `/api/flowchart/submission/detail` | FlowchartSubmissionDetailAPI.as_view | 是 | 否 | | | KEEP | flowchart | oj | `/api/flowchart/submission/current` | FlowchartSubmissionCurrentAPI.as_view | 是 | 否 | | +| KEEP | flowchart | admin | `/api/admin/flowchart/statistics` | FlowchartStatisticsAPI.as_view | 是 | 否 | | | KEEP | message | oj | `/api/message` | MessageAPI.as_view | 是 | 否 | | +| KEEP | problem | oj | `/api/problem/tags` | ProblemTagAPI.as_view | 是 | 否 | | +| KEEP | problem | oj | `/api/problem` | ProblemAPI.as_view | 是 | 否 | | +| KEEP | problem | oj | `/api/problem/beat_count` | ProblemSolvedPeopleCount.as_view | 是 | 否 | | +| KEEP | problem | oj | `/api/problem/similar` | SimilarProblemAPI.as_view | 是 | 否 | | +| KEEP | problem | oj | `/api/problem/author` | ProblemAuthorAPI.as_view | 是 | 否 | | +| KEEP | problem | oj | `/api/problem/yearly_ac` | ProblemYearlyACRateAPI.as_view | 是 | 否 | | +| KEEP | problem | oj | `/api/pickone` | PickOneAPI.as_view | 是 | 否 | | +| KEEP | problem | oj | `/api/contest/problem` | ContestProblemAPI.as_view | 是 | 否 | | | KEEP | problem | admin | `/api/admin/test_case` | TestCaseAPI.as_view | 是 | 否 | | | KEEP | problem | admin | `/api/admin/sql_test_case_preview` | SQLTestCasePreviewAPI.as_view | 是 | 否 | | | KEEP | problem | admin | `/api/admin/sql_test_case_scripts` | SQLTestCaseScriptsAPI.as_view | 是 | 否 | | @@ -94,14 +107,13 @@ | KEEP | problem | admin | `/api/admin/contest/problem` | ContestProblemAPI.as_view | 是 | 否 | | | KEEP | problem | admin | `/api/admin/contest_problem/make_public` | MakeContestProblemPublicAPIView.as_view | 是 | 否 | | | KEEP | problem | admin | `/api/admin/contest/add_problem_from_public` | AddContestProblemAPI.as_view | 是 | 否 | | -| KEEP | problem | oj | `/api/problem/tags` | ProblemTagAPI.as_view | 是 | 否 | | -| KEEP | problem | oj | `/api/problem` | ProblemAPI.as_view | 是 | 否 | | -| KEEP | problem | oj | `/api/problem/beat_count` | ProblemSolvedPeopleCount.as_view | 是 | 否 | | -| KEEP | problem | oj | `/api/problem/similar` | SimilarProblemAPI.as_view | 是 | 否 | | -| KEEP | problem | oj | `/api/problem/author` | ProblemAuthorAPI.as_view | 是 | 否 | | -| KEEP | problem | oj | `/api/problem/yearly_ac` | ProblemYearlyACRateAPI.as_view | 是 | 否 | | -| KEEP | problem | oj | `/api/pickone` | PickOneAPI.as_view | 是 | 否 | | -| KEEP | problem | oj | `/api/contest/problem` | ContestProblemAPI.as_view | 是 | 否 | | +| KEEP | problemset | oj | `/api/problemset` | ProblemSetAPI.as_view | 是 | 否 | | +| KEEP | problemset | oj | `/api/problemset/` | ProblemSetDetailAPI.as_view | 是 | 否 | | +| KEEP | problemset | oj | `/api/problemset//problems` | ProblemSetProblemAPI.as_view | 是 | 否 | | +| KEEP | problemset | oj | `/api/problemset/progress` | ProblemSetProgressAPI.as_view | 是 | 否 | | +| KEEP | problemset | oj | `/api/user/badges` | UserBadgeAPI.as_view | 是 | 否 | | +| KEEP | problemset | oj | `/api/problemset//badges` | ProblemSetBadgeAPI.as_view | 是 | 否 | | +| KEEP | problemset | oj | `/api/problemset//users_progress` | ProblemSetUserProgressAPI.as_view | 是 | 否 | | | KEEP | problemset | admin | `/api/admin/problemset` | ProblemSetAdminAPI.as_view | 是 | 否 | | | KEEP | problemset | admin | `/api/admin/problemset/visible` | ProblemSetVisibleAPI.as_view | 是 | 否 | | | KEEP | problemset | admin | `/api/admin/problemset/status` | ProblemSetStatusAPI.as_view | 是 | 否 | | @@ -112,23 +124,20 @@ | KEEP | problemset | admin | `/api/admin/problemset//badges/` | ProblemSetBadgeAdminAPI.as_view | 是 | 否 | | | KEEP | problemset | admin | `/api/admin/problemset//progress` | ProblemSetProgressAdminAPI.as_view | 是 | 否 | | | KEEP | problemset | admin | `/api/admin/problemset//progress/` | ProblemSetProgressAdminAPI.as_view | 是 | 否 | | -| KEEP | problemset | oj | `/api/problemset` | ProblemSetAPI.as_view | 是 | 否 | | -| KEEP | problemset | oj | `/api/problemset/` | ProblemSetDetailAPI.as_view | 是 | 否 | | -| KEEP | problemset | oj | `/api/problemset//problems` | ProblemSetProblemAPI.as_view | 是 | 否 | | -| KEEP | problemset | oj | `/api/problemset/progress` | ProblemSetProgressAPI.as_view | 是 | 否 | | -| KEEP | problemset | oj | `/api/user/badges` | UserBadgeAPI.as_view | 是 | 否 | | -| KEEP | problemset | oj | `/api/problemset//badges` | ProblemSetBadgeAPI.as_view | 是 | 否 | | -| KEEP | problemset | oj | `/api/problemset//users_progress` | ProblemSetUserProgressAPI.as_view | 是 | 否 | | | KEEP | reaction | oj | `/api/reaction` | ReactionAPI.as_view | 是 | 否 | | -| KEEP | submission | admin | `/api/admin/submission/rejudge` | SubmissionRejudgeAPI.as_view | 是 | 否 | | -| KEEP | submission | admin | `/api/admin/submission/statistics` | SubmissionStatisticsAPI.as_view | 是 | 否 | | | KEEP | submission | oj | `/api/submission` | SubmissionAPI.as_view | 是 | 否 | | | KEEP | submission | oj | `/api/submissions` | SubmissionListAPI.as_view | 是 | 否 | | | KEEP | submission | oj | `/api/submissions/today_count` | SubmissionsTodayCount.as_view | 是 | 否 | | | KEEP | submission | oj | `/api/format_code` | FormatCodeAPI.as_view | 是 | 否 | | +| KEEP | submission | admin | `/api/admin/submission/rejudge` | SubmissionRejudgeAPI.as_view | 是 | 否 | | +| KEEP | submission | admin | `/api/admin/submission/statistics` | SubmissionStatisticsAPI.as_view | 是 | 否 | | +| KEEP | tutorial | oj | `/api/tutorial` | TutorialAPI.as_view | 是 | 否 | | +| KEEP | tutorial | oj | `/api/tutorials` | TutorialTitlesAPI.as_view | 是 | 否 | | +| KEEP | tutorial | oj | `/api/exercises` | ExerciseAPI.as_view | 是 | 否 | | | KEEP | tutorial | admin | `/api/admin/tutorial` | TutorialAdminAPI.as_view | 是 | 否 | | | KEEP | tutorial | admin | `/api/admin/tutorial/visibility` | TutorialVisibilityAPI.as_view | 是 | 否 | | | KEEP | tutorial | admin | `/api/admin/exercise` | ExerciseAdminAPI.as_view | 是 | 否 | | +| KEEP | utils | admin | `/api/admin/upload_image` | SimditorImageUploadAPIView.as_view | 是 | 否 | | | REVIEW | ai | oj | `/api/ai/analysis` | AIAnalysisAPI.as_view | 否 | 否 | | | REVIEW | ai | oj | `/api/ai/hint` | AIHintAPI.as_view | 否 | 否 | | | REVIEW | ai | oj | `/api/ai/class_pk` | ClassPKAnalysisAPI.as_view | 否 | 否 | | diff --git a/docs/spikes/extract-endpoints.ts b/docs/spikes/extract-endpoints.ts index c3955b0..68fc67c 100644 --- a/docs/spikes/extract-endpoints.ts +++ b/docs/spikes/extract-endpoints.ts @@ -1,26 +1,42 @@ #!/usr/bin/env bun // 从 Django urls/*.py 提取后端端点全集 -import { readdirSync, readFileSync, statSync } from "node:fs" +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 } -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)) - } +// 唯一入口是 oj/urls.py —— 它才是 Django 真实的挂载表。 +// +// 不要按文件名白名单(oj.py / admin.py)去猜 urls 文件:那样会漏掉两类真实存在的形态, +// 1. urls/ 目录里文件名不叫 oj/admin 的,如 tutorial/urls/tutorial.py(3 个端点,前端在用) +// 2. 根本没有 urls/ 目录、直接是模块文件的,如 utils/urls.py(2 个端点,其一前端在用) +// 历史上这两类共 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 out.sort() + return mounts } // 必须跨行匹配:18 处 path( 的参数换行写,按行扫会漏 @@ -33,12 +49,9 @@ 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 mounts = findMounts(ROOT) +for (const { file, app, side, prefix } of mounts) { const src = readFileSync(file, "utf8") - const prefix = side === "oj" ? "/api/" : "/api/admin/" // 按括号深度找出每个 path(...) 的完整片段,再连同该行剩余部分(行尾注释可能带 // DEPRECATED 标记)一起分析。单纯用正则切片会在 .as_view() 的右括号处截断。 @@ -68,7 +81,7 @@ for (const file of findUrlFiles(ROOT)) { endpoints.push({ app, side, - pattern: prefix + m[1], + pattern: "/" + prefix + m[1], view: m[2], name: chunk.match(NAME_RE)?.[1] ?? "", deprecated: /DEPRECATED/.test(chunk), @@ -78,6 +91,7 @@ for (const file of findUrlFiles(ROOT)) { 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)) diff --git a/docs/spikes/reconcile.ts b/docs/spikes/reconcile.ts index 09ab0ca..8a82259 100644 --- a/docs/spikes/reconcile.ts +++ b/docs/spikes/reconcile.ts @@ -40,10 +40,23 @@ const rows = backend.map((e) => { const count = (v: Verdict) => rows.filter((r) => r.verdict === v).length console.log(`KEEP ${count("KEEP")} / CUT ${count("CUT")} / REVIEW ${count("REVIEW")} 合计 ${rows.length}`) +// 反向对账:前端调用了、后端却查无此端点。 +// 只查"后端端点有没有被前端调用"是单向的,提取器漏抓整个 urls 文件时这一侧毫无反应 +// —— 历史上 tutorial/urls/tutorial.py 与 utils/urls.py 共 5 个端点被漏掉,靠人眼评审才发现。 +// 加上这条反向差集,同类系统性盲区会由脚本自己报警。 +// 残留的 orphan 不一定是 bug:前端可能确实留着调用死路径的代码,那本身就是 Task 4 要清的信息。 +const beKeys = new Set(rows.map((r) => r.key)) +const orphans = [...new Set(frontend.map((c) => key(c.path)))].filter((k) => !beKeys.has(k)).sort() +if (orphans.length) console.warn(`⚠ 前端调用无对应后端端点 ${orphans.length} 条:`, orphans) + +// 生成日期用本地时区。toISOString() 是 UTC,本地 UTC+8 晚间生成会写成前一天, +// 而这份产物要靠生成时间判断是否需要重跑。 +const today = new Date().toLocaleDateString("sv-SE") + const md = [ "# 端点清单(机器初判)", "", - `生成时间:${new Date().toISOString().slice(0, 10)}`, + `生成时间:${today}`, `合计 ${rows.length} 个端点 —— KEEP ${count("KEEP")}、CUT ${count("CUT")}、REVIEW ${count("REVIEW")}`, "", "> REVIEW 项需人工裁决,裁决后把本行的 REVIEW 改成 KEEP 或 CUT,并在末列写明理由。", @@ -52,7 +65,13 @@ const md = [ "", "> 已知盲点 2:`ojnext` 里有 4 处用原生 `fetch(\"/api/...\")` 而非 `http.get/post(...)` 发起请求(AI 流式响应场景:`src/oj/store/ai.ts`、`src/oj/problem/components/SubmissionResult.vue`、`src/oj/rank/list.vue`、`src/oj/class/pk.vue`),提取脚本只认 `get/post/put/delete(...)` 调用形式,完全抓不到 `fetch(...)`。本轮 REVIEW 里的 `/api/ai/analysis`、`/api/ai/hint`、`/api/ai/class_pk`、`/api/ai/class_single` 经人工核实均属此类,实际都在用。", "", - "> 上述两类盲点都是“前端有调用=否”但实际有调用,人工裁决时不要仅凭本表这一列就判 CUT;REVIEW 里唯一不属于此类的是 `/api/judge_server_heartbeat/`——它是判题机而非前端调用的接口,不受提取脚本盲点影响,是否保留需按后端间调用来判断。", + "> 已知盲点 3:`ojnext/src/utils/download.ts` 是一个独立的 axios 实例(`baseURL: \"/api/admin\"`,与 `src/utils/http.ts` 那个共用实例无关),对外只暴露 `download(url)` 一个函数,内部走 `http.get(url)`。提取脚本既不认 `download(...)` 这种调用名,也抓不到内部那个变量 `url`,所以这条通道上的调用一律是假阴性。当前两个调用点(`src/admin/problem/components/Actions.vue:46`、`src/admin/problem/detail.vue:316`)都指向 `admin/test_case`,而该端点已因别处的字面量调用被判成 KEEP,**本轮结论不受影响**。但日后新增的 `download(...)` 调用会静默变成假阴性 CUT,裁决时留意。", + "", + "> 盲点 1、2、3 都是“前端有调用=否”但实际有调用,人工裁决时不要仅凭本表这一列就判 CUT;REVIEW 里唯一不属于此类的是 `/api/judge_server_heartbeat/`——它是判题机而非前端调用的接口,不受提取脚本盲点影响,是否保留需按后端间调用来判断。", + "", + `> 反向对账(前端调用了、后端却查无此端点):${ + orphans.length === 0 ? "**0 条**,前端全部调用路径都能在后端端点全集里找到对应。" : `**${orphans.length} 条**,见下。这类路径要么是提取器又漏了某个 urls 文件,要么是前端留着调用死路径的代码,两种都要查。\n>\n${orphans.map((o) => `> - \`${o}\``).join("\n")}` + }`, "", "| 裁决 | app | 侧 | 路径 | 视图 | 前端有调用 | 已标 DEPRECATED | 理由 |", "|---|---|---|---|---|---|---|---|",