import { mkdir, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const swaggerUrl = process.env.XUEBANG_SWAGGER_URL ?? "http://api.xuebangsoft.net/v2/api-docs"; const scriptDirectory = dirname(fileURLToPath(import.meta.url)); const projectDirectory = resolve(scriptDirectory, ".."); const catalogPath = resolve( projectDirectory, "src/main/resources/xuebang-get-api-catalog.json", ); const referencePath = resolve( projectDirectory, "docs/xuebang-get-api-catalog.md", ); const blockedPaths = new Map([ ["/token/{clientId}", "AccessToken 由后端鉴权组件管理"], ["/api/user/findUserForLogin", "登录密码不得通过通用查询接口传输"], ["/api/wxapp/getStudentToken", "学生 Token 属于敏感凭证"], ]); const response = await fetch(swaggerUrl); if (!response.ok) { throw new Error(`Unable to download ${swaggerUrl}: HTTP ${response.status}`); } const swagger = await response.json(); const operations = []; for (const [path, pathItem] of Object.entries(swagger.paths ?? {})) { if (!pathItem.get) continue; const operation = pathItem.get; const parameters = (operation.parameters ?? []) .filter((parameter) => parameter.in !== "header") .map((parameter) => ({ name: parameter.name, in: parameter.in, required: Boolean(parameter.required), type: parameter.type ?? parameter.schema?.type ?? "string", format: parameter.format ?? parameter.schema?.format ?? null, description: parameter.description ?? "", enumValues: parameter.enum ?? parameter.schema?.enum ?? [], })); const blockedReason = blockedPaths.get(path) ?? null; operations.push({ operationId: operation.operationId, path, summary: operation.summary ?? operation.description ?? operation.operationId, tags: operation.tags ?? [], parameters, pageable: parameters.some((parameter) => parameter.name === "pageNo") && parameters.some((parameter) => parameter.name === "pageSize"), exposed: blockedReason === null, blockedReason, }); } operations.sort((left, right) => left.path.localeCompare(right.path, "zh-CN")); const ids = new Set(); for (const operation of operations) { if (!operation.operationId) throw new Error(`Missing operationId: ${operation.path}`); if (ids.has(operation.operationId)) throw new Error(`Duplicate operationId: ${operation.operationId}`); ids.add(operation.operationId); } const generatedAt = new Date().toISOString(); const catalog = { source: swaggerUrl, generatedAt, getCount: operations.length, exposedCount: operations.filter((operation) => operation.exposed).length, operations, }; const markdown = [ "# 学邦 GET 接口目录", "", `- 来源:${swaggerUrl}`, `- 生成时间:${generatedAt}`, `- GET 接口:${catalog.getCount}`, `- WorkBuddy 可查询:${catalog.exposedCount}`, "", "| operationId | 分类 | 接口说明 | 路径 | 必填参数 | 可分页 | 状态 |", "|---|---|---|---|---|---|---|", ...operations.map((operation) => { const required = operation.parameters .filter((parameter) => parameter.required) .map((parameter) => `\`${parameter.name}\``) .join("、") || "无"; return `| \`${operation.operationId}\` | ${operation.tags.join(" / ")} | ${operation.summary} | \`${operation.path}\` | ${required} | ${operation.pageable ? "是" : "否"} | ${operation.exposed ? "可查询" : `后端专用:${operation.blockedReason}`} |`; }), "", "完整参数定义以 `src/main/resources/xuebang-get-api-catalog.json` 为准。", "", ].join("\n"); await mkdir(dirname(catalogPath), { recursive: true }); await mkdir(dirname(referencePath), { recursive: true }); await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`, "utf8"); await writeFile(referencePath, markdown, "utf8"); console.log(`Generated ${operations.length} GET operations (${catalog.exposedCount} exposed).`);