@cnbcool/cnb-api-generate 2.8.0 → 2.8.2

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.
@@ -79,18 +79,14 @@ function flattenToolOptions(toolInfo) {
79
79
  const optKey = usedKeys.has(key) ? `d-${key}` : key;
80
80
  const isArray = prop.type === 'array';
81
81
  const itemType = isArray ? ((_c = prop.items) === null || _c === void 0 ? void 0 : _c.type) || 'string' : prop.type;
82
- const supportsFileRef = !isArray && prop.type === 'string' && key === 'body';
83
- const valuePlaceholder = supportsFileRef
84
- ? '<string|@file>'
85
- : (0, option_value_flag_1.optionValueFlag)(itemType);
82
+ const valuePlaceholder = (0, option_value_flag_1.optionValueFlag)(itemType);
86
83
  const rawDesc = isArray
87
84
  ? (0, clean_array_desc_1.cleanArrayDesc)((0, trim_summary_1.trimSummary)(prop.description || ''))
88
85
  : (0, trim_summary_1.trimSummary)(prop.description || '');
89
86
  const arrayHint = isArray ? ' (可多次传入)' : '';
90
- const fileRefHint = supportsFileRef ? ' (支持 @file 引用)' : '';
91
87
  const desc = isRequired
92
- ? `[必填] ${rawDesc}${arrayHint}${fileRefHint}`
93
- : `${rawDesc}${arrayHint}${fileRefHint}`;
88
+ ? `[必填] ${rawDesc}${arrayHint}`
89
+ : `${rawDesc}${arrayHint}`;
94
90
  const opt = {
95
91
  optKey,
96
92
  valuePlaceholder,
@@ -101,13 +97,26 @@ function flattenToolOptions(toolInfo) {
101
97
  };
102
98
  options.push(opt);
103
99
  }
100
+ // 若 body 中存在 string 类型的 `body` 字段,提供独立的 --body-file 选项,
101
+ // 从指定文件读取内容作为正文(适合多行/长文本,避免 shell 参数长度限制)。
102
+ const bodyProp = bodyDef.schema.properties.body;
103
+ if (bodyProp && bodyProp.type === 'string') {
104
+ options.push({
105
+ optKey: 'body-file',
106
+ valuePlaceholder: '<path>',
107
+ description: '从文件读取内容作为 body 正文',
108
+ required: false,
109
+ isArray: false,
110
+ source: 'body',
111
+ });
112
+ }
104
113
  }
105
114
  // body 存在时添加 --data fallback
106
115
  if (bodyDef) {
107
116
  options.push({
108
117
  optKey: 'data',
109
118
  valuePlaceholder: '<json>',
110
- description: 'Request body (JSON 字符串或 @file 引用,可替代逐个 body 字段)',
119
+ description: 'Request body (JSON 字符串,可替代逐个 body 字段)',
111
120
  required: false,
112
121
  isArray: false,
113
122
  source: 'body',
@@ -1,5 +1,6 @@
1
+ import { removeSlashes } from 'slashes';
1
2
  import { helpData } from './help-data';
2
- import { tryParseJSON, tryReadFileRef } from './parsers';
3
+ import { tryParseJSON, readFileContent } from './parsers';
3
4
  import { buildNestedFieldMap } from '../utils/build-nested-field-map';
4
5
  import type { NestedFieldMapping } from '../utils/build-nested-field-map';
5
6
  import { restoreOriginalKeys } from '../utils/restore-original-keys';
@@ -16,12 +17,14 @@ export function getToolParamDefs(moduleName: string, toolName: string) {
16
17
 
17
18
  /**
18
19
  * 将 body 字段值按 schema 类型写入 target。
19
- *
20
- * 行为约定(无冲突分支与 d- 前缀分支共用):
21
20
  * - array 值:原样保留
22
- * - string 字段:保留原始字符串,并支持 @file 语法(避免纯数字字符串被转 number、
23
- * 避免长文本因 shell 参数限制被截断)
21
+ * - string 字段:还原 shell 字面转义(如 --body "a\n b" 中的 \n → 真换行),
22
+ * 并保留原始字符串(避免纯数字字符串被转 number)
24
23
  * - 其他字段:走 tryParseJSON 以兼容嵌套 JSON
24
+ *
25
+ * 说明:shell 在单/双引号下不解释 `\n`,外部程序(包括本 CLI 经 commander)
26
+ * 收到的是字面的反斜杠 + n。这里用 slashes.removeSlashes 做一次反解析,
27
+ * 让 `--body "a\n b"` 与 `echo -e` 体验一致,正确还原为真实换行/制表符等。
25
28
  */
26
29
  function assignBodyValue(
27
30
  target: Record<string, any>,
@@ -32,7 +35,7 @@ function assignBodyValue(
32
35
  if (Array.isArray(value)) {
33
36
  target[key] = value;
34
37
  } else if (propType === 'string') {
35
- target[key] = tryReadFileRef(value as string);
38
+ target[key] = typeof value === 'string' ? removeSlashes(value) : value;
36
39
  } else {
37
40
  target[key] = tryParseJSON(value as string);
38
41
  }
@@ -75,8 +78,20 @@ export function formatParams(
75
78
  'module', 'tool', 'help', 'short', 'verbose', 'h', 'v',
76
79
  ]);
77
80
 
81
+ // --body-file: 从指定文件读取内容作为 body 字段正文(适合多行/长文本)。
82
+ // 仅当 tool 存在 string 类型的 body 字段时,flatten-tool-options 才注册该选项。
83
+ // restoreOriginalKeys 已把 commander 的 camelCase(bodyFile)还原为 body-file,这里兼容两种形态。
84
+ // 读取到内容后在最后统一写入 formatted.data.body(优先级最高)。
85
+ reservedKeys.add('body-file');
86
+ reservedKeys.add('bodyFile');
87
+ let bodyFromFile: string | undefined;
88
+ const bodyFileRef = (params['body-file'] ?? (params as any).bodyFile);
89
+ if (typeof bodyFileRef === 'string' && bodyFileRef.length > 0) {
90
+ bodyFromFile = readFileContent(bodyFileRef);
91
+ }
92
+
78
93
  // --data 是 CLI 通用 fallback 选项(见 flatten-tool-options.ts,仅在 tool 有 body 时注册),
79
- // 接收 JSON 字符串(或 @file 引用)作为 body 整体。
94
+ // 接收 JSON 字符串作为 body 整体。
80
95
  // 仅当 'data' 不是该 tool 真实的 path/query/body 字段名时才启用 fallback 语义,
81
96
  // 否则按 swagger 定义当业务字段处理(保留 commit 120d557 引入的能力)。
82
97
  //
@@ -92,7 +107,7 @@ export function formatParams(
92
107
  if (dataIsFallback) {
93
108
  reservedKeys.add('data');
94
109
  if (typeof params.data === 'string' && params.data.length > 0) {
95
- const parsed = tryParseJSON(tryReadFileRef(params.data));
110
+ const parsed = tryParseJSON(params.data);
96
111
  if (bodyIsArrayType && Array.isArray(parsed)) {
97
112
  dataArrayFromFlag = parsed;
98
113
  } else if (!bodyIsArrayType && parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
@@ -302,6 +317,13 @@ export function formatParams(
302
317
  } else if (dataBaseFromFlag) {
303
318
  formatted.data = { ...dataBaseFromFlag, ...(formatted.data || {}) };
304
319
  }
320
+
321
+ // --body-file 优先级最高:覆盖 --body 逐字段值与 --data 中的 body,
322
+ // 确保「从文件读取正文」的显式意图最终生效。
323
+ if (bodyFromFile !== undefined) {
324
+ if (!formatted.data) formatted.data = {};
325
+ formatted.data.body = bodyFromFile;
326
+ }
305
327
  }
306
328
 
307
329
  // formatted.query 仅在用户真传了 query 值时才存在;toolFunction 第 1 形参形态
@@ -21,19 +21,12 @@ export function tryParseJSON(str: string | boolean | undefined): any {
21
21
  }
22
22
 
23
23
  /**
24
- * 尝试从文件引用或 stdin 读取内容(类似 curl 的 @file / @- 语法)
25
- *
26
- * 注意:一旦用户显式使用 @ 前缀,就表示其意图是读取文件/stdin。
27
- * 此时若文件不存在或读取失败,应直接报错退出,避免把 "@/path/xxx"
28
- * 字面量当作正文发送出去导致数据错误。
24
+ * 从指定路径读取文件内容(用于 --body-file)。
25
+ * 支持 `-` 表示从 stdin 读取,读取失败直接报错退出。
29
26
  */
30
- export function tryReadFileRef(str: string | boolean | undefined) {
31
- if (typeof str !== 'string' || !str.startsWith('@')) return str;
32
-
33
- const ref = str.slice(1);
34
-
35
- // @- 表示从 stdin 读取
36
- if (ref === '-') {
27
+ export function readFileContent(filePath: string): string {
28
+ // - 表示从 stdin 读取
29
+ if (filePath === '-') {
37
30
  try {
38
31
  return fs.readFileSync(0, 'utf8').trim();
39
32
  } catch (e) {
@@ -43,17 +36,15 @@ export function tryReadFileRef(str: string | boolean | undefined) {
43
36
  }
44
37
  }
45
38
 
46
- // @/path/to/file 表示从文件读取
47
- // 直接 readFileSync 并用 errno 区分错误,避免 existsSync + readFileSync 的 TOCTOU 竞态
48
39
  try {
49
- return fs.readFileSync(ref, 'utf8').trim();
40
+ return fs.readFileSync(filePath, 'utf8').trim();
50
41
  } catch (e) {
51
42
  const { code } = e as NodeJS.ErrnoException;
52
43
  if (code === 'ENOENT') {
53
- console.error(`文件不存在: ${ref}`);
44
+ console.error(`文件不存在: ${filePath}`);
54
45
  } else {
55
46
  const msg = e instanceof Error ? e.message : String(e);
56
- console.error(`读取文件失败: ${ref} - ${msg}`);
47
+ console.error(`读取文件失败: ${filePath} - ${msg}`);
57
48
  }
58
49
  process.exit(1);
59
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -45,6 +45,7 @@
45
45
  "ora": "5.4.1",
46
46
  "prettier": "3.4.2",
47
47
  "rimraf": "6.0.1",
48
+ "slashes": "^3.0.12",
48
49
  "typescript": "5.9.3"
49
50
  },
50
51
  "devDependencies": {