czh-api 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  所有重要的版本更新都会记录在此文件中。
4
4
 
5
+ ## [1.0.5] - 2026-03-19
6
+
7
+ ### 修复
8
+ - 修复 FastAPI 场景下返回类型字段被错误降级为 `any` 的问题(如 `items.$ref`、`anyOf`、`oneOf`、`allOf` 组合场景)。
9
+ - 修复数组响应与嵌套对象类型在 fallback 生成流程中丢失字段的问题,避免出现仅有 `{ [key: string]: any }` 的空壳接口。
10
+ - 修复 `group_ids` 等多层结构类型解析不完整的问题,支持递归解析到更深层级(如 `number[] | null`)。
11
+ - 修复类型增强阶段对复杂类型名(数组、联合类型)处理不稳定导致的类型声明异常问题。
12
+
13
+ ### 改进
14
+ - 增强 fallback 类型生成策略:优先基于 `module.schemas` 还原字段结构,再回退到 `any`,生成结果更可用。
15
+ - 增强类型依赖收集能力:从字段类型表达式中提取嵌套引用类型并自动补齐声明。
16
+ - 完善 README 模板示例并与当前默认模板保持一致(`api.hbs`、`index.hbs`、`types.hbs`)。
17
+
5
18
  ## [1.0.4] - 2026-03-19
6
19
 
7
20
  ### 修复
package/README.md CHANGED
@@ -190,12 +190,7 @@ export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeNam
190
190
  return http.request<{{responseTypeName}}>({
191
191
  url: `{{path}}`,
192
192
  method: '{{method}}',
193
- {{#if hasParams}}
194
- params,
195
- {{/if}}
196
- {{#if hasData}}
197
- data,
198
- {{/if}}
193
+ {{#if hasParams}}{{#unless (eq method 'put')}}{{#unless (eq method 'post')}}params,{{/unless}}{{/unless}}{{/if}}{{#if hasData}}data,{{/if}}
199
194
  {{#if contentType}}
200
195
  headers: { 'Content-Type': '{{contentType}}' },
201
196
  {{/if}}
@@ -209,11 +204,9 @@ export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeNam
209
204
  export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeName}}{{/if}}{{#if hasData}}{{#if hasParams}}, {{/if}}data: {{requestBodyTypeName}}{{/if}}) => {
210
205
  return request.{{#if (eq method "delete")}}del{{else}}{{method}}{{/if}}<{{responseTypeName}}>({
211
206
  url: `{{path}}`,
212
- {{#if hasParams}}
213
- params,
214
- {{/if}}
215
- {{#if hasData}}
216
- data,
207
+ {{#if hasParams}}{{#unless (eq method 'put')}}{{#unless (eq method 'post')}}params,{{/unless}}{{/unless}}{{/if}}{{#if hasData}}data,{{/if}}
208
+ {{#if contentType}}
209
+ headers: { 'Content-Type': '{{contentType}}' },
217
210
  {{/if}}
218
211
  });
219
212
  };
@@ -48,7 +48,7 @@ function convertOpenApiTypeToTypeScript(openApiType) {
48
48
  case 'number':
49
49
  return 'number';
50
50
  default:
51
- return 'any';
51
+ return openApiType;
52
52
  }
53
53
  }
54
54
  // 改进类型定义的辅助函数
@@ -93,6 +93,138 @@ function findInterfaceBlockRange(content, typeName) {
93
93
  }
94
94
  return null;
95
95
  }
96
+ const TS_BUILTIN_TYPE_NAMES = new Set([
97
+ 'string',
98
+ 'number',
99
+ 'boolean',
100
+ 'null',
101
+ 'undefined',
102
+ 'void',
103
+ 'any',
104
+ 'unknown',
105
+ 'never',
106
+ 'object',
107
+ 'Record',
108
+ 'Array',
109
+ 'Date',
110
+ 'Promise',
111
+ 'true',
112
+ 'false',
113
+ ]);
114
+ function extractTypeIdentifiers(typeExpr) {
115
+ if (!typeExpr)
116
+ return [];
117
+ const matches = typeExpr.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [];
118
+ return [...new Set(matches.filter(name => !TS_BUILTIN_TYPE_NAMES.has(name)))];
119
+ }
120
+ function getSchemaNameFromRef(ref) {
121
+ if (!ref)
122
+ return '';
123
+ return ref.split('/').pop() || '';
124
+ }
125
+ function resolveSchemaTypeFromModuleSchemas(schema, moduleSchemas, visitedRefs = new Set()) {
126
+ if (!schema)
127
+ return 'any';
128
+ if (schema.$ref) {
129
+ const refName = getSchemaNameFromRef(schema.$ref);
130
+ return refName || 'any';
131
+ }
132
+ const schemaObj = schema;
133
+ if (schemaObj.anyOf && Array.isArray(schemaObj.anyOf) && schemaObj.anyOf.length > 0) {
134
+ const types = schemaObj.anyOf
135
+ .map(item => resolveSchemaTypeFromModuleSchemas(item, moduleSchemas, visitedRefs))
136
+ .filter(Boolean);
137
+ if (types.length > 0) {
138
+ return [...new Set(types)].join(' | ');
139
+ }
140
+ }
141
+ if (schemaObj.oneOf && Array.isArray(schemaObj.oneOf) && schemaObj.oneOf.length > 0) {
142
+ const types = schemaObj.oneOf
143
+ .map(item => resolveSchemaTypeFromModuleSchemas(item, moduleSchemas, visitedRefs))
144
+ .filter(Boolean);
145
+ if (types.length > 0) {
146
+ return [...new Set(types)].join(' | ');
147
+ }
148
+ }
149
+ if (schemaObj.type === 'array') {
150
+ const itemType = resolveSchemaTypeFromModuleSchemas(schemaObj.items, moduleSchemas, visitedRefs);
151
+ return `${itemType || 'any'}[]`;
152
+ }
153
+ if (schemaObj.type === 'object' && schemaObj.additionalProperties) {
154
+ if (schemaObj.additionalProperties === true) {
155
+ return 'Record<string, any>';
156
+ }
157
+ const valueType = resolveSchemaTypeFromModuleSchemas(schemaObj.additionalProperties, moduleSchemas, visitedRefs);
158
+ return `Record<string, ${valueType || 'any'}>`;
159
+ }
160
+ if (schemaObj.type === 'object' && schemaObj.properties && Object.keys(schemaObj.properties).length > 0) {
161
+ const requiredSet = new Set(schemaObj.required || []);
162
+ const inlineFields = Object.entries(schemaObj.properties).map(([key, value]) => {
163
+ const fieldType = resolveSchemaTypeFromModuleSchemas(value, moduleSchemas, visitedRefs);
164
+ const optional = requiredSet.has(key) ? '' : '?';
165
+ return `${key}${optional}: ${fieldType || 'any'}`;
166
+ });
167
+ return `{ ${inlineFields.join('; ')} }`;
168
+ }
169
+ const baseType = convertOpenApiTypeToTypeScript(schemaObj.type);
170
+ if (!baseType)
171
+ return 'any';
172
+ return schemaObj.nullable ? `${baseType} | null` : baseType;
173
+ }
174
+ function collectSchemaProperties(schema, moduleSchemas, visitedRefs = new Set()) {
175
+ const collected = {
176
+ properties: {},
177
+ required: new Set(),
178
+ };
179
+ if (!schema)
180
+ return collected;
181
+ if (schema.$ref) {
182
+ const ref = schema.$ref;
183
+ if (!visitedRefs.has(ref)) {
184
+ visitedRefs.add(ref);
185
+ const refName = getSchemaNameFromRef(ref);
186
+ const refSchema = moduleSchemas[refName];
187
+ if (refSchema) {
188
+ const nested = collectSchemaProperties(refSchema, moduleSchemas, visitedRefs);
189
+ Object.assign(collected.properties, nested.properties);
190
+ nested.required.forEach(field => collected.required.add(field));
191
+ }
192
+ }
193
+ return collected;
194
+ }
195
+ const schemaObj = schema;
196
+ if (schemaObj.allOf && Array.isArray(schemaObj.allOf)) {
197
+ schemaObj.allOf.forEach(item => {
198
+ const nested = collectSchemaProperties(item, moduleSchemas, visitedRefs);
199
+ Object.assign(collected.properties, nested.properties);
200
+ nested.required.forEach(field => collected.required.add(field));
201
+ });
202
+ }
203
+ if (schemaObj.properties) {
204
+ Object.entries(schemaObj.properties).forEach(([name, value]) => {
205
+ collected.properties[name] = value;
206
+ });
207
+ }
208
+ (schemaObj.required || []).forEach(field => collected.required.add(field));
209
+ return collected;
210
+ }
211
+ function buildInterfaceDefinitionFromModuleSchemas(typeName, moduleSchemas) {
212
+ const schema = moduleSchemas[typeName];
213
+ if (!schema)
214
+ return null;
215
+ const { properties, required } = collectSchemaProperties(schema, moduleSchemas);
216
+ const entries = Object.entries(properties);
217
+ if (entries.length === 0)
218
+ return null;
219
+ const lines = entries.map(([propName, propSchema]) => {
220
+ const optional = required.has(propName) ? '' : '?';
221
+ const propType = resolveSchemaTypeFromModuleSchemas(propSchema, moduleSchemas) || 'any';
222
+ const propDescription = propSchema.description || '';
223
+ const comment = propDescription ? ` // ${propDescription}` : '';
224
+ return ` ${propName}${optional}: ${propType};${comment}`;
225
+ });
226
+ return `export interface ${typeName} {\n${lines.join('\n')}\n}`;
227
+ }
96
228
  function improveTypeDefinitions(typesContent, endpoints) {
97
229
  let improvedContent = typesContent;
98
230
  // 收集所有需要改进的类型定义
@@ -481,6 +613,7 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
481
613
  console.log(chalk_1.default.blue(`为模块 "${moduleName}" 生成基于 JSDoc 的类型定义...`));
482
614
  // 创建类型定义映射
483
615
  const typeDefinitions = {};
616
+ const nestedReferencedTypes = new Set();
484
617
  // 从 endpoints 中提取类型定义
485
618
  module.endpoints.forEach(endpoint => {
486
619
  // 辅助函数:将 jsdoc params 转换为字段
@@ -497,6 +630,11 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
497
630
  else {
498
631
  type = convertOpenApiTypeToTypeScript(type);
499
632
  }
633
+ extractTypeIdentifiers(type).forEach(typeName => {
634
+ if (isValidTypeIdentifier(typeName)) {
635
+ nestedReferencedTypes.add(typeName);
636
+ }
637
+ });
500
638
  const comment = param.description ? ` // ${param.description}` : '';
501
639
  return ` ${param.name}${optional}: ${type};${comment}`;
502
640
  })
@@ -533,11 +671,23 @@ ${responseFields}
533
671
  }
534
672
  });
535
673
  // 为没有定义的类型添加基本定义
536
- allReferencedTypes.forEach(typeName => {
674
+ const fallbackReferencedTypes = [
675
+ ...new Set([
676
+ ...allReferencedTypes,
677
+ ...Object.keys(module.schemas || {}),
678
+ ...nestedReferencedTypes,
679
+ ])
680
+ ];
681
+ fallbackReferencedTypes.forEach(typeName => {
537
682
  if (!isValidTypeIdentifier(typeName)) {
538
683
  return;
539
684
  }
540
685
  if (!typeDefinitions[typeName]) {
686
+ const schemaBasedDefinition = buildInterfaceDefinitionFromModuleSchemas(typeName, module.schemas);
687
+ if (schemaBasedDefinition) {
688
+ typeDefinitions[typeName] = schemaBasedDefinition;
689
+ return;
690
+ }
541
691
  typeDefinitions[typeName] = `export interface ${typeName} {
542
692
  [key: string]: any;
543
693
  }`;
@@ -87,11 +87,68 @@ function convertOpenApiTypeToTypeScript(openApiType) {
87
87
  case 'number':
88
88
  return 'number';
89
89
  default:
90
- return 'any';
90
+ return openApiType;
91
91
  }
92
92
  }
93
+ function resolveSchemaToTypeScript(schema, allSchemas) {
94
+ if (!schema)
95
+ return 'any';
96
+ if (isReferenceObject(schema)) {
97
+ return getSchemaName(schema.$ref) || 'any';
98
+ }
99
+ if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
100
+ const unionTypes = schema.anyOf
101
+ .map(item => resolveSchemaToTypeScript(item, allSchemas))
102
+ .filter(Boolean);
103
+ if (unionTypes.length > 0) {
104
+ return [...new Set(unionTypes)].join(' | ');
105
+ }
106
+ }
107
+ if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
108
+ const unionTypes = schema.oneOf
109
+ .map(item => resolveSchemaToTypeScript(item, allSchemas))
110
+ .filter(Boolean);
111
+ if (unionTypes.length > 0) {
112
+ return [...new Set(unionTypes)].join(' | ');
113
+ }
114
+ }
115
+ if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) {
116
+ const intersectionTypes = schema.allOf
117
+ .map(item => resolveSchemaToTypeScript(item, allSchemas))
118
+ .filter(Boolean);
119
+ if (intersectionTypes.length > 0) {
120
+ return [...new Set(intersectionTypes)].join(' & ');
121
+ }
122
+ }
123
+ if (schema.type === 'array') {
124
+ const itemType = resolveSchemaToTypeScript(schema.items, allSchemas);
125
+ return `${itemType || 'any'}[]`;
126
+ }
127
+ if (schema.type === 'object') {
128
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
129
+ const requiredSet = new Set(schema.required || []);
130
+ const inlineFields = Object.entries(schema.properties).map(([key, value]) => {
131
+ const fieldType = resolveSchemaToTypeScript(value, allSchemas);
132
+ const optional = requiredSet.has(key) ? '' : '?';
133
+ return `${key}${optional}: ${fieldType || 'any'}`;
134
+ });
135
+ return `{ ${inlineFields.join('; ')} }`;
136
+ }
137
+ if (schema.additionalProperties) {
138
+ if (schema.additionalProperties === true) {
139
+ return 'Record<string, any>';
140
+ }
141
+ const valueType = resolveSchemaToTypeScript(schema.additionalProperties, allSchemas);
142
+ return `Record<string, ${valueType || 'any'}>`;
143
+ }
144
+ return 'any';
145
+ }
146
+ const baseType = convertOpenApiTypeToTypeScript(schema.type);
147
+ const nullable = schema.nullable ? ' | null' : '';
148
+ return `${baseType}${nullable}`;
149
+ }
93
150
  function extractJsdocParamsFromSchema(schema, allSchemas) {
94
- var _a;
151
+ var _a, _b;
95
152
  const params = [];
96
153
  if (!schema)
97
154
  return params;
@@ -106,9 +163,12 @@ function extractJsdocParamsFromSchema(schema, allSchemas) {
106
163
  if (targetSchema === null || targetSchema === void 0 ? void 0 : targetSchema.properties) {
107
164
  for (const propName in targetSchema.properties) {
108
165
  const prop = targetSchema.properties[propName];
109
- let propType = 'any';
166
+ let propType = resolveSchemaToTypeScript(prop, allSchemas);
167
+ const propDescription = isReferenceObject(prop)
168
+ ? (((_a = allSchemas[getSchemaName(prop.$ref)]) === null || _a === void 0 ? void 0 : _a.description) || '')
169
+ : ((prop === null || prop === void 0 ? void 0 : prop.description) || '');
110
170
  // 处理 anyOf 数组 (FastAPI 常用的联合类型)
111
- if (prop.anyOf && Array.isArray(prop.anyOf)) {
171
+ if (false && prop.anyOf && Array.isArray(prop.anyOf)) {
112
172
  const types = prop.anyOf.map(item => {
113
173
  if (isReferenceObject(item)) {
114
174
  return getSchemaName(item.$ref);
@@ -130,7 +190,7 @@ function extractJsdocParamsFromSchema(schema, allSchemas) {
130
190
  }
131
191
  }
132
192
  // 处理 oneOf 数组
133
- else if (prop.oneOf && Array.isArray(prop.oneOf)) {
193
+ else if (false && prop.oneOf && Array.isArray(prop.oneOf)) {
134
194
  const types = prop.oneOf.map(item => {
135
195
  if (isReferenceObject(item)) {
136
196
  return getSchemaName(item.$ref);
@@ -152,14 +212,26 @@ function extractJsdocParamsFromSchema(schema, allSchemas) {
152
212
  }
153
213
  }
154
214
  // 处理普通类型
155
- else if (prop.type) {
156
- propType = convertOpenApiTypeToTypeScript(prop.type);
215
+ else if (false && prop.type) {
216
+ const propSchema = prop;
217
+ if (propSchema.type === 'array' && propSchema.items) {
218
+ if (isReferenceObject(propSchema.items)) {
219
+ propType = `${getSchemaName(propSchema.items.$ref)}[]`;
220
+ }
221
+ else {
222
+ const itemType = convertOpenApiTypeToTypeScript(propSchema.items.type);
223
+ propType = `${itemType}[]`;
224
+ }
225
+ }
226
+ else {
227
+ propType = convertOpenApiTypeToTypeScript(propSchema.type);
228
+ }
157
229
  }
158
230
  params.push({
159
231
  name: propName,
160
- type: propType,
161
- description: (prop === null || prop === void 0 ? void 0 : prop.description) || '',
162
- required: (_a = targetSchema.required) === null || _a === void 0 ? void 0 : _a.includes(propName)
232
+ type: propType || 'any',
233
+ description: propDescription,
234
+ required: (_b = targetSchema.required) === null || _b === void 0 ? void 0 : _b.includes(propName)
163
235
  });
164
236
  }
165
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czh-api",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "A CLI tool to generate TypeScript API clients from Swagger/OpenAPI documents.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -18,8 +18,8 @@ import { compile } from 'json-schema-to-typescript';
18
18
  import { Converter } from '@apiture/openapi-down-convert';
19
19
 
20
20
  // 统一的类型转换函数
21
- function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string {
22
- if (!openApiType) return 'any';
21
+ function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string {
22
+ if (!openApiType) return 'any';
23
23
 
24
24
  switch (openApiType) {
25
25
  case 'integer':
@@ -32,12 +32,12 @@ function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string
32
32
  return 'boolean';
33
33
  case 'string':
34
34
  return 'string';
35
- case 'number':
36
- return 'number';
37
- default:
38
- return 'any';
39
- }
40
- }
35
+ case 'number':
36
+ return 'number';
37
+ default:
38
+ return openApiType;
39
+ }
40
+ }
41
41
 
42
42
  // 改进类型定义的辅助函数
43
43
  function isValidTypeIdentifier(typeName: string | undefined): boolean {
@@ -85,6 +85,176 @@ function findInterfaceBlockRange(content: string, typeName: string): { start: nu
85
85
  return null;
86
86
  }
87
87
 
88
+ const TS_BUILTIN_TYPE_NAMES = new Set([
89
+ 'string',
90
+ 'number',
91
+ 'boolean',
92
+ 'null',
93
+ 'undefined',
94
+ 'void',
95
+ 'any',
96
+ 'unknown',
97
+ 'never',
98
+ 'object',
99
+ 'Record',
100
+ 'Array',
101
+ 'Date',
102
+ 'Promise',
103
+ 'true',
104
+ 'false',
105
+ ]);
106
+
107
+ function extractTypeIdentifiers(typeExpr: string | undefined): string[] {
108
+ if (!typeExpr) return [];
109
+ const matches = typeExpr.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [];
110
+ return [...new Set(matches.filter(name => !TS_BUILTIN_TYPE_NAMES.has(name)))];
111
+ }
112
+
113
+ function getSchemaNameFromRef(ref: string | undefined): string {
114
+ if (!ref) return '';
115
+ return ref.split('/').pop() || '';
116
+ }
117
+
118
+ function resolveSchemaTypeFromModuleSchemas(
119
+ schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
120
+ moduleSchemas: { [typeName: string]: OpenAPIV3.SchemaObject },
121
+ visitedRefs: Set<string> = new Set()
122
+ ): string {
123
+ if (!schema) return 'any';
124
+
125
+ if ((schema as OpenAPIV3.ReferenceObject).$ref) {
126
+ const refName = getSchemaNameFromRef((schema as OpenAPIV3.ReferenceObject).$ref);
127
+ return refName || 'any';
128
+ }
129
+
130
+ const schemaObj = schema as OpenAPIV3.SchemaObject;
131
+
132
+ if (schemaObj.anyOf && Array.isArray(schemaObj.anyOf) && schemaObj.anyOf.length > 0) {
133
+ const types = schemaObj.anyOf
134
+ .map(item => resolveSchemaTypeFromModuleSchemas(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, moduleSchemas, visitedRefs))
135
+ .filter(Boolean);
136
+ if (types.length > 0) {
137
+ return [...new Set(types)].join(' | ');
138
+ }
139
+ }
140
+
141
+ if (schemaObj.oneOf && Array.isArray(schemaObj.oneOf) && schemaObj.oneOf.length > 0) {
142
+ const types = schemaObj.oneOf
143
+ .map(item => resolveSchemaTypeFromModuleSchemas(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, moduleSchemas, visitedRefs))
144
+ .filter(Boolean);
145
+ if (types.length > 0) {
146
+ return [...new Set(types)].join(' | ');
147
+ }
148
+ }
149
+
150
+ if (schemaObj.type === 'array') {
151
+ const itemType = resolveSchemaTypeFromModuleSchemas(
152
+ schemaObj.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
153
+ moduleSchemas,
154
+ visitedRefs
155
+ );
156
+ return `${itemType || 'any'}[]`;
157
+ }
158
+
159
+ if (schemaObj.type === 'object' && schemaObj.additionalProperties) {
160
+ if (schemaObj.additionalProperties === true) {
161
+ return 'Record<string, any>';
162
+ }
163
+ const valueType = resolveSchemaTypeFromModuleSchemas(
164
+ schemaObj.additionalProperties as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
165
+ moduleSchemas,
166
+ visitedRefs
167
+ );
168
+ return `Record<string, ${valueType || 'any'}>`;
169
+ }
170
+
171
+ if (schemaObj.type === 'object' && schemaObj.properties && Object.keys(schemaObj.properties).length > 0) {
172
+ const requiredSet = new Set(schemaObj.required || []);
173
+ const inlineFields = Object.entries(schemaObj.properties).map(([key, value]) => {
174
+ const fieldType = resolveSchemaTypeFromModuleSchemas(
175
+ value as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
176
+ moduleSchemas,
177
+ visitedRefs
178
+ );
179
+ const optional = requiredSet.has(key) ? '' : '?';
180
+ return `${key}${optional}: ${fieldType || 'any'}`;
181
+ });
182
+ return `{ ${inlineFields.join('; ')} }`;
183
+ }
184
+
185
+ const baseType = convertOpenApiTypeToTypeScript(schemaObj.type);
186
+ if (!baseType) return 'any';
187
+ return schemaObj.nullable ? `${baseType} | null` : baseType;
188
+ }
189
+
190
+ function collectSchemaProperties(
191
+ schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
192
+ moduleSchemas: { [typeName: string]: OpenAPIV3.SchemaObject },
193
+ visitedRefs: Set<string> = new Set()
194
+ ): { properties: Record<string, OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject>; required: Set<string> } {
195
+ const collected: { properties: Record<string, OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject>; required: Set<string> } = {
196
+ properties: {},
197
+ required: new Set<string>(),
198
+ };
199
+ if (!schema) return collected;
200
+
201
+ if ((schema as OpenAPIV3.ReferenceObject).$ref) {
202
+ const ref = (schema as OpenAPIV3.ReferenceObject).$ref;
203
+ if (!visitedRefs.has(ref)) {
204
+ visitedRefs.add(ref);
205
+ const refName = getSchemaNameFromRef(ref);
206
+ const refSchema = moduleSchemas[refName];
207
+ if (refSchema) {
208
+ const nested = collectSchemaProperties(refSchema, moduleSchemas, visitedRefs);
209
+ Object.assign(collected.properties, nested.properties);
210
+ nested.required.forEach(field => collected.required.add(field));
211
+ }
212
+ }
213
+ return collected;
214
+ }
215
+
216
+ const schemaObj = schema as OpenAPIV3.SchemaObject;
217
+
218
+ if (schemaObj.allOf && Array.isArray(schemaObj.allOf)) {
219
+ schemaObj.allOf.forEach(item => {
220
+ const nested = collectSchemaProperties(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, moduleSchemas, visitedRefs);
221
+ Object.assign(collected.properties, nested.properties);
222
+ nested.required.forEach(field => collected.required.add(field));
223
+ });
224
+ }
225
+
226
+ if (schemaObj.properties) {
227
+ Object.entries(schemaObj.properties).forEach(([name, value]) => {
228
+ collected.properties[name] = value as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject;
229
+ });
230
+ }
231
+
232
+ (schemaObj.required || []).forEach(field => collected.required.add(field));
233
+ return collected;
234
+ }
235
+
236
+ function buildInterfaceDefinitionFromModuleSchemas(
237
+ typeName: string,
238
+ moduleSchemas: { [typeName: string]: OpenAPIV3.SchemaObject }
239
+ ): string | null {
240
+ const schema = moduleSchemas[typeName];
241
+ if (!schema) return null;
242
+
243
+ const { properties, required } = collectSchemaProperties(schema, moduleSchemas);
244
+ const entries = Object.entries(properties);
245
+ if (entries.length === 0) return null;
246
+
247
+ const lines = entries.map(([propName, propSchema]) => {
248
+ const optional = required.has(propName) ? '' : '?';
249
+ const propType = resolveSchemaTypeFromModuleSchemas(propSchema, moduleSchemas) || 'any';
250
+ const propDescription = (propSchema as OpenAPIV3.SchemaObject).description || '';
251
+ const comment = propDescription ? ` // ${propDescription}` : '';
252
+ return ` ${propName}${optional}: ${propType};${comment}`;
253
+ });
254
+
255
+ return `export interface ${typeName} {\n${lines.join('\n')}\n}`;
256
+ }
257
+
88
258
  function improveTypeDefinitions(typesContent: string, endpoints: any[]): string {
89
259
  let improvedContent = typesContent;
90
260
 
@@ -100,13 +270,13 @@ function improveTypeDefinitions(typesContent: string, endpoints: any[]): string
100
270
  let type = param.type;
101
271
 
102
272
  // 处理联合类型,确保正确的 TypeScript 语法
103
- if (type && type.includes(' | ')) {
104
- if (type.includes('null')) {
105
- type = type.replace(/\s*\|\s*null/g, ' | null');
106
- }
107
- } else {
108
- type = convertOpenApiTypeToTypeScript(type);
109
- }
273
+ if (type && type.includes(' | ')) {
274
+ if (type.includes('null')) {
275
+ type = type.replace(/\s*\|\s*null/g, ' | null');
276
+ }
277
+ } else {
278
+ type = convertOpenApiTypeToTypeScript(type);
279
+ }
110
280
 
111
281
  const comment = param.description ? ` // ${param.description}` : '';
112
282
  return ` ${param.name}${optional}: ${type};${comment}`;
@@ -542,7 +712,8 @@ export const handleBuild = async () => {
542
712
  console.log(chalk.blue(`为模块 "${moduleName}" 生成基于 JSDoc 的类型定义...`));
543
713
 
544
714
  // 创建类型定义映射
545
- const typeDefinitions: { [typeName: string]: string } = {};
715
+ const typeDefinitions: { [typeName: string]: string } = {};
716
+ const nestedReferencedTypes = new Set<string>();
546
717
 
547
718
  // 从 endpoints 中提取类型定义
548
719
  module.endpoints.forEach(endpoint => {
@@ -553,17 +724,23 @@ export const handleBuild = async () => {
553
724
  const optional = !param.required ? '?' : '';
554
725
  let type = param.type;
555
726
 
556
- if (type && type.includes(' | ')) {
557
- if (type.includes('null')) {
558
- type = type.replace(/\s*\|\s*null/g, ' | null');
559
- }
560
- } else {
561
- type = convertOpenApiTypeToTypeScript(type);
562
- }
563
-
564
- const comment = param.description ? ` // ${param.description}` : '';
565
- return ` ${param.name}${optional}: ${type};${comment}`;
566
- })
727
+ if (type && type.includes(' | ')) {
728
+ if (type.includes('null')) {
729
+ type = type.replace(/\s*\|\s*null/g, ' | null');
730
+ }
731
+ } else {
732
+ type = convertOpenApiTypeToTypeScript(type);
733
+ }
734
+
735
+ extractTypeIdentifiers(type).forEach(typeName => {
736
+ if (isValidTypeIdentifier(typeName)) {
737
+ nestedReferencedTypes.add(typeName);
738
+ }
739
+ });
740
+
741
+ const comment = param.description ? ` // ${param.description}` : '';
742
+ return ` ${param.name}${optional}: ${type};${comment}`;
743
+ })
567
744
  .join('\n');
568
745
 
569
746
  // 处理 params 类型 - 使用 paramsJsdocParams
@@ -604,11 +781,26 @@ ${responseFields}
604
781
  });
605
782
 
606
783
  // 为没有定义的类型添加基本定义
607
- allReferencedTypes.forEach(typeName => {
784
+ const fallbackReferencedTypes = [
785
+ ...new Set([
786
+ ...allReferencedTypes,
787
+ ...Object.keys(module.schemas || {}),
788
+ ...nestedReferencedTypes,
789
+ ])
790
+ ];
791
+ fallbackReferencedTypes.forEach(typeName => {
608
792
  if (!isValidTypeIdentifier(typeName)) {
609
793
  return;
610
794
  }
611
795
  if (!typeDefinitions[typeName]) {
796
+ const schemaBasedDefinition = buildInterfaceDefinitionFromModuleSchemas(
797
+ typeName,
798
+ module.schemas as { [typeName: string]: OpenAPIV3.SchemaObject }
799
+ );
800
+ if (schemaBasedDefinition) {
801
+ typeDefinitions[typeName] = schemaBasedDefinition;
802
+ return;
803
+ }
612
804
  typeDefinitions[typeName] = `export interface ${typeName} {
613
805
  [key: string]: any;
614
806
  }`;
@@ -111,7 +111,7 @@ function getModuleName(path: string, pathPrefixes: Array<{ path: string; package
111
111
  return parts[0] || 'default';
112
112
  }
113
113
 
114
- function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string {
114
+ function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string {
115
115
  if (!openApiType) return 'any';
116
116
 
117
117
  switch (openApiType) {
@@ -125,12 +125,88 @@ function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string
125
125
  return 'boolean';
126
126
  case 'string':
127
127
  return 'string';
128
- case 'number':
129
- return 'number';
130
- default:
131
- return 'any';
132
- }
133
- }
128
+ case 'number':
129
+ return 'number';
130
+ default:
131
+ return openApiType;
132
+ }
133
+ }
134
+
135
+ function resolveSchemaToTypeScript(
136
+ schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
137
+ allSchemas: { [key: string]: any }
138
+ ): string {
139
+ if (!schema) return 'any';
140
+
141
+ if (isReferenceObject(schema)) {
142
+ return getSchemaName(schema.$ref) || 'any';
143
+ }
144
+
145
+ if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
146
+ const unionTypes = schema.anyOf
147
+ .map(item => resolveSchemaToTypeScript(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, allSchemas))
148
+ .filter(Boolean);
149
+ if (unionTypes.length > 0) {
150
+ return [...new Set(unionTypes)].join(' | ');
151
+ }
152
+ }
153
+
154
+ if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
155
+ const unionTypes = schema.oneOf
156
+ .map(item => resolveSchemaToTypeScript(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, allSchemas))
157
+ .filter(Boolean);
158
+ if (unionTypes.length > 0) {
159
+ return [...new Set(unionTypes)].join(' | ');
160
+ }
161
+ }
162
+
163
+ if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) {
164
+ const intersectionTypes = schema.allOf
165
+ .map(item => resolveSchemaToTypeScript(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, allSchemas))
166
+ .filter(Boolean);
167
+ if (intersectionTypes.length > 0) {
168
+ return [...new Set(intersectionTypes)].join(' & ');
169
+ }
170
+ }
171
+
172
+ if (schema.type === 'array') {
173
+ const itemType = resolveSchemaToTypeScript(
174
+ schema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
175
+ allSchemas
176
+ );
177
+ return `${itemType || 'any'}[]`;
178
+ }
179
+
180
+ if (schema.type === 'object') {
181
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
182
+ const requiredSet = new Set(schema.required || []);
183
+ const inlineFields = Object.entries(schema.properties).map(([key, value]) => {
184
+ const fieldType = resolveSchemaToTypeScript(
185
+ value as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
186
+ allSchemas
187
+ );
188
+ const optional = requiredSet.has(key) ? '' : '?';
189
+ return `${key}${optional}: ${fieldType || 'any'}`;
190
+ });
191
+ return `{ ${inlineFields.join('; ')} }`;
192
+ }
193
+ if (schema.additionalProperties) {
194
+ if (schema.additionalProperties === true) {
195
+ return 'Record<string, any>';
196
+ }
197
+ const valueType = resolveSchemaToTypeScript(
198
+ schema.additionalProperties as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
199
+ allSchemas
200
+ );
201
+ return `Record<string, ${valueType || 'any'}>`;
202
+ }
203
+ return 'any';
204
+ }
205
+
206
+ const baseType = convertOpenApiTypeToTypeScript(schema.type);
207
+ const nullable = schema.nullable ? ' | null' : '';
208
+ return `${baseType}${nullable}`;
209
+ }
134
210
 
135
211
  function extractJsdocParamsFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, allSchemas: { [key: string]: any }): Required<Endpoint>['jsdocParams'] {
136
212
  const params: Required<Endpoint>['jsdocParams'] = [];
@@ -146,13 +222,15 @@ function extractJsdocParamsFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3
146
222
 
147
223
  if (targetSchema?.properties) {
148
224
  for (const propName in targetSchema.properties) {
149
- const prop = targetSchema.properties[propName] as OpenAPIV3.SchemaObject;
150
-
151
- let propType = 'any';
225
+ const prop = targetSchema.properties[propName] as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject;
226
+ let propType = resolveSchemaToTypeScript(prop, allSchemas);
227
+ const propDescription = isReferenceObject(prop)
228
+ ? ((allSchemas[getSchemaName(prop.$ref)] as OpenAPIV3.SchemaObject | undefined)?.description || '')
229
+ : (prop?.description || '');
152
230
 
153
231
  // 处理 anyOf 数组 (FastAPI 常用的联合类型)
154
- if (prop.anyOf && Array.isArray(prop.anyOf)) {
155
- const types = prop.anyOf.map(item => {
232
+ if (false && (prop as OpenAPIV3.SchemaObject).anyOf && Array.isArray((prop as OpenAPIV3.SchemaObject).anyOf)) {
233
+ const types = (prop as OpenAPIV3.SchemaObject).anyOf!.map(item => {
156
234
  if (isReferenceObject(item)) {
157
235
  return getSchemaName(item.$ref);
158
236
  } else {
@@ -173,8 +251,8 @@ function extractJsdocParamsFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3
173
251
  }
174
252
  }
175
253
  // 处理 oneOf 数组
176
- else if (prop.oneOf && Array.isArray(prop.oneOf)) {
177
- const types = prop.oneOf.map(item => {
254
+ else if (false && (prop as OpenAPIV3.SchemaObject).oneOf && Array.isArray((prop as OpenAPIV3.SchemaObject).oneOf)) {
255
+ const types = (prop as OpenAPIV3.SchemaObject).oneOf!.map(item => {
178
256
  if (isReferenceObject(item)) {
179
257
  return getSchemaName(item.$ref);
180
258
  } else {
@@ -195,16 +273,26 @@ function extractJsdocParamsFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3
195
273
  }
196
274
  }
197
275
  // 处理普通类型
198
- else if (prop.type) {
199
- propType = convertOpenApiTypeToTypeScript(prop.type);
200
- }
276
+ else if (false && (prop as OpenAPIV3.SchemaObject).type) {
277
+ const propSchema: any = prop as any;
278
+ if (propSchema.type === 'array' && propSchema.items) {
279
+ if (isReferenceObject(propSchema.items)) {
280
+ propType = `${getSchemaName(propSchema.items.$ref)}[]`;
281
+ } else {
282
+ const itemType = convertOpenApiTypeToTypeScript((propSchema.items as OpenAPIV3.SchemaObject).type);
283
+ propType = `${itemType}[]`;
284
+ }
285
+ } else {
286
+ propType = convertOpenApiTypeToTypeScript(propSchema.type);
287
+ }
288
+ }
201
289
 
202
- params.push({
203
- name: propName,
204
- type: propType,
205
- description: prop?.description || '',
206
- required: targetSchema.required?.includes(propName)
207
- });
290
+ params.push({
291
+ name: propName,
292
+ type: propType || 'any',
293
+ description: propDescription,
294
+ required: targetSchema.required?.includes(propName)
295
+ });
208
296
  }
209
297
  }
210
298
  return params;