import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { BACKEND_CAPABILITY_MANIFEST, type BackendCapabilityManifest, type BackendDomainModule, type BackendRouteCapability, type BackendRouteSurface, } from '../src/manifest/backendCapabilityManifest.js'; type GeneratedSummary = { surfaceCount: number; routeCount: number; moduleCount: number; publicRouteCount: number; jwtRouteCount: number; envSwitchRouteCount: number; streamRouteCount: number; }; type GeneratedArtifact = { generatedAt: string; manifestVersion: string; generatedCommand: string; outputTargets: BackendCapabilityManifest['outputTargets']; summary: GeneratedSummary; surfaces: Array< BackendRouteSurface & { routeCount: number; routeIds: string[]; } >; modules: Array< BackendDomainModule & { routeCount: number; routeIds: string[]; } >; routes: BackendRouteCapability[]; maintenanceRules: string[]; }; const currentFilePath = fileURLToPath(import.meta.url); const scriptDirectory = path.dirname(currentFilePath); const repoRoot = path.resolve(scriptDirectory, '..', '..'); /** * 统一把 repo 相对路径转成绝对路径,避免不同工作目录下解析不一致。 */ function resolveRepoPath(relativePath: string) { return path.resolve(repoRoot, relativePath); } function sortById(items: T[]) { return [...items].sort((left, right) => left.id.localeCompare(right.id, 'zh-Hans-CN')); } function sortRoutes(routes: BackendRouteCapability[]) { return [...routes].sort((left, right) => { if (left.path === right.path) { return left.method.localeCompare(right.method, 'en-US'); } return left.path.localeCompare(right.path, 'en-US'); }); } /** * 用最小约束校验 manifest 的唯一性与引用完整性,确保生成结果可维护。 */ function assertUniqueIds(items: Array<{ id: string }>, label: string) { const seen = new Set(); const duplicates: string[] = []; items.forEach((item) => { if (seen.has(item.id)) { duplicates.push(item.id); return; } seen.add(item.id); }); if (duplicates.length > 0) { throw new Error(`${label} 存在重复 id:${duplicates.join('、')}`); } } async function assertSourceFileContains(params: { sourceFile: string; sourceHint: string; routeId: string; }) { const absolutePath = resolveRepoPath(params.sourceFile); const content = await readFile(absolutePath, 'utf8'); if (!content.includes(params.sourceHint)) { throw new Error( `路由 ${params.routeId} 的 sourceHint 未命中源码:${params.sourceFile} -> ${params.sourceHint}`, ); } } async function validateModuleCoverage(modules: BackendDomainModule[]) { const modulesRoot = resolveRepoPath('server-node/src/modules'); const directoryEntries = await readdir(modulesRoot, { withFileTypes: true }); const actualDirectories = directoryEntries .filter((entry) => entry.isDirectory()) .map((entry) => `server-node/src/modules/${entry.name}`) .sort((left, right) => left.localeCompare(right, 'en-US')); const manifestDirectories = modules .map((moduleItem) => moduleItem.directory) .sort((left, right) => left.localeCompare(right, 'en-US')); const missingInManifest = actualDirectories.filter( (directory) => !manifestDirectories.includes(directory), ); const staleInManifest = manifestDirectories.filter( (directory) => !actualDirectories.includes(directory), ); if (missingInManifest.length > 0) { throw new Error( `以下模块目录尚未进入能力 manifest:${missingInManifest.join('、')}`, ); } if (staleInManifest.length > 0) { throw new Error( `manifest 中存在已失效的模块目录:${staleInManifest.join('、')}`, ); } } async function validateManifest(manifest: BackendCapabilityManifest) { assertUniqueIds(manifest.surfaces, '挂载面'); assertUniqueIds(manifest.modules, '内部模块'); assertUniqueIds(manifest.routes, '路由'); const surfaceIds = new Set(manifest.surfaces.map((surface) => surface.id)); const moduleIds = new Set(manifest.modules.map((moduleItem) => moduleItem.id)); for (const surface of manifest.surfaces) { for (const relatedModuleId of surface.relatedModuleIds) { if (!moduleIds.has(relatedModuleId)) { throw new Error( `挂载面 ${surface.id} 引用了未定义的模块:${relatedModuleId}`, ); } } for (const mount of surface.mounts) { const absoluteEntryPath = resolveRepoPath(mount.entryFile); const content = await readFile(absoluteEntryPath, 'utf8'); if (!content.includes(mount.routeFactory)) { throw new Error( `挂载面 ${surface.id} 的入口文件缺少工厂引用:${mount.entryFile} -> ${mount.routeFactory}`, ); } } } for (const moduleItem of manifest.modules) { for (const surfaceId of moduleItem.exposedBySurfaceIds) { if (!surfaceIds.has(surfaceId)) { throw new Error( `模块 ${moduleItem.id} 引用了未定义的挂载面:${surfaceId}`, ); } } const absoluteDirectory = resolveRepoPath(moduleItem.directory); const statsEntries = await readdir(absoluteDirectory); if (statsEntries.length === 0) { throw new Error(`模块目录为空,无法作为能力边界:${moduleItem.directory}`); } } for (const route of manifest.routes) { if (!surfaceIds.has(route.surfaceId)) { throw new Error(`路由 ${route.id} 引用了未定义的挂载面:${route.surfaceId}`); } for (const moduleId of route.domainModuleIds) { if (!moduleIds.has(moduleId)) { throw new Error(`路由 ${route.id} 引用了未定义的模块:${moduleId}`); } } await assertSourceFileContains({ sourceFile: route.sourceFile, sourceHint: route.sourceHint, routeId: route.id, }); } await validateModuleCoverage(manifest.modules); } function buildSummary(routes: BackendRouteCapability[]): GeneratedSummary { return { surfaceCount: BACKEND_CAPABILITY_MANIFEST.surfaces.length, routeCount: routes.length, moduleCount: BACKEND_CAPABILITY_MANIFEST.modules.length, publicRouteCount: routes.filter((route) => route.access === '公开').length, jwtRouteCount: routes.filter((route) => route.access === 'JWT').length, envSwitchRouteCount: routes.filter((route) => route.access.startsWith('开关:')).length, streamRouteCount: routes.filter((route) => route.responseMode === 'stream').length, }; } function buildArtifact(manifest: BackendCapabilityManifest): GeneratedArtifact { const routes = sortRoutes(manifest.routes); const summary = buildSummary(routes); const surfaces = sortById(manifest.surfaces).map((surface) => { const surfaceRoutes = routes.filter((route) => route.surfaceId === surface.id); return { ...surface, routeCount: surfaceRoutes.length, routeIds: surfaceRoutes.map((route) => route.id), }; }); const modules = sortById(manifest.modules).map((moduleItem) => { const moduleRoutes = routes.filter((route) => route.domainModuleIds.includes(moduleItem.id), ); return { ...moduleItem, routeCount: moduleRoutes.length, routeIds: moduleRoutes.map((route) => route.id), }; }); return { generatedAt: new Date().toISOString(), manifestVersion: manifest.version, generatedCommand: manifest.generatedCommand, outputTargets: manifest.outputTargets, summary, surfaces, modules, routes, maintenanceRules: manifest.maintenanceRules, }; } function renderMarkdown(artifact: GeneratedArtifact) { const lines: string[] = []; lines.push('# Node 后端模块与接口索引'); lines.push(''); lines.push('> 该文档由 `server-node/src/manifest/backendCapabilityManifest.ts` 自动生成。'); lines.push(`> 生成命令:\`${artifact.generatedCommand}\``); lines.push(`> 生成时间:\`${artifact.generatedAt}\``); lines.push(''); lines.push('## 总览'); lines.push(''); lines.push(`- 对外挂载面:${artifact.summary.surfaceCount} 个`); lines.push(`- 已登记路由:${artifact.summary.routeCount} 条`); lines.push(`- 内部模块目录:${artifact.summary.moduleCount} 个`); lines.push(`- 公开接口:${artifact.summary.publicRouteCount} 条`); lines.push(`- JWT 接口:${artifact.summary.jwtRouteCount} 条`); lines.push(`- 受环境开关控制的接口:${artifact.summary.envSwitchRouteCount} 条`); lines.push(`- 流式接口:${artifact.summary.streamRouteCount} 条`); lines.push(''); lines.push('## 产物'); lines.push(''); lines.push(`- JSON 清单:\`${artifact.outputTargets.json}\``); lines.push(`- Markdown 索引:\`${artifact.outputTargets.markdown}\``); lines.push(`- Manifest 源:\`server-node/src/manifest/backendCapabilityManifest.ts\``); lines.push(''); lines.push('## 对外挂载面'); lines.push(''); artifact.surfaces.forEach((surface) => { lines.push(`### ${surface.title}`); lines.push(''); lines.push(`- 标识:\`${surface.id}\``); lines.push(`- 路由数:${surface.routeCount}`); lines.push(`- 入口:${surface.mounts.map((mount) => `\`${mount.entryFile} -> ${mount.mountPath} -> ${mount.routeFactory}\``).join(';')}`); lines.push(`- 关联模块:${surface.relatedModuleIds.length > 0 ? surface.relatedModuleIds.map((moduleId) => `\`${moduleId}\``).join('、') : '无'}`); lines.push('- 责任:'); surface.responsibilities.forEach((item) => { lines.push(` - ${item}`); }); lines.push('- 主要服务边界:'); surface.primaryServiceBoundaries.forEach((item) => { lines.push(` - ${item}`); }); lines.push(''); }); lines.push('## 接口索引'); lines.push(''); lines.push('| 方法 | 路径 | 访问 | 响应 | 挂载面 | 内部模块 | 说明 |'); lines.push('| --- | --- | --- | --- | --- | --- | --- |'); artifact.routes.forEach((route) => { const moduleLabel = route.domainModuleIds.length > 0 ? route.domainModuleIds.map((moduleId) => `\`${moduleId}\``).join('、') : '无'; lines.push( `| ${route.method} | \`${route.path}\` | ${route.access} | ${route.responseMode} | \`${route.surfaceId}\` | ${moduleLabel} | ${route.summary} |`, ); }); lines.push(''); lines.push('## 内部模块边界'); lines.push(''); artifact.modules.forEach((moduleItem) => { lines.push(`### ${moduleItem.title}`); lines.push(''); lines.push(`- 标识:\`${moduleItem.id}\``); lines.push(`- 目录:\`${moduleItem.directory}\``); lines.push(`- 对外可见面:${moduleItem.exposedBySurfaceIds.map((surfaceId) => `\`${surfaceId}\``).join('、')}`); lines.push(`- 关联路由数:${moduleItem.routeCount}`); lines.push('- 职责:'); moduleItem.responsibilities.forEach((item) => { lines.push(` - ${item}`); }); lines.push('- 主要服务边界:'); moduleItem.primaryServiceBoundaries.forEach((item) => { lines.push(` - ${item}`); }); lines.push('- 关键文件:'); moduleItem.keyFiles.forEach((filePath) => { lines.push(` - \`${filePath}\``); }); lines.push(''); }); lines.push('## 维护规则'); lines.push(''); artifact.maintenanceRules.forEach((rule) => { lines.push(`- ${rule}`); }); lines.push(''); return `${lines.join('\n')}`; } async function writeArtifactFile(relativePath: string, content: string) { const absolutePath = resolveRepoPath(relativePath); await mkdir(path.dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, content, 'utf8'); } async function main() { await validateManifest(BACKEND_CAPABILITY_MANIFEST); const artifact = buildArtifact(BACKEND_CAPABILITY_MANIFEST); const jsonContent = `${JSON.stringify(artifact, null, 2)}\n`; const markdownContent = renderMarkdown(artifact); await writeArtifactFile(artifact.outputTargets.json, jsonContent); await writeArtifactFile(artifact.outputTargets.markdown, markdownContent); console.log( [ `backend capability artifacts generated`, `json=${artifact.outputTargets.json}`, `markdown=${artifact.outputTargets.markdown}`, `routes=${artifact.summary.routeCount}`, `modules=${artifact.summary.moduleCount}`, ].join(' | '), ); } void main().catch((error) => { console.error(error); process.exit(1); });