@cnbcool/cnb-api-generate 2.11.8 → 2.12.0

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.
@@ -45,6 +45,7 @@ const is_english_only_1 = require("../../utils/is-english-only");
45
45
  const generate_record_ast_node_1 = require("../record/generate-record-ast-node");
46
46
  const generate_interface_anchor_ast_node_1 = require("./generate-interface-anchor-ast-node");
47
47
  const comment_line_break_1 = require("../../utils/comment-line-break");
48
+ const is_schema_like_1 = require("../../utils/is-schema-like");
48
49
  const logger = (0, debug_1.default)('cag:interface');
49
50
  function generateInterfaceASTNode(name, definitionsType, currProperties, definitions, defintionsMap, parentKey) {
50
51
  const currLoggerName = parentKey ? `${name}.${parentKey}` : name;
@@ -65,6 +66,9 @@ function generateInterfaceASTNode(name, definitionsType, currProperties, definit
65
66
  const { type, $ref, properties, additionalProperties, items, enum: _enum, oneOf, allOf, anyOf, example, description, default: _default, } = property;
66
67
  type && logger(`[${currLoggerName}] <${key}> is ${type}`);
67
68
  // 当存在oneOf、allOf、anyOf时,需要特殊处理
69
+ // Known limitation: 当 someOf 项引用一个 record definition($ref 到 additionalProperties 型)时,
70
+ // 下面把 anchor.jsonData 塞到 properties 里可能产生 map 伪装形态。当前生产 swagger 里未使用
71
+ // someOf + record 组合,未修补。若未来触发,可参考 $ref 分支的 schema-aware 判定。
68
72
  if (oneOf || allOf || anyOf) {
69
73
  const someOfList = oneOf || allOf || anyOf || [];
70
74
  const someOfDataTypeFlag = [];
@@ -132,7 +136,17 @@ function generateInterfaceASTNode(name, definitionsType, currProperties, definit
132
136
  interfaceJSONData[key] = anchorPointItem.jsonData;
133
137
  }
134
138
  else {
135
- interfaceJSONData[key] = { type: 'object', properties: anchorPointItem.jsonData };
139
+ // anchor jsonData 有两种形态:
140
+ // A) interface 型:{ fieldA: {schema...}, ... } ← 需包成 { type: 'object', properties: ... }
141
+ // B) record/schema 型:{ type: 'object'|'array', additionalProperties|items|properties: ... } ← 已是合法 schema,直接透传
142
+ // 判定见 utils/is-schema-like.ts(要求 type 为字符串且含 additionalProperties/items/properties 之一)。
143
+ const anchorJSON = anchorPointItem.jsonData;
144
+ if ((0, is_schema_like_1.isSchemaLike)(anchorJSON)) {
145
+ interfaceJSONData[key] = anchorJSON;
146
+ }
147
+ else {
148
+ interfaceJSONData[key] = { type: 'object', properties: anchorJSON };
149
+ }
136
150
  }
137
151
  keywordTypeNode = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(anchorPointItem.exportName), undefined);
138
152
  // 定义Record
@@ -140,7 +154,13 @@ function generateInterfaceASTNode(name, definitionsType, currProperties, definit
140
154
  else if (additionalProperties) {
141
155
  const { recordContent, recordDependency, descriptionMap, recordJSON } = (0, generate_record_ast_node_1.generateRecordASTNode)(name, definitionsType, additionalProperties, definitions, defintionsMap);
142
156
  keywordTypeNode = recordContent;
143
- interfaceJSONData[key] = { type: 'object', properties: recordJSON };
157
+ // recordJSON 本身就是合法 map schema:
158
+ // { type: 'object', additionalProperties: {...} } 或
159
+ // { type: 'array', items: {...} }
160
+ // 直接透传即可;不要包成 { type: 'object', properties: recordJSON },
161
+ // 否则 additionalProperties/items 会被下游 CLI 生成器误认为具名子字段,
162
+ // 产生 --env-type / --env-additionalProperties 这样的幻影 flag。
163
+ interfaceJSONData[key] = recordJSON;
144
164
  // 合并依赖
145
165
  interfaceDependency = new Set([
146
166
  ...interfaceDependency,
@@ -61,7 +61,25 @@ function generateRecordASTNode(name, type, additionalProperties, swaggerDefiniti
61
61
  if (ref) {
62
62
  logger(`[${name}] is anchor point, $ref -> ${ref}`);
63
63
  const anchorPointItem = (0, generate_record_anchor_ast_node_1.generateRecordAnchorASTNode)(name, ref, swaggerDefinitions, defintionsMap, recordDependency);
64
- jsonData.properties = anchorPointItem.jsonData;
64
+ // 生成合法的 JSON Schema map 结构:
65
+ // 数组元素为 $ref → { type: 'array', items: { type: 'object', properties: {...} } }
66
+ // 对象值为 $ref → { type: 'object', additionalProperties: { type: 'object', properties: {...} } }
67
+ // 注意:不要把 anchor 的 jsonData 直接塞到父级 properties,会产生
68
+ // {type,additionalProperties} 被误解为具名子字段的伪装结构。
69
+ if (additionalProperties.type === constants_1.DefinitionsType.ARRAY) {
70
+ jsonData.type = 'array';
71
+ jsonData.items = {
72
+ type: 'object',
73
+ properties: anchorPointItem.jsonData,
74
+ };
75
+ }
76
+ else {
77
+ jsonData.type = 'object';
78
+ jsonData.additionalProperties = {
79
+ type: 'object',
80
+ properties: anchorPointItem.jsonData,
81
+ };
82
+ }
65
83
  const typeReferenceNode = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(anchorPointItem.exportName), undefined);
66
84
  recordContent = [
67
85
  ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.generateSkillCliHelp = generateSkillCliHelp;
7
7
  const debug_1 = __importDefault(require("debug"));
8
8
  const get_defintion_name_1 = require("../utils/get-defintion-name");
9
+ const is_schema_like_1 = require("../utils/is-schema-like");
9
10
  const logger = (0, debug_1.default)('csg:cli');
10
11
  function isBaseApiParamArrayData(schema) {
11
12
  return schema.type && schema.type === 'array';
@@ -106,14 +107,38 @@ function generateSkillCliHelp(requestMap, defintionsMap) {
106
107
  if ($ref) {
107
108
  const defintionName = (0, get_defintion_name_1.getDefinitionName)($ref);
108
109
  const defintion = defintionsMap[defintionName];
109
- if (item.schema.type === 'array') {
110
- item.schema.items = {
111
- type: 'object',
112
- properties: { ...defintion.jsonData },
113
- };
110
+ if (!defintion) {
111
+ // $ref 无法在 defintionsMap 中命中(可能是 stale/未生成的 definition);
112
+ // 保底:回退到原始 parameter.schema,避免访问 undefined.jsonData 崩溃。
113
+ logger(`[skip parameter schema] $ref not found in defintionsMap: ${$ref}`);
114
+ item.schema = parameter.schema;
114
115
  }
115
116
  else {
116
- item.schema.properties = { ...defintion.jsonData };
117
+ // definition.jsonData 有两种形态:
118
+ // A) interface 型:{ fieldA: {schema...}, fieldB: {...} } ← 键 = 字段名
119
+ // B) record/顶层 schema 型:{ type: 'object'|'array', additionalProperties|items|properties: ... }
120
+ // A 型应展开为 properties/items.properties;B 型是完整 schema,直接透传。
121
+ // 判定见 utils/is-schema-like.ts。
122
+ const jsonData = defintion.jsonData;
123
+ if ((0, is_schema_like_1.isSchemaLike)(jsonData)) {
124
+ // record / array / primitive schema 本身作为 body
125
+ if (item.schema.type === 'array') {
126
+ item.schema.items = { ...jsonData };
127
+ }
128
+ else {
129
+ // 直接用 schema 覆盖 item.schema(保留 type/additionalProperties/items 等)
130
+ item.schema = { ...jsonData };
131
+ }
132
+ }
133
+ else if (item.schema.type === 'array') {
134
+ item.schema.items = {
135
+ type: 'object',
136
+ properties: { ...jsonData },
137
+ };
138
+ }
139
+ else {
140
+ item.schema.properties = { ...jsonData };
141
+ }
117
142
  }
118
143
  }
119
144
  else {
@@ -158,14 +183,30 @@ function generateSkillCliHelp(requestMap, defintionsMap) {
158
183
  if ($ref) {
159
184
  const defintionName = (0, get_defintion_name_1.getDefinitionName)($ref);
160
185
  const defintion = defintionsMap[defintionName];
161
- if (item.schema.type === 'array') {
162
- item.schema.items = {
163
- type: 'object',
164
- properties: defintion.jsonData,
165
- };
186
+ if (!defintion) {
187
+ // $ref 未命中:跳过 schema 展开,保留 item.schema 已有的最小结构(type 已在上文赋值)。
188
+ logger(`[skip response schema] $ref not found in defintionsMap: ${$ref}`);
166
189
  }
167
190
  else {
168
- item.schema.properties = defintion.jsonData;
191
+ // 见上方(parameter 分支)注释:区分 interface 型 vs schema jsonData
192
+ const jsonData = defintion.jsonData;
193
+ if ((0, is_schema_like_1.isSchemaLike)(jsonData)) {
194
+ if (item.schema.type === 'array') {
195
+ item.schema.items = { ...jsonData };
196
+ }
197
+ else {
198
+ item.schema = { ...jsonData };
199
+ }
200
+ }
201
+ else if (item.schema.type === 'array') {
202
+ item.schema.items = {
203
+ type: 'object',
204
+ properties: jsonData,
205
+ };
206
+ }
207
+ else {
208
+ item.schema.properties = jsonData;
209
+ }
169
210
  }
170
211
  }
171
212
  else {
@@ -5,6 +5,7 @@ const trim_summary_1 = require("./trim-summary");
5
5
  const option_value_flag_1 = require("./option-value-flag");
6
6
  const is_nested_object_1 = require("./is-nested-object");
7
7
  const is_array_of_objects_1 = require("./is-array-of-objects");
8
+ const is_map_field_1 = require("./is-map-field");
8
9
  const flatten_array_object_options_1 = require("./flatten-array-object-options");
9
10
  /** 递归扁平化嵌套对象类型的子属性 */
10
11
  function flattenNestedObjectOptions(objectKey, prop, isRequired, usedKeys) {
@@ -21,6 +22,11 @@ function flattenNestedObjectOptions(objectKey, prop, isRequired, usedKeys) {
21
22
  options.push(...(0, flatten_array_object_options_1.flattenArrayObjectOptions)(flatKey, subProp, isRequired, usedKeys));
22
23
  continue;
23
24
  }
25
+ // 深层 map / 字典型:暂不为其生成 CLI flag(避免歧义),
26
+ // 让调用方走 `--data` fallback 传该字段。若未来需要,可扩展 KEY=VALUE 语义。
27
+ if ((0, is_map_field_1.isMapField)(subProp)) {
28
+ continue;
29
+ }
24
30
  const optKey = usedKeys.has(flatKey) ? `d-${flatKey}` : flatKey;
25
31
  const valuePlaceholder = (0, option_value_flag_1.optionValueFlag)(subProp.type || 'string');
26
32
  const rawDesc = (0, trim_summary_1.trimSummary)(subProp.description || '') || subKey;
@@ -6,6 +6,7 @@ const option_value_flag_1 = require("./option-value-flag");
6
6
  const clean_array_desc_1 = require("./clean-array-desc");
7
7
  const is_array_of_objects_1 = require("./is-array-of-objects");
8
8
  const is_nested_object_1 = require("./is-nested-object");
9
+ const is_map_field_1 = require("./is-map-field");
9
10
  const collect_used_keys_1 = require("./collect-used-keys");
10
11
  const flatten_array_object_options_1 = require("./flatten-array-object-options");
11
12
  const flatten_nested_object_options_1 = require("./flatten-nested-object-options");
@@ -72,6 +73,24 @@ function flattenToolOptions(toolInfo) {
72
73
  options.push(...(0, flatten_array_object_options_1.flattenArrayObjectOptions)(key, prop, isRequired, usedKeys));
73
74
  continue;
74
75
  }
76
+ // map / 字典型(swagger additionalProperties):注册为 `--<key> NAME=VALUE`
77
+ // 可多次传入,运行时组装成对象。
78
+ if ((0, is_map_field_1.isMapField)(prop)) {
79
+ const optKey = usedKeys.has(key) ? `d-${key}` : key;
80
+ const rawDesc = (0, trim_summary_1.trimSummary)(prop.description || '');
81
+ const hint = ' (KEY=VALUE 格式,可多次传入)';
82
+ const desc = isRequired ? `[必填] ${rawDesc}${hint}` : `${rawDesc}${hint}`;
83
+ options.push({
84
+ optKey,
85
+ valuePlaceholder: '<KEY=VALUE>',
86
+ description: desc,
87
+ required: !!isRequired,
88
+ isArray: true,
89
+ source: 'body',
90
+ isMapField: true,
91
+ });
92
+ continue;
93
+ }
75
94
  if ((0, is_nested_object_1.isNestedObject)(prop)) {
76
95
  options.push(...(0, flatten_nested_object_options_1.flattenNestedObjectOptions)(key, prop, isRequired, usedKeys));
77
96
  continue;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isMapField = isMapField;
4
+ exports.getMapValueType = getMapValueType;
5
+ /**
6
+ * 判断 body schema property 是否为 map / 字典类型(swagger `additionalProperties`)。
7
+ *
8
+ * 合法形态:
9
+ * { type: 'object', additionalProperties: { type: 'string' | 'number' | 'boolean' | 'any' } }
10
+ *
11
+ * 不同时具有具名 `properties` 才算 map;若同时具有 properties 则视为混合结构,
12
+ * 按嵌套对象处理,避免行为歧义。
13
+ *
14
+ * 注意:仅当 map value 是 primitive(string/number/boolean/integer/any)时才当作
15
+ * "可通过 `--<key> KEY=VALUE` 传入" 的 map;若 value 是 $ref 或嵌套 object/array,
16
+ * `KEY=VALUE` 语义无法表达复杂结构,此时不视为 CLI 层面的 map 字段(回退到 --data)。
17
+ *
18
+ * ⚠️ 调用顺序约束:必须先于 `isNestedObject` 调用(见 flatten-tool-options.ts /
19
+ * flatten-nested-object-options.ts)。原因:若 help.json 中存在「additionalProperties
20
+ * 被误塞进 properties」的伪装形态,`isNestedObject` 虽然通过 `looksLikeAdditionalPropertiesMisnested`
21
+ * 做了防御,但 `isMapField` 对这种伪装无法识别(此时 `prop.properties` 非空、
22
+ * `prop.additionalProperties` 缺失)。当前生成器已修复该伪装,但调用顺序仍作为
23
+ * 二次防御依赖被保留,反转顺序会导致伪装数据被错误分类。
24
+ */
25
+ function isMapField(prop) {
26
+ if (!prop || prop.type !== 'object')
27
+ return false;
28
+ const ap = prop.additionalProperties;
29
+ if (!ap || typeof ap !== 'object')
30
+ return false;
31
+ // 有具名 properties 时不视为 map(保留嵌套对象语义)
32
+ if (prop.properties && Object.keys(prop.properties).length > 0)
33
+ return false;
34
+ // value 是 $ref / 嵌套 object 结构:CLI 层无法用 KEY=VAL 表达
35
+ if (ap.$ref)
36
+ return false;
37
+ if (ap.properties && Object.keys(ap.properties).length > 0)
38
+ return false;
39
+ return true;
40
+ }
41
+ /**
42
+ * 获取 map 型字段的 value 类型(默认 string)。
43
+ */
44
+ function getMapValueType(prop) {
45
+ var _a;
46
+ const t = (_a = prop === null || prop === void 0 ? void 0 : prop.additionalProperties) === null || _a === void 0 ? void 0 : _a.type;
47
+ return typeof t === 'string' ? t : 'string';
48
+ }
@@ -1,9 +1,58 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isNestedObject = isNestedObject;
4
- /** 判断 body 属性是否为嵌套对象类型 */
4
+ /**
5
+ * 判断 body 属性是否为「真正的嵌套对象」(有具名子字段),
6
+ * 用于决定要不要把子字段扁平化成独立的 CLI 选项。
7
+ *
8
+ * 排除情况:map/字典型(swagger `additionalProperties`)。
9
+ * 某些 help.json 生成器会把 `additionalProperties` 误塞进 `properties`,
10
+ * 产生非法伪装结构,例如:
11
+ * {
12
+ * "type": "object",
13
+ * "properties": {
14
+ * "type": "object", // 字面量值,不是 schema
15
+ * "additionalProperties": { "type": "string" }
16
+ * }
17
+ * }
18
+ * 这种情况下 `env` 本质是 `map[string]string`,不应展开出
19
+ * `--env-type` / `--env-additionalProperties` 这样的幻影 flag。
20
+ *
21
+ * ⚠️ 调用顺序约束:应在 `isMapField` 之后调用(见 flatten-tool-options.ts /
22
+ * flatten-nested-object-options.ts)。合法 map 字段(无 properties + 有 additionalProperties)
23
+ * 会被 `isMapField` 先行捕获;本函数只兜底伪装形态。反转顺序会导致合法 map 被误当作嵌套对象展开。
24
+ */
5
25
  function isNestedObject(prop) {
6
- if (prop.type !== 'object')
26
+ if (!prop || prop.type !== 'object')
7
27
  return false;
8
- return !!(prop.properties && Object.keys(prop.properties).length > 0);
28
+ // 显式声明为字典型(`additionalProperties` schema 对象且无具名 properties):直接排除
29
+ // 注意:`additionalProperties: true|false` 是合法 JSON Schema 布尔值(表示是否允许额外属性),
30
+ // 不构成"字典型"语义,这里只对 object 值排除。
31
+ if (prop.additionalProperties
32
+ && typeof prop.additionalProperties === 'object'
33
+ && (!prop.properties || Object.keys(prop.properties).length === 0)) {
34
+ return false;
35
+ }
36
+ if (!prop.properties || Object.keys(prop.properties).length === 0)
37
+ return false;
38
+ // 启发式:合法 properties 里每个值都应该是 schema 对象(object 类型)。
39
+ // 若出现字面量值(如 "type": "object"),说明是被误塞进来的 additionalProperties 结构。
40
+ if (looksLikeAdditionalPropertiesMisnested(prop.properties))
41
+ return false;
42
+ return true;
43
+ }
44
+ /**
45
+ * 识别「additionalProperties 被错误嵌入到 properties」的伪装结构。
46
+ * 判定条件(同时满足即认为是伪装):
47
+ * 1) properties 中存在字面量 `type` 键(值不是对象)
48
+ * 2) properties 中存在 `additionalProperties` 键(值是对象)
49
+ * 这种组合在合法 JSON Schema 里不可能出现(子字段名不会是 "type"/"additionalProperties"
50
+ * 且值同时又是这种形态),因此可以稳妥地识别为伪装。
51
+ */
52
+ function looksLikeAdditionalPropertiesMisnested(properties) {
53
+ const typeVal = properties.type;
54
+ const addVal = properties.additionalProperties;
55
+ const typeIsLiteral = typeof typeVal === 'string';
56
+ const addIsObject = addVal !== null && typeof addVal === 'object';
57
+ return typeIsLiteral && addIsObject;
9
58
  }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSchemaLike = isSchemaLike;
4
+ /**
5
+ * 判断 codegen 中间产物 `jsonData` 是「schema 本身」还是「interface 型字段映射」。
6
+ *
7
+ * 背景:在 codegen 过程中,anchor / $ref 指向的 definition 的 `jsonData` 存在两种形态:
8
+ *
9
+ * A) interface 型:`{ fieldA: {schema...}, fieldB: {schema...}, ... }`
10
+ * - 键为字段名,值为该字段的 schema 对象
11
+ * - 顶层不含字面量 `type: 'object' | 'array'` 键
12
+ *
13
+ * B) record / schema 型:`{ type: 'object' | 'array', additionalProperties?|items?|properties?: ... }`
14
+ * - 本身就是一个完整合法的 JSON Schema
15
+ * - 顶层 `type` 键为字符串字面量
16
+ *
17
+ * 消费侧(如 generate-interface-ast-node、generate-skill-cli-help)需要区分二者:
18
+ * - A 型需包成 `{ type: 'object', properties: jsonData }`
19
+ * - B 型可直接透传
20
+ *
21
+ * 判定策略(严格):
22
+ * 同时满足以下条件才视为 B 型:
23
+ * 1) `type` 为字符串('object' / 'array' / 'string' / ...)
24
+ * 2) 至少含有 `additionalProperties` / `items` / `properties` 之一
25
+ * (避免"某个 interface 恰好有一个 string 类型的 type 字段"被误判为 B 型)
26
+ *
27
+ * 保守起见:不满足严格条件时按 A 型处理(interface 型),走 `{ type: 'object', properties: ... }` 包装,
28
+ * 与旧行为兼容。
29
+ */
30
+ function isSchemaLike(jsonData) {
31
+ if (!jsonData || typeof jsonData !== 'object')
32
+ return false;
33
+ if (typeof jsonData.type !== 'string')
34
+ return false;
35
+ return (jsonData.additionalProperties !== undefined
36
+ || jsonData.items !== undefined
37
+ || jsonData.properties !== undefined);
38
+ }
@@ -5,6 +5,8 @@ import { buildNestedFieldMap } from '../utils/build-nested-field-map';
5
5
  import type { NestedFieldMapping } from '../utils/build-nested-field-map';
6
6
  import { restoreOriginalKeys } from '../utils/restore-original-keys';
7
7
  import { matchArrayObjectField } from '../utils/match-array-object-field';
8
+ import { isNestedObjectSchema } from '../utils/is-nested-object-schema';
9
+ import { isMapField, getMapValueType } from '../utils/is-map-field';
8
10
 
9
11
  /**
10
12
  * 获取 tool 的参数定义(用于自动分发 --key value 到 path/query)
@@ -36,6 +38,20 @@ function filterIllegalChars(str: string): string {
36
38
  return str.replace(/\x00/g, '');
37
39
  }
38
40
 
41
+ /**
42
+ * 将不受信任的字符串安全地转成日志片段:
43
+ * - 转义 C0/DEL 控制字符(\r \n \t 及 ANSI 转义序列等),避免污染终端显示
44
+ * - 截断到 80 字符,防止超长 item 刷屏
45
+ * 用于 console.warn 中拼接用户输入片段的场景。
46
+ */
47
+ function sanitizeForLog(str: string): string {
48
+ const escaped = str.replace(/[\x00-\x1f\x7f]/g, (ch) => {
49
+ const code = ch.charCodeAt(0).toString(16).padStart(2, '0');
50
+ return `\\x${code}`;
51
+ });
52
+ return escaped.length > 80 ? `${escaped.slice(0, 80)}…` : escaped;
53
+ }
54
+
39
55
  function assignBodyValue(
40
56
  target: Record<string, any>,
41
57
  key: string,
@@ -51,6 +67,72 @@ function assignBodyValue(
51
67
  }
52
68
  }
53
69
 
70
+ /**
71
+ * 将 `--<key> NAME=VALUE`(可多次)收集到的字符串数组解析为对象,写入 body。
72
+ * 用法示例:`--env FOO=bar --env BAZ=qux` → `{ env: { FOO: 'bar', BAZ: 'qux' } }`
73
+ *
74
+ * value 侧类型转换(依据 additionalProperties.type):
75
+ * - number:Number(v),NaN 时回退到原字符串(避免误吞非数字输入)
76
+ * - boolean:仅 "true"/"false" 严格匹配转换,其他保留字符串
77
+ * - 其他:保留原字符串(对 string 型做一次 shell 反解,与 assignBodyValue 一致)
78
+ *
79
+ * 兼容:单次传入时 commander 也可能给到字符串(isArray 会写数组,这里保底)。
80
+ */
81
+ function assignMapField(
82
+ target: Record<string, any>,
83
+ key: string,
84
+ value: any,
85
+ valueType: string,
86
+ ): void {
87
+ const items = Array.isArray(value) ? value : [value];
88
+ const map: Record<string, any> = target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
89
+ ? target[key]
90
+ : {};
91
+
92
+ for (const item of items) {
93
+ if (typeof item !== 'string') continue;
94
+ const eq = item.indexOf('=');
95
+ if (eq <= 0) {
96
+ // 非 KEY=VALUE 形态:跳过并给个 stderr 提示,避免静默丢数据
97
+ // eslint-disable-next-line no-console
98
+ console.warn(`[cnb] --${key} 需要 KEY=VALUE 格式,已忽略: ${sanitizeForLog(item)}`);
99
+ continue;
100
+ }
101
+ const k = item.slice(0, eq);
102
+ // KEY 合法性校验:拒绝空、纯空白、含控制字符、以及前后带空白的 key。
103
+ // 若前后有空白(如 ' A '),trim 后写入会与 'A' 静默合并,非用户预期;直接拒绝。
104
+ // 与 value 侧的 filterIllegalChars 保持对称防御。
105
+ if (!k || k !== k.trim() || /[\x00-\x1f\x7f]/.test(k)) {
106
+ // eslint-disable-next-line no-console
107
+ console.warn(`[cnb] --${key} KEY 非法,已忽略: ${sanitizeForLog(item)}`);
108
+ continue;
109
+ }
110
+ const rawV = item.slice(eq + 1);
111
+ let v: any = rawV;
112
+ if (valueType === 'number') {
113
+ // 空字符串或非数字:保留原字符串(避免 Number('') === 0 的静默转换)
114
+ if (rawV.length === 0) {
115
+ v = rawV;
116
+ } else {
117
+ const n = Number(rawV);
118
+ v = Number.isNaN(n) ? rawV : n;
119
+ }
120
+ } else if (valueType === 'boolean') {
121
+ if (rawV === 'true') v = true;
122
+ else if (rawV === 'false') v = false;
123
+ } else {
124
+ // 与 assignBodyValue 不同:map value 不做 removeSlashes 反解。
125
+ // 原因:环境变量常见值(URL / Windows 路径 / JWT 等)中的反斜杠会被
126
+ // removeSlashes 无损伤规则误吞(如 "C:\path" → "C:path")。
127
+ // shell 侧的转义($VAR / 引号)用户已经掌控;这里仅过滤非法 NUL 字符。
128
+ v = filterIllegalChars(rawV);
129
+ }
130
+ map[k] = v;
131
+ }
132
+
133
+ target[key] = map;
134
+ }
135
+
54
136
  /**
55
137
  * 格式化参数
56
138
  * 支持新的 --key value 扁平格式,同时向后兼容旧的 --path/--query JSON 格式。
@@ -132,10 +214,14 @@ export function formatParams(
132
214
  // 收集嵌套对象展开字段的临时存储(深层嵌套结构):{ rootObjectKey: { nested: { path: value } } }
133
215
  const nestedObjectCollector: Record<string, Record<string, any>> = {};
134
216
 
217
+ // 记录已被 `--<key> KEY=VAL` flag 处理过的 map 字段名,
218
+ // 用于在 --data fallback 合并时对同名 map 字段做 shallow merge(避免 flag 覆盖 --data 的整个 map)。
219
+ const mapFieldKeysUsed = new Set<string>();
220
+
135
221
  // 预建所有嵌套对象字段的 camelCased flat key → pathKeys 映射表
136
222
  const nestedFieldMap = new Map<string, NestedFieldMapping>();
137
223
  for (const [rootKey, rootProp] of Object.entries(bodyProps) as [string, any][]) {
138
- if (rootProp.type === 'object' && rootProp.properties && Object.keys(rootProp.properties).length > 0) {
224
+ if (isNestedObjectSchema(rootProp)) {
139
225
  const subMap = buildNestedFieldMap(rootProp.properties, rootKey, [rootKey]);
140
226
  for (const [k, v] of subMap) {
141
227
  nestedFieldMap.set(k, v);
@@ -226,7 +312,12 @@ export function formatParams(
226
312
  } else if (bodyProps[key]) {
227
313
  // body 字段(无冲突,直接用原 key)
228
314
  if (!formatted.data) formatted.data = {};
229
- assignBodyValue(formatted.data, key, value, bodyProps[key].type);
315
+ if (isMapField(bodyProps[key])) {
316
+ assignMapField(formatted.data, key, value, getMapValueType(bodyProps[key]));
317
+ mapFieldKeysUsed.add(key);
318
+ } else {
319
+ assignBodyValue(formatted.data, key, value, bodyProps[key].type);
320
+ }
230
321
  } else {
231
322
  // 处理 d- 前缀(冲突时 CLI 以 d- 前缀传入)
232
323
  const stripped = (key.startsWith('d-') || key.startsWith('d_'))
@@ -276,7 +367,12 @@ export function formatParams(
276
367
  const originalKey = key.replace(/^d[-_]/, '');
277
368
  if (bodyProps[originalKey]) {
278
369
  if (!formatted.data) formatted.data = {};
279
- assignBodyValue(formatted.data, originalKey, value, bodyProps[originalKey].type);
370
+ if (isMapField(bodyProps[originalKey])) {
371
+ assignMapField(formatted.data, originalKey, value, getMapValueType(bodyProps[originalKey]));
372
+ mapFieldKeysUsed.add(originalKey);
373
+ } else {
374
+ assignBodyValue(formatted.data, originalKey, value, bodyProps[originalKey].type);
375
+ }
280
376
  } else {
281
377
  if (!formatted.query) formatted.query = {};
282
378
  formatted.query[key] = typeof value === 'string' && !isNaN(Number(value)) ? Number(value) : value;
@@ -322,10 +418,23 @@ export function formatParams(
322
418
  // 合并 --data 提供的 body:
323
419
  // - 数组 body:整体替换 formatted.data(数组 body 接口没有逐字段 flag,直接覆盖)
324
420
  // - 对象 body:作为 base,逐个 body flag(如 --event)已写入 formatted.data,flag 优先
421
+ // 特殊:对 map 型字段(`--<key> KEY=VAL` flag 处理过的),做 shallow merge,
422
+ // 即 { ...dataBaseMap, ...flagMap },避免 flag 传的单条 kv 覆盖 --data 中提供的整块 map。
325
423
  if (dataArrayFromFlag) {
326
424
  formatted.data = dataArrayFromFlag;
327
425
  } else if (dataBaseFromFlag) {
328
- formatted.data = { ...dataBaseFromFlag, ...(formatted.data || {}) };
426
+ const mergedMapFields: Record<string, any> = {};
427
+ for (const mapKey of mapFieldKeysUsed) {
428
+ const baseVal = dataBaseFromFlag[mapKey];
429
+ const flagVal = (formatted.data || {})[mapKey];
430
+ // 显式判 `!= null`(同时排除 null 和 undefined),避免脏 --data 传入 null 时
431
+ // 短路失效的隐患;`Array.isArray` 兜底避免把数组当对象 merge。
432
+ if (baseVal != null && typeof baseVal === 'object' && !Array.isArray(baseVal)
433
+ && flagVal != null && typeof flagVal === 'object' && !Array.isArray(flagVal)) {
434
+ mergedMapFields[mapKey] = { ...baseVal, ...flagVal };
435
+ }
436
+ }
437
+ formatted.data = { ...dataBaseFromFlag, ...(formatted.data || {}), ...mergedMapFields };
329
438
  }
330
439
 
331
440
  // --body-file 优先级最高:覆盖 --body 逐字段值与 --data 中的 body,
@@ -1,3 +1,6 @@
1
+ import { isNestedObjectSchema } from './is-nested-object-schema';
2
+ import { isMapField } from './is-map-field';
3
+
1
4
  export interface NestedFieldMapping {
2
5
  pathKeys: string[]; // schema 路径,如 ["testBody", "critical_count", "id"]
3
6
  leafSchema: any; // 叶子节点的 schema 定义
@@ -21,12 +24,17 @@ export function buildNestedFieldMap(
21
24
  const flatKey = `${prefix}@${subKey}`;
22
25
  const currentPath = [...pathKeys, subKey];
23
26
 
24
- if (subProp.type === 'object' && subProp.properties && Object.keys(subProp.properties).length > 0) {
27
+ if (isNestedObjectSchema(subProp)) {
25
28
  // 嵌套对象:继续递归
26
29
  const subMap = buildNestedFieldMap(subProp.properties, flatKey, currentPath);
27
30
  for (const [k, v] of subMap) {
28
31
  map.set(k, v);
29
32
  }
33
+ } else if (isMapField(subProp)) {
34
+ // 深层 map 字段:不作为叶子注册(CLI 层暂不为深层 map 生成 flag,
35
+ // 与 src/utils/flatten-nested-object-options.ts 保持一致)。
36
+ // 用户如需传值,应通过 --data 传整体 JSON。
37
+ continue;
30
38
  } else {
31
39
  // 叶子节点:注册 flat key
32
40
  const camelKey = flatKey;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 运行时判断 body schema property 是否为 map / 字典类型(`additionalProperties`)。
3
+ * 与 src/utils/is-map-field.ts 保持一致。
4
+ */
5
+ export function isMapField(prop: any): boolean {
6
+ if (!prop || prop.type !== 'object') return false;
7
+ const ap = prop.additionalProperties;
8
+ if (!ap || typeof ap !== 'object') return false;
9
+ if (prop.properties && Object.keys(prop.properties).length > 0) return false;
10
+ if (ap.$ref) return false;
11
+ if (ap.properties && Object.keys(ap.properties).length > 0) return false;
12
+ return true;
13
+ }
14
+
15
+ export function getMapValueType(prop: any): string {
16
+ const t = prop?.additionalProperties?.type;
17
+ return typeof t === 'string' ? t : 'string';
18
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 判断 schema 属性是否为「真正的嵌套对象」(有具名子字段)。
3
+ * 用于运行期决定是否要为该字段构建 nestedFieldMap。
4
+ *
5
+ * 与 `src/utils/is-nested-object.ts` 保持一致的判定规则:
6
+ * 排除 map/字典型结构(swagger `additionalProperties`),
7
+ * 包括 help.json 生成器把 `additionalProperties` 误塞进 `properties`
8
+ * 的伪装形态(会产生非法子字段 `type` / `additionalProperties`)。
9
+ */
10
+ export function isNestedObjectSchema(prop: any): boolean {
11
+ if (!prop || prop.type !== 'object') return false;
12
+
13
+ // 显式字典型(additionalProperties 是 schema 对象且无具名 properties)
14
+ if (
15
+ prop.additionalProperties
16
+ && typeof prop.additionalProperties === 'object'
17
+ && (!prop.properties || Object.keys(prop.properties).length === 0)
18
+ ) {
19
+ return false;
20
+ }
21
+
22
+ if (!prop.properties || Object.keys(prop.properties).length === 0) return false;
23
+
24
+ if (looksLikeAdditionalPropertiesMisnested(prop.properties)) return false;
25
+
26
+ return true;
27
+ }
28
+
29
+ function looksLikeAdditionalPropertiesMisnested(properties: Record<string, any>): boolean {
30
+ const typeVal = properties.type;
31
+ const addVal = properties.additionalProperties;
32
+ const typeIsLiteral = typeof typeVal === 'string';
33
+ const addIsObject = addVal !== null && typeof addVal === 'object';
34
+ return typeIsLiteral && addIsObject;
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "2.11.8",
3
+ "version": "2.12.0",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -24,6 +24,9 @@
24
24
  "lint:fix": "eslint src --fix",
25
25
  "test": "jest",
26
26
  "test:cover": "jest --coverage",
27
+ "test:cli": "node scripts/cli-smoke-test.js",
28
+ "test:cli:quick": "node scripts/cli-smoke-test.js --skip-gen",
29
+ "test:cli:live": "node scripts/cli-smoke-test.js --skip-gen --live",
27
30
  "gen:test": "npx swagger-typescript-api generate --path ./template/swagger.json --axios --extract-request-body --extract-response-body --extract-request-params --extract-response-error -o ./test"
28
31
  },
29
32
  "dependencies": {
package/built/test.js DELETED
@@ -1 +0,0 @@
1
- "use strict";