agent-foundation/tools/generate-xuebang-get-catalog.mjs
2026-09-23 10:15:56 +08:00

104 lines
3.9 KiB
JavaScript

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).`);