diff --git a/integration-docs/scripts/upload-openapi.mjs b/integration-docs/scripts/upload-openapi.mjs index d0b8116..ca629b8 100644 --- a/integration-docs/scripts/upload-openapi.mjs +++ b/integration-docs/scripts/upload-openapi.mjs @@ -7,6 +7,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const packageRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(packageRoot, '..'); const defaultApiReferenceDir = path.join(repoRoot, 'api-reference'); +const docsBaseUrl = 'https://docs.flashcat.cloud'; +const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']; export const requiredEnv = [ 'CDN_ACCESS_KEY', @@ -46,6 +48,36 @@ export function validateJsonFile(filePath) { JSON.parse(fs.readFileSync(filePath, 'utf8')); } +export function buildOpenapiReferenceIndex(openapi) { + const result = {}; + + for (const [apiPath, pathItem] of Object.entries(openapi.paths ?? {})) { + const operation = httpMethods + .map((method) => pathItem?.[method]) + .find(Boolean); + const href = operation?.['x-mint']?.href; + const label = operation?.summary || operation?.['x-mint']?.metadata?.sidebarTitle || operation?.operationId; + + if (href && label) { + result[apiPath] = { + label, + url: new URL(href, docsBaseUrl).toString() + }; + } + } + + return result; +} + +function buildJsonUploadOptions() { + return { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'public, max-age=300' + } + }; +} + async function createOssClient(env = process.env) { const { default: OSS } = await import('ali-oss'); return new OSS({ @@ -77,6 +109,20 @@ async function refreshCdnCache(cdnRuntime, url) { console.log(`Refreshed CDN cache: ${url}`); } +async function uploadJsonAsset({ + env, + ossClient, + cdnRuntime, + ossFilePath, + payload, + label +}) { + const result = await ossClient.put(ossFilePath, payload, buildJsonUploadOptions()); + const cdnUrl = buildCdnUrl(result.url, env.CDN_ENDPOINT, env.CDN_URL); + console.log(`Uploaded ${label} -> ${cdnUrl}`); + await refreshCdnCache(cdnRuntime, cdnUrl); +} + export async function uploadOpenapiJsonFiles({ apiReferenceDir = defaultApiReferenceDir, env = process.env, @@ -103,18 +149,32 @@ export async function uploadOpenapiJsonFiles({ for (const file of files) { const localFilePath = path.join(apiReferenceDir, file); const ossFilePath = buildOssFilePath(env.CDN_DIR, file); - const result = await resolvedOssClient.put(ossFilePath, localFilePath, { - headers: { - 'Content-Type': 'application/json; charset=utf-8', - 'Cache-Control': 'public, max-age=300' - } + const uploadPayload = Buffer.from(`${JSON.stringify( + buildOpenapiReferenceIndex(JSON.parse(fs.readFileSync(localFilePath, 'utf8'))), + null, + 2 + )}\n`); + await uploadJsonAsset({ + env, + ossClient: resolvedOssClient, + cdnRuntime: resolvedCdnRuntime, + ossFilePath, + payload: uploadPayload, + label: file }); - const cdnUrl = buildCdnUrl(result.url, env.CDN_ENDPOINT, env.CDN_URL); - console.log(`Uploaded ${file} -> ${cdnUrl}`); - await refreshCdnCache(resolvedCdnRuntime, cdnUrl); } - console.log(`Uploaded ${files.length} OpenAPI JSON files from ${apiReferenceDir}`); + const manifestPayload = Buffer.from(`${JSON.stringify({ files }, null, 2)}\n`); + await uploadJsonAsset({ + env, + ossClient: resolvedOssClient, + cdnRuntime: resolvedCdnRuntime, + ossFilePath: buildOssFilePath(env.CDN_DIR, 'manifest.json'), + payload: manifestPayload, + label: 'manifest.json' + }); + + console.log(`Uploaded ${files.length} OpenAPI JSON files and manifest from ${apiReferenceDir}`); } async function loadDotenvIfAvailable() { diff --git a/integration-docs/scripts/upload-openapi.test.mjs b/integration-docs/scripts/upload-openapi.test.mjs index 2018fcd..0e4fc56 100644 --- a/integration-docs/scripts/upload-openapi.test.mjs +++ b/integration-docs/scripts/upload-openapi.test.mjs @@ -5,6 +5,7 @@ import path from 'node:path'; import test from 'node:test'; import { + buildOpenapiReferenceIndex, buildOssFilePath, listOpenapiJsonFiles, uploadOpenapiJsonFiles, @@ -50,12 +51,71 @@ test('validateRequiredEnv reports every missing upload credential', () => { ]); }); -test('uploadOpenapiJsonFiles uploads every JSON file and refreshes each CDN URL', async () => { +test('buildOpenapiReferenceIndex keeps only path label and docs URL', () => { + assert.deepEqual(buildOpenapiReferenceIndex({ + paths: { + '/alert/list': { + post: { + summary: '查询告警列表', + 'x-mint': { + href: '/zh/api-reference/on-call/alerts/alert-read-list' + } + } + }, + '/alert/ignored': { + post: { + summary: '缺少文档链接' + } + } + } + }), { + '/alert/list': { + label: '查询告警列表', + url: 'https://docs.flashcat.cloud/zh/api-reference/on-call/alerts/alert-read-list' + } + }); +}); + +test('uploadOpenapiJsonFiles uploads every JSON file, a manifest, and refreshes each CDN URL', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-upload-')); const apiReferenceDir = path.join(tempDir, 'api-reference'); fs.mkdirSync(apiReferenceDir); - fs.writeFileSync(path.join(apiReferenceDir, 'openapi.en.json'), '{"openapi":"3.1.0"}\n'); - fs.writeFileSync(path.join(apiReferenceDir, 'openapi.zh.json'), '{"openapi":"3.1.0"}\n'); + fs.writeFileSync(path.join(apiReferenceDir, 'openapi.en.json'), JSON.stringify({ + openapi: '3.1.0', + paths: { + '/alert/list': { + post: { + summary: 'List alerts', + 'x-mint': { + href: '/en/api-reference/on-call/alerts/alert-read-list' + }, + responses: { + 200: { + description: 'Success' + } + } + } + } + } + })); + fs.writeFileSync(path.join(apiReferenceDir, 'openapi.zh.json'), JSON.stringify({ + openapi: '3.1.0', + paths: { + '/alert/list': { + post: { + summary: '查询告警列表', + 'x-mint': { + href: '/zh/api-reference/on-call/alerts/alert-read-list' + }, + responses: { + 200: { + description: '成功' + } + } + } + } + } + })); fs.writeFileSync(path.join(apiReferenceDir, 'ignored.txt'), 'not json\n'); const uploaded = []; @@ -92,15 +152,33 @@ test('uploadOpenapiJsonFiles uploads every JSON file and refreshes each CDN URL' uploaded.map((item) => item.ossFilePath), [ '/docs/api-reference/openapi.en.json', - '/docs/api-reference/openapi.zh.json' + '/docs/api-reference/openapi.zh.json', + '/docs/api-reference/manifest.json' ] ); assert.deepEqual( refreshed, [ 'https://docs-cdn.flashcat.cloud/docs/api-reference/openapi.en.json', - 'https://docs-cdn.flashcat.cloud/docs/api-reference/openapi.zh.json' + 'https://docs-cdn.flashcat.cloud/docs/api-reference/openapi.zh.json', + 'https://docs-cdn.flashcat.cloud/docs/api-reference/manifest.json' ] ); assert.equal(uploaded[0].options.headers['Content-Type'], 'application/json; charset=utf-8'); + const zhUpload = uploaded.find((item) => item.ossFilePath.endsWith('/openapi.zh.json')); + assert.ok(zhUpload); + assert.deepEqual(JSON.parse(zhUpload.localFilePath.toString('utf8')), { + '/alert/list': { + label: '查询告警列表', + url: 'https://docs.flashcat.cloud/zh/api-reference/on-call/alerts/alert-read-list' + } + }); + const manifestUpload = uploaded.find((item) => item.ossFilePath.endsWith('/manifest.json')); + assert.ok(manifestUpload); + assert.deepEqual(JSON.parse(manifestUpload.localFilePath.toString('utf8')), { + files: [ + 'openapi.en.json', + 'openapi.zh.json' + ] + }); });