This commit is contained in:
时瑾
2026-02-07 13:32:58 +08:00
3 changed files with 623 additions and 96 deletions

View File

@@ -17,7 +17,7 @@ export const OB11UserSchema = Type.Object({
login_days: Type.Optional(Type.Number({ description: '登录天数' })),
categoryName: Type.Optional(Type.String({ description: '分组名称' })),
categoryId: Type.Optional(Type.Number({ description: '分组ID' })),
}, { description: 'OneBot 11 用户信息' });
}, { $id: 'OB11User', description: 'OneBot 11 用户信息' });
export const OB11GroupSchema = Type.Object({
group_all_shut: Type.Number({ description: '是否全员禁言' }),
@@ -26,7 +26,7 @@ export const OB11GroupSchema = Type.Object({
group_name: Type.String({ description: '群名称' }),
member_count: Type.Optional(Type.Number({ description: '成员人数' })),
max_member_count: Type.Optional(Type.Number({ description: '最大成员人数' })),
}, { description: 'OneBot 11 群信息' });
}, { $id: 'OB11Group', description: 'OneBot 11 群信息' });
export const OB11GroupMemberSchema = Type.Object({
group_id: Type.Number({ description: '群号' }),
@@ -48,7 +48,7 @@ export const OB11GroupMemberSchema = Type.Object({
shut_up_timestamp: Type.Optional(Type.Number({ description: '禁言截止时间戳' })),
is_robot: Type.Optional(Type.Boolean({ description: '是否为机器人' })),
qage: Type.Optional(Type.Number({ description: 'Q龄' })),
}, { description: 'OneBot 11 群成员信息' });
}, { $id: 'OB11GroupMember', description: 'OneBot 11 群成员信息' });
export const OB11NotifySchema = Type.Object({
request_id: Type.Number({ description: '请求ID' }),
@@ -60,7 +60,7 @@ export const OB11NotifySchema = Type.Object({
checked: Type.Boolean({ description: '是否已处理' }),
actor: Type.Number({ description: '操作者QQ' }),
requester_nick: Type.String({ description: '申请者昵称' }),
}, { description: 'OneBot 11 通知信息' });
}, { $id: 'OB11Notify', description: 'OneBot 11 通知信息' });
export const lastestMessageSchema = Type.Object({
self_id: Type.Number({ description: '发送者QQ号' }),
@@ -82,7 +82,7 @@ export const lastestMessageSchema = Type.Object({
post_type: Type.String({ description: '发布类型' }),
group_id: Type.Number({ description: '群号' }),
group_name: Type.String({ description: '群名称' }),
}, { description: '最后一条消息' });
}, { $id: 'OB11LatestMessage', description: '最后一条消息' });
export const OB11MessageSchema = Type.Intersect([
lastestMessageSchema,
@@ -95,4 +95,4 @@ export const OB11MessageSchema = Type.Intersect([
likes_cnt: Type.String({ description: '点赞数' }),
})),
}, { description: 'OneBot 11 消息信息' })
]);
], { $id: 'OB11ActionMessage', description: 'OneBot 11 消息信息' });

View File

@@ -325,8 +325,11 @@ export const OB11MessageDataSchema = Type.Union([
OB11MessageDiceSchema,
OB11MessageRPSSchema,
OB11MessageContactSchema,
OB11MessageLocationSchema,
OB11MessageJsonSchema,
OB11MessageXmlSchema,
OB11MessageMarkdownSchema,
OB11MessageMiniAppSchema,
OB11MessageNodeSchema,
OB11MessageForwardSchema,
OB11MessageOnlineFileSchema,

View File

@@ -2,13 +2,16 @@ import { getAllHandlers } from '@/napcat-onebot/action/index';
import { AutoRegisterRouter } from '@/napcat-onebot/action/auto-register';
import { writeFileSync, existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { TSchema } from '@sinclair/typebox';
import { fileURLToPath } from 'node:url';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { TSchema } from '@sinclair/typebox';
import { OneBotAction, ActionExamples } from '@/napcat-onebot/action/OneBotAction';
import { napCatVersion } from 'napcat-common/src/version';
import * as MessageSchemas from '@/napcat-onebot/types/message';
import * as ActionSchemas from '@/napcat-onebot/action/schemas';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/* -------------------------------------------------------------------------- */
/* 基础类型 */
/* -------------------------------------------------------------------------- */
interface ActionSchemaInfo {
payload?: TSchema;
@@ -21,52 +24,524 @@ interface ActionSchemaInfo {
errorExamples?: Array<{ code: number, description: string; }>;
}
type JsonObject = { [key: string]: unknown; };
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const OPENAPI_OUTPUT_PATH = resolve(__dirname, 'openapi.json');
const MISSING_REPORT_PATH = resolve(__dirname, 'missing_props.log');
export const actionSchemas: Record<string, ActionSchemaInfo> = {};
/* -------------------------------------------------------------------------- */
/* 日志工具 */
/* -------------------------------------------------------------------------- */
/**
* 统一日志前缀,方便在构建日志中快速检索。
*/
const LOG_SCOPE = '[napcat-schema]';
function logSection (title: string) {
console.log(`\n${LOG_SCOPE} ── ${title}`);
}
function logInfo (message: string) {
console.log(`${LOG_SCOPE} ${message}`);
}
function logWarn (message: string) {
console.warn(`${LOG_SCOPE}${message}`);
}
function logSuccess (message: string) {
console.log(`${LOG_SCOPE}${message}`);
}
/* -------------------------------------------------------------------------- */
/* OpenAPI 基础组件(固定部分) */
/* -------------------------------------------------------------------------- */
const BaseResponseSchema: JsonObject = {
type: 'object',
'x-schema-id': 'BaseResponse',
properties: {
status: { type: 'string', description: '状态 (ok/failed)' },
retcode: { type: 'number', description: '返回码' },
data: { description: '业务数据(具体结构由各接口定义)' },
message: { type: 'string', description: '消息' },
wording: { type: 'string', description: '提示' },
stream: {
type: 'string',
description: '流式响应',
enum: ['stream-action', 'normal-action']
}
},
required: ['status', 'retcode']
};
const EmptyDataSchema: JsonObject = {
description: '无数据',
type: 'null'
};
const DEFAULT_SUCCESS_EXAMPLE_VALUE = {
status: 'ok',
retcode: 0,
data: {},
message: '',
wording: '',
stream: 'normal-action'
} as const;
const DEFAULT_ERROR_EXAMPLE_DEFINITIONS = ActionExamples.Common.errors;
const SUCCESS_DEFAULT_EXAMPLE_KEY = 'Success_Default';
function isObjectRecord (value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function isEmptyObject (value: unknown): value is Record<string, never> {
return isObjectRecord(value) && Object.keys(value).length === 0;
}
function isEmptyArray (value: unknown): value is [] {
return Array.isArray(value) && value.length === 0;
}
function isMeaninglessSuccessExampleData (value: unknown): boolean {
return value === null || isEmptyObject(value) || isEmptyArray(value);
}
function resolveCommonErrorExampleKey (error: { code: number, description: string; }): string | null {
const matched = DEFAULT_ERROR_EXAMPLE_DEFINITIONS.find(
item => item.code === error.code && item.description === error.description
);
return matched ? `Error_${matched.code}` : null;
}
/* -------------------------------------------------------------------------- */
/* 通用工具函数 */
/* -------------------------------------------------------------------------- */
/**
* 深拷贝 schema优先使用 structuredClone失败时回落到 JSON 序列化。
*/
function cloneSchema<T> (schema: T): T {
if (typeof globalThis.structuredClone === 'function') {
try {
return globalThis.structuredClone(schema);
} catch {
// fallback
}
}
return JSON.parse(JSON.stringify(schema)) as T;
}
/**
* 在 anyOf/oneOf 中,将“多个单值 enum 分支 + 可选 nullable 分支”压缩为单个 enum。
*
* 例:
* - anyOf: [{ type:'string', enum:['a'] }, { type:'string', enum:['b'] }]
* -> { type:'string', enum:['a','b'] }
*/
function collapseSingleValueEnumCombinator (items: unknown[]): Record<string, unknown> | null {
const enumValues: unknown[] = [];
let type: string | undefined;
let nullable = false;
for (const item of items) {
if (!item || typeof item !== 'object') {
return null;
}
const branch = item as Record<string, unknown>;
// 兼容仅有 nullable 的分支
if (branch['nullable'] === true && Object.keys(branch).length === 1) {
nullable = true;
continue;
}
const branchEnum = branch['enum'];
if (!Array.isArray(branchEnum) || branchEnum.length !== 1) {
return null;
}
enumValues.push(branchEnum[0]);
if (typeof branch['type'] === 'string') {
if (!type) {
type = branch['type'];
} else if (type !== branch['type']) {
return null;
}
}
}
if (enumValues.length === 0) {
return null;
}
const merged: Record<string, unknown> = { enum: [...new Set(enumValues)] };
if (type) {
merged['type'] = type;
}
if (nullable) {
merged['nullable'] = true;
}
return merged;
}
/**
* 将 TypeBox/JSON-Schema 映射为 OpenAPI 3.1 兼容结构。
*
* 关键规则:
* - $id -> x-schema-id保留标识用于后续 $ref 替换)
* - const -> enum:[const]
* - type:'void' / type:'undefined' -> type:'null'
* - nullable:true -> type 包含 'null'
* - anyOf/oneOf 的简单 enum 分支做压缩
*/
function sanitizeSchemaForOpenAPI<T> (schema: T): T {
const walk = (value: unknown): unknown => {
if (Array.isArray(value)) {
return value.map(walk);
}
if (value && typeof value === 'object') {
const obj = value as Record<string, unknown>;
const next: Record<string, unknown> = {};
for (const [key, child] of Object.entries(obj)) {
// 特殊处理 properties 容器:只遍历每个属性的 schema避免将容器对象误判为 schema 元对象
if (key === 'properties' && child && typeof child === 'object' && !Array.isArray(child)) {
const cleanProps: Record<string, unknown> = {};
for (const [propName, propSchema] of Object.entries(child as Record<string, unknown>)) {
cleanProps[propName] = walk(propSchema);
}
next[key] = cleanProps;
continue;
}
if (key === '$id') {
if (typeof child === 'string' && child.length > 0) {
next['x-schema-id'] = child;
}
continue;
}
if (key === 'const') {
next['enum'] = [child];
continue;
}
if (key === 'type' && typeof child === 'string') {
if (child === 'void' || child === 'undefined') {
next['type'] = 'null';
} else {
next['type'] = child;
}
continue;
}
if (key === 'type' && Array.isArray(child)) {
const types = child
.filter((t): t is string => typeof t === 'string')
.map(t => (t === 'void' || t === 'undefined') ? 'null' : t);
const normalizedTypes = [...new Set(types)];
if (normalizedTypes.length === 0) {
next['type'] = 'null';
} else if (normalizedTypes.length === 1) {
next['type'] = normalizedTypes[0];
} else {
next['type'] = normalizedTypes;
}
continue;
}
if ((key === 'anyOf' || key === 'oneOf') && Array.isArray(child)) {
const normalized = child.map(walk);
const mergedEnum = collapseSingleValueEnumCombinator(normalized);
if (mergedEnum) {
Object.assign(next, mergedEnum);
} else {
next[key] = normalized;
}
continue;
}
next[key] = walk(child);
}
// OpenAPI 3.1:将 nullable 归一到 type 包含 null
if (next['nullable'] === true) {
const currentType = next['type'];
if (typeof currentType === 'string') {
next['type'] = currentType === 'null' ? 'null' : [currentType, 'null'];
} else if (Array.isArray(currentType)) {
const normalizedTypes = [
...new Set(currentType
.filter((t): t is string => typeof t === 'string')
.map(t => (t === 'void' || t === 'undefined') ? 'null' : t)
.concat('null'))
];
next['type'] = normalizedTypes.length === 1 ? normalizedTypes[0] : normalizedTypes;
} else if (!('anyOf' in next) && !('oneOf' in next) && !('allOf' in next) && !('$ref' in next)) {
next['type'] = 'null';
}
delete next['nullable'];
}
// 兜底:仅有描述/元信息但缺少 type 时,补 object避免严格校验失败
if (
!('type' in next)
&& !('$ref' in next)
&& !('anyOf' in next)
&& !('oneOf' in next)
&& !('allOf' in next)
&& !('enum' in next)
&& !('properties' in next)
&& !('items' in next)
) {
const schemaMetaKeys = [
'description', 'title', 'default', 'examples', 'example',
'deprecated', 'readOnly', 'writeOnly', 'x-schema-id'
];
if (schemaMetaKeys.some(key => key in next)) {
next['type'] = 'object';
}
}
return next;
}
return value;
};
return walk(schema) as T;
}
/* -------------------------------------------------------------------------- */
/* Schema 注册 & 引用替换逻辑 */
/* -------------------------------------------------------------------------- */
/**
* 将模块中所有“含 $id 的导出 schema”注册到 components.schemas。
*/
function registerSchemasFromModule (
openapi: JsonObject,
source: Record<string, unknown>,
sourceName: string
) {
const components = ((openapi['components'] as JsonObject)['schemas'] as JsonObject);
let registeredCount = 0;
let duplicatedCount = 0;
for (const exportedValue of Object.values(source)) {
if (!exportedValue || typeof exportedValue !== 'object') {
continue;
}
const schema = cloneSchema(exportedValue) as JsonObject;
const schemaId = typeof schema['$id'] === 'string' && (schema['$id'] as string).length > 0
? schema['$id'] as string
: '';
if (!schemaId) {
continue;
}
if (components[schemaId]) {
duplicatedCount += 1;
logWarn(`发现重复 schema id${sourceName}${schemaId},将覆盖旧定义`);
}
components[schemaId] = sanitizeSchemaForOpenAPI(schema);
registeredCount += 1;
}
logInfo(`${sourceName} 注册完成:${registeredCount} 个 schema重复 ${duplicatedCount}`);
}
/**
* 对 components.schemas 做去内联:
* - 若子节点含 x-schema-id 且在 components.schemas 可命中
* - 则替换为 $ref
*
* 注意:组件根节点不会替换为自身,避免根级自引用。
*/
function replaceComponentInlineSchemasWithRefs (openapi: JsonObject) {
const components = openapi['components'] as JsonObject | undefined;
const schemas = components?.['schemas'] as JsonObject | undefined;
if (!schemas || typeof schemas !== 'object') {
return;
}
const availableSchemaIds = new Set(Object.keys(schemas));
let replacedCount = 0;
const walk = (value: unknown, ownerSchemaId: string): unknown => {
if (Array.isArray(value)) {
return value.map(item => walk(item, ownerSchemaId));
}
if (value && typeof value === 'object') {
const obj = value as JsonObject;
const schemaId = obj['x-schema-id'];
if (
typeof schemaId === 'string'
&& schemaId !== ownerSchemaId
&& availableSchemaIds.has(schemaId)
) {
replacedCount += 1;
return { $ref: `#/components/schemas/${schemaId}` };
}
const next: JsonObject = {};
for (const [key, child] of Object.entries(obj)) {
next[key] = walk(child, ownerSchemaId);
}
return next;
}
return value;
};
for (const [schemaId, schema] of Object.entries(schemas)) {
schemas[schemaId] = walk(schema, schemaId);
}
logInfo(`components 内联替换完成:${replacedCount}`);
}
/**
* 对 paths 做去内联:
* - 若节点含 x-schema-id 且在 components.schemas 可命中
* - 则替换为 $ref
*/
function replacePathInlineSchemasWithRefs (openapi: JsonObject) {
const paths = openapi['paths'];
const components = openapi['components'] as JsonObject | undefined;
const schemas = components?.['schemas'] as JsonObject | undefined;
if (!paths || typeof paths !== 'object' || !schemas || typeof schemas !== 'object') {
return;
}
const availableSchemaIds = new Set(Object.keys(schemas));
let replacedCount = 0;
const walk = (value: unknown): unknown => {
if (Array.isArray(value)) {
return value.map(walk);
}
if (value && typeof value === 'object') {
const obj = value as JsonObject;
const schemaId = obj['x-schema-id'];
if (typeof schemaId === 'string' && availableSchemaIds.has(schemaId)) {
replacedCount += 1;
return { $ref: `#/components/schemas/${schemaId}` };
}
const next: JsonObject = {};
for (const [key, child] of Object.entries(obj)) {
next[key] = walk(child);
}
return next;
}
return value;
};
openapi['paths'] = walk(paths) as JsonObject;
logInfo(`paths 内联替换完成:${replacedCount}`);
}
/* -------------------------------------------------------------------------- */
/* Action 收集逻辑 */
/* -------------------------------------------------------------------------- */
/**
* 收集全部 action schema 信息。
*/
export function initSchemas () {
const handlers = getAllHandlers(null as any, null as any);
handlers.forEach(handler => {
if (handler.actionName && (handler.actionName as string) !== 'unknown') {
const action = handler as OneBotAction<unknown, unknown>;
actionSchemas[handler.actionName] = {
payload: action.payloadSchema,
return: action.returnSchema,
summary: action.actionSummary,
description: action.actionDescription,
tags: action.actionTags,
payloadExample: action.payloadExample,
returnExample: action.returnExample,
errorExamples: action.errorExamples
};
if (!handler.actionName || (handler.actionName as string) === 'unknown') {
return;
}
const action = handler as OneBotAction<unknown, unknown>;
actionSchemas[handler.actionName] = {
payload: action.payloadSchema,
return: action.returnSchema,
summary: action.actionSummary,
description: action.actionDescription,
tags: action.actionTags,
payloadExample: action.payloadExample,
returnExample: action.returnExample,
errorExamples: action.errorExamples
};
});
AutoRegisterRouter.forEach((ActionClass) => {
AutoRegisterRouter.forEach(ActionClass => {
const handler = new ActionClass(null as any, null as any);
if (handler.actionName && (handler.actionName as string) !== 'unknown') {
const action = handler as OneBotAction<unknown, unknown>;
actionSchemas[handler.actionName] = {
payload: action.payloadSchema,
return: action.returnSchema,
summary: action.actionSummary,
description: action.actionDescription,
tags: action.actionTags,
payloadExample: action.payloadExample,
returnExample: action.returnExample,
errorExamples: action.errorExamples
};
if (!handler.actionName || (handler.actionName as string) === 'unknown') {
return;
}
const action = handler as OneBotAction<unknown, unknown>;
actionSchemas[handler.actionName] = {
payload: action.payloadSchema,
return: action.returnSchema,
summary: action.actionSummary,
description: action.actionDescription,
tags: action.actionTags,
payloadExample: action.payloadExample,
returnExample: action.returnExample,
errorExamples: action.errorExamples
};
});
}
export function generateOpenAPI () {
try {
initSchemas();
} catch (e) {
console.warn('Init schemas partial failure, proceeding with collected data...');
}
/* -------------------------------------------------------------------------- */
/* OpenAPI 构建主流程 */
/* -------------------------------------------------------------------------- */
const openapi: Record<string, unknown> = {
openapi: '3.0.1',
function createOpenAPIDocument (): Record<string, unknown> {
const componentExamples: Record<string, unknown> = {
[SUCCESS_DEFAULT_EXAMPLE_KEY]: {
summary: '成功响应',
value: DEFAULT_SUCCESS_EXAMPLE_VALUE
}
};
DEFAULT_ERROR_EXAMPLE_DEFINITIONS.forEach(error => {
componentExamples[`Error_${error.code}`] = {
summary: error.description,
value: {
status: 'failed',
retcode: error.code,
data: null,
message: error.description,
wording: error.description,
stream: 'normal-action'
}
};
});
return {
openapi: '3.1.0',
info: {
title: 'NapCat OneBot 11 HTTP API',
description: 'NapCatOneBot11 HTTP POST 接口文档',
@@ -81,39 +556,43 @@ export function generateOpenAPI () {
],
paths: {} as Record<string, unknown>,
components: {
schemas: {},
schemas: {
BaseResponse: BaseResponseSchema,
EmptyData: EmptyDataSchema
},
examples: componentExamples,
responses: {},
securitySchemes: {}
},
servers: [],
security: []
};
}
for (const [actionName, schemas] of Object.entries(actionSchemas)) {
if (!schemas.payload && !schemas.summary) continue;
const path = '/' + actionName;
const cleanPayload = schemas.payload ? JSON.parse(JSON.stringify(schemas.payload)) : { type: 'object', properties: {} };
const cleanReturn = schemas.return ? JSON.parse(JSON.stringify(schemas.return)) : { type: 'object', properties: {} };
// 构造响应示例
const responseExamples: Record<string, any> = {
'Success': {
function buildResponseExamples (schemas: ActionSchemaInfo): Record<string, unknown> {
const successData = schemas.returnExample ?? {};
const examples: Record<string, any> = {
Success: isMeaninglessSuccessExampleData(successData)
? { $ref: `#/components/examples/${SUCCESS_DEFAULT_EXAMPLE_KEY}` }
: {
summary: '成功响应',
value: {
status: 'ok',
retcode: 0,
data: schemas.returnExample || {},
data: successData,
message: '',
wording: '',
stream: 'normal-action'
}
}
};
};
if (schemas.errorExamples) {
schemas.errorExamples.forEach(error => {
responseExamples['Error_' + error.code] = {
if (schemas.errorExamples) {
schemas.errorExamples.forEach(error => {
const commonErrorKey = resolveCommonErrorExampleKey(error);
examples[`Error_${error.code}`] = commonErrorKey
? { $ref: `#/components/examples/${commonErrorKey}` }
: {
summary: error.description,
value: {
status: 'failed',
@@ -124,23 +603,33 @@ export function generateOpenAPI () {
stream: 'normal-action'
}
};
});
} else {
// 默认提供一个通用错误
responseExamples['Generic_Error'] = {
summary: '通用错误',
value: {
status: 'failed',
retcode: 1400,
data: null,
message: '请求参数错误或业务逻辑执行失败',
wording: '请求参数错误或业务逻辑执行失败',
stream: 'normal-action'
}
};
});
return examples;
}
examples['Generic_Error'] = {
$ref: '#/components/examples/Error_1400'
};
return examples;
}
function appendActionPaths (openapi: Record<string, unknown>) {
const paths = openapi['paths'] as Record<string, any>;
for (const [actionName, schemas] of Object.entries(actionSchemas)) {
if (!schemas.payload && !schemas.summary) {
continue;
}
const paths = openapi['paths'] as Record<string, any>;
const path = `/${actionName}`;
const cleanPayload = schemas.payload
? sanitizeSchemaForOpenAPI(cloneSchema(schemas.payload))
: { type: 'object', properties: {} };
const cleanReturn = schemas.return
? sanitizeSchemaForOpenAPI(cloneSchema(schemas.return))
: { $ref: '#/components/schemas/EmptyData' };
paths[path] = {
post: {
summary: schemas.summary || actionName,
@@ -154,7 +643,7 @@ export function generateOpenAPI () {
'application/json': {
schema: cleanPayload,
examples: {
'Default': {
Default: {
summary: '默认请求示例',
value: schemas.payloadExample || {}
}
@@ -168,18 +657,21 @@ export function generateOpenAPI () {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
status: { type: 'string', description: '状态 (ok/failed)' },
retcode: { type: 'number', description: '返回码' },
data: { ...cleanReturn, description: '数据' },
message: { type: 'string', description: '消息' },
wording: { type: 'string', description: '提示' },
stream: { type: 'string', description: '流式响应', enum: ['stream-action', 'normal-action'] }
},
required: ['status', 'retcode', 'data']
allOf: [
{ $ref: '#/components/schemas/BaseResponse' },
{
type: 'object',
required: ['data'],
properties: {
data: {
...(typeof cleanReturn === 'object' && cleanReturn ? cleanReturn : {}),
description: '业务数据'
}
}
}
]
},
examples: responseExamples
examples: buildResponseExamples(schemas)
}
}
}
@@ -188,16 +680,46 @@ export function generateOpenAPI () {
}
};
}
}
const outputPath = resolve(__dirname, 'openapi.json');
writeFileSync(outputPath, JSON.stringify(openapi, null, 2));
console.log('OpenAPI schema (3.0.1 Format) generated at: ' + outputPath);
export function generateOpenAPI () {
logSection('开始生成 OpenAPI 文档');
try {
initSchemas();
logInfo(`已收集 action: ${Object.keys(actionSchemas).length}`);
} catch {
logWarn('初始化 schema 过程中出现部分失败,将继续使用已收集的数据');
}
const openapi = createOpenAPIDocument();
logSection('注册组件 schema');
registerSchemasFromModule(openapi as JsonObject, MessageSchemas, 'types/message.ts');
registerSchemasFromModule(openapi as JsonObject, ActionSchemas, 'action/schemas.ts');
logSection('处理组件内联引用');
replaceComponentInlineSchemasWithRefs(openapi as JsonObject);
logSection('构建 paths');
appendActionPaths(openapi);
logSection('处理 paths 内联引用');
replacePathInlineSchemasWithRefs(openapi as JsonObject);
writeFileSync(OPENAPI_OUTPUT_PATH, JSON.stringify(openapi, null, 2));
logSuccess(`OpenAPI 生成完成:${OPENAPI_OUTPUT_PATH}`);
generateMissingReport();
}
/* -------------------------------------------------------------------------- */
/* 元数据缺失报告 */
/* -------------------------------------------------------------------------- */
function generateMissingReport () {
const missingReport: string[] = [];
for (const [actionName, schemas] of Object.entries(actionSchemas)) {
const missing: string[] = [];
if (!schemas.summary) missing.push('actionSummary');
@@ -206,18 +728,20 @@ function generateMissingReport () {
if (schemas.returnExample === undefined) missing.push('returnExample');
if (missing.length > 0) {
missingReport.push('[' + actionName + '] 缺失属性: ' + missing.join(', '));
missingReport.push(`[${actionName}] 缺失属性: ${missing.join(', ')}`);
}
}
const reportPath = resolve(__dirname, 'missing_props.log');
if (missingReport.length > 0) {
writeFileSync(reportPath, missingReport.join('\n'));
console.warn('\n检查到 ' + missingReport.length + ' 个接口存在元数据缺失,报告已保存至: ' + reportPath);
} else {
if (existsSync(reportPath)) writeFileSync(reportPath, '');
console.log('\n所有接口元数据已完整');
writeFileSync(MISSING_REPORT_PATH, missingReport.join('\n'));
logWarn(`检查到 ${missingReport.length} 个接口元数据缺失,报告已写入:${MISSING_REPORT_PATH}`);
return;
}
if (existsSync(MISSING_REPORT_PATH)) {
writeFileSync(MISSING_REPORT_PATH, '');
}
logSuccess('所有接口元数据完整');
}
generateOpenAPI();