@cnbcool/cnb-api-generate 1.0.2 → 1.1.1

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.
@@ -10,9 +10,45 @@ const logger = (0, debug_1.default)('csg:cli');
10
10
  function isBaseApiParamArrayData(schema) {
11
11
  return schema.type && schema.type === 'array';
12
12
  }
13
+ /**
14
+ * 去除 summary/description 中的英文部分
15
+ * 如 "查询仓库的 Issues。List issues." → "查询仓库的 Issues"
16
+ * 纯英文文本不匹配时返回原文,不会丢失信息
17
+ */
18
+ function trimEnglish(text) {
19
+ if (!text)
20
+ return '';
21
+ // 匹配中文句号"。"后面跟英文的模式
22
+ const match = text.match(/^(.*?[\u4e00-\u9fff].*?)[。.]\s*[A-Z]/);
23
+ if (match)
24
+ return match[1];
25
+ return text;
26
+ }
27
+ /**
28
+ * 从 description 中提取权限字符串
29
+ * 匹配格式如 "repo-issue:r"、"repo-code:rw"、"group-manage:rw"
30
+ * @param description 原始 description 文本
31
+ * @returns 逗号分隔的权限字符串,如 "repo-manage:rw,repo-code:rw"
32
+ */
33
+ function extractPermission(description) {
34
+ if (!description)
35
+ return '';
36
+ const match = description.match(/([\w-]+:rw?)/g);
37
+ if (match)
38
+ return match.join(',');
39
+ return '';
40
+ }
41
+ /**
42
+ * 生成 skills CLI 的 help.json 数据
43
+ *
44
+ * 与旧版的差异:
45
+ * - summary/description 去除英文部分(节省 token)
46
+ * - 参数 description 同样去除英文
47
+ * - 新增 permission 字段(从 description 中提取权限字符串)
48
+ * - response schema 仍保留在 help.json 中,但 CLI 的 --help 输出不展示
49
+ */
13
50
  function generateSkillCliHelp(requestMap, defintionsMap) {
14
51
  var _a, _b, _c;
15
- // 按照分类分为总help和每个模块的help
16
52
  const mainHelp = {};
17
53
  const modulesHelp = {};
18
54
  for (const [path, pathItem] of Object.entries(requestMap)) {
@@ -23,17 +59,20 @@ function generateSkillCliHelp(requestMap, defintionsMap) {
23
59
  if (!modulesHelp[methodItem.category]) {
24
60
  modulesHelp[methodItem.category] = {};
25
61
  }
62
+ const rawSummary = (methodItem.summary || '')
63
+ .replaceAll('\n', '')
64
+ .replaceAll(' *', '');
65
+ const rawDescription = (methodItem.description || '')
66
+ .replaceAll('\n', '')
67
+ .replaceAll(' *', '');
26
68
  modulesHelp[methodItem.category][methodItem.filename] = {
27
69
  path: methodItem.path,
28
70
  method: methodItem.method,
29
71
  category: methodItem.category,
30
72
  filename: methodItem.filename,
31
- summary: (methodItem.summary || '')
32
- .replaceAll('\n', '')
33
- .replaceAll(' *', ''),
34
- description: (methodItem.description || '')
35
- .replaceAll('\n', '')
36
- .replaceAll(' *', ''),
73
+ summary: trimEnglish(rawSummary) || rawSummary,
74
+ description: rawDescription,
75
+ permission: extractPermission(rawDescription),
37
76
  help: {
38
77
  parameter: {},
39
78
  response: {},
@@ -46,7 +85,7 @@ function generateSkillCliHelp(requestMap, defintionsMap) {
46
85
  const item = {
47
86
  type: parameter.type === 'integer' ? 'number' : parameter.type,
48
87
  default: parameter.default,
49
- description: parameter.description,
88
+ description: trimEnglish(parameter.description || '') || parameter.description,
50
89
  name: parameter.name,
51
90
  required: parameter.required || false,
52
91
  enum: parameter.enum,
@@ -76,7 +115,6 @@ function generateSkillCliHelp(requestMap, defintionsMap) {
76
115
  else {
77
116
  item.schema.properties = { ...defintion.jsonData };
78
117
  }
79
- // 没有$ref
80
118
  }
81
119
  else {
82
120
  item.schema = parameter.schema;
@@ -86,7 +124,6 @@ function generateSkillCliHelp(requestMap, defintionsMap) {
86
124
  .parameter) === null || _b === void 0 ? void 0 : _b[parameter.in])) {
87
125
  modulesHelp[methodItem.category][methodItem.filename].help.parameter[parameter.in] = {};
88
126
  }
89
- // item不为空对象
90
127
  switch (parameter.in) {
91
128
  case 'path':
92
129
  case 'query':
@@ -98,7 +135,7 @@ function generateSkillCliHelp(requestMap, defintionsMap) {
98
135
  }
99
136
  }
100
137
  }
101
- // 获取返回数据
138
+ // 获取返回数据 schema
102
139
  const { responses } = methodItem.original;
103
140
  if (responses) {
104
141
  for (const [statusCode, response] of Object.entries(responses)) {
@@ -14,7 +14,7 @@ function getGenerateLicense({ source }) {
14
14
  * ## THIS FILE WAS GENERATED VIA CNB-API-GENERATE ##
15
15
  * ## ##
16
16
  * ## AUTHOR: bapelin ##
17
- * ## SOURCE: https://cnb.woa.com/cnb/frontend-science/cnb-api-generate ##
17
+ * ## SOURCE: https://cnb.cool/cnb/frontend-science/cnb-api-generate ##
18
18
  * -------------------------------------------------------------------------
19
19
  * @Version ${pkg.version}
20
20
  * @Source ${source}
@@ -30,7 +30,7 @@ function getApiGenerateLicense({ source }) {
30
30
  * ## THIS FILE WAS GENERATED VIA CNB-API-GENERATE ##
31
31
  * ## ##
32
32
  * ## AUTHOR: bapelin ##
33
- * ## SOURCE: https://cnb.woa.com/cnb/frontend-science/cnb-api-generate ##
33
+ * ## SOURCE: https://cnb.cool/cnb/frontend-science/cnb-api-generate ##
34
34
  * -------------------------------------------------------------------------
35
35
  * @Version ${pkg.version}
36
36
  * @Source ${source}
package/client/core.ts CHANGED
@@ -5,23 +5,55 @@ import { fetchResponseHandler } from "./fetch-response-handler";
5
5
  import { generateUniqueId } from './utils/generate-unique-id';
6
6
 
7
7
 
8
+ /**
9
+ * 格式化 API 响应
10
+ * 构建包含以下字段的响应对象:
11
+ * - status: HTTP 状态码
12
+ * - trace: traceparent(用于调试追踪)
13
+ * - header: 原始分页 header(x-cnb-page 等)
14
+ * - page/pageSize/total/totalPages: 从 header 计算的分页信息(仅列表 API)
15
+ * - data: 响应体(JSON/文本/图片路径/base64)
16
+ *
17
+ * 注意:所有字段始终保留,由 index.ts 的输出层决定展示哪些
18
+ */
8
19
  async function formatResponse(data: any, response: any) {
20
+ const status = response.status;
21
+ const trace = response.headers.get('traceparent');
22
+ const page = response.headers.get('x-cnb-page');
23
+ const pageSize = response.headers.get('x-cnb-page-size');
24
+ const total = response.headers.get('x-cnb-total');
25
+
9
26
  const responseData: {
10
27
  status: number;
11
- trace: string;
12
- header: Record<string, string>;
28
+ trace?: string;
29
+ header?: Record<string, string | null>;
30
+ page?: number;
31
+ pageSize?: number;
32
+ total?: number;
33
+ totalPages?: number;
13
34
  data: Record<string, any> | string | null;
14
35
  } = {
15
- status: response.status,
16
- trace: response.headers.get('traceparent'),
36
+ status,
37
+ trace,
17
38
  header: {
18
- 'x-cnb-page': response.headers.get('x-cnb-page'),
19
- 'x-cnb-page-size': response.headers.get('x-cnb-page-size'),
20
- 'x-cnb-total': response.headers.get('x-cnb-total'),
39
+ 'x-cnb-page': page,
40
+ 'x-cnb-page-size': pageSize,
41
+ 'x-cnb-total': total,
21
42
  },
22
43
  data: null,
23
44
  };
24
45
 
46
+ // 分页信息(始终计算,输出层决定是否展示)
47
+ if (total != null) {
48
+ const pageNum = parseInt(page, 10) || 1;
49
+ const pageSizeNum = parseInt(pageSize, 10) || 10;
50
+ const totalNum = parseInt(total, 10) || 0;
51
+ responseData.page = pageNum;
52
+ responseData.pageSize = pageSizeNum;
53
+ responseData.total = totalNum;
54
+ responseData.totalPages = Math.ceil(totalNum / pageSizeNum);
55
+ }
56
+
25
57
  const contentType = response.headers.get('content-type') || '';
26
58
  const isJson = [
27
59
  'application/vnd.cnb.api+json',
@@ -77,7 +109,6 @@ async function formatResponse(data: any, response: any) {
77
109
  responseData.data = err?.message || 'Unknown Error';
78
110
  }
79
111
 
80
-
81
112
  if (responseData.status >= 200 && responseData.status < 300) {
82
113
  return await fetchResponseHandler(data._originParams, responseData);
83
114
  }
@@ -1,10 +1,14 @@
1
1
  export async function fetchResponseHandler(fetchOriginParams: Record<string, any>, response: {
2
2
  status: number,
3
- trace: string,
4
- header: Record<string, string>
5
- data: Record<string, any> | string | { // 二进制数据标识
3
+ trace?: string,
4
+ header?: Record<string, string | null>,
5
+ page?: number,
6
+ pageSize?: number,
7
+ total?: number,
8
+ totalPages?: number,
9
+ data: Record<string, any> | string | {
6
10
  type: 'base64';
7
- data: string; // base64 字符串
11
+ data: string; // base64 字符串
8
12
  mimeType: string; // 原始 MIME type,方便模型知道是什么文件
9
13
  }
10
14
  | null
package/client/index.ts CHANGED
@@ -5,7 +5,6 @@ import path from 'path';
5
5
  import { showModuleHelp } from './modules.help';
6
6
  import { showToolHelp } from './tools.help';
7
7
  import { showShort, resolveShortcut } from './shortcuts';
8
- import util from 'util';
9
8
 
10
9
  const helpFileContent = fs.readFileSync(
11
10
  path.join(__dirname, 'help.json'),
@@ -17,33 +16,12 @@ if (!helpFileContent) {
17
16
  }
18
17
 
19
18
  const helpData = JSON.parse(helpFileContent);
19
+
20
20
  /**
21
21
  * 解析命令行参数
22
+ * 支持:--key value, --key=value, -k value (短参数), 位置参数 (module, tool)
22
23
  * @returns 解析后的参数对象
23
24
  */
24
- // function parseArguments(): Record<string, string | boolean | undefined> {
25
- // const args = process.argv.slice(2);
26
- // const result: Record<string, string | boolean | undefined> = {};
27
-
28
- // for (let i = 0; i < args.length; i++) {
29
- // const arg = args[i];
30
-
31
- // // 检查是否是参数名(以--开头)
32
- // if (arg.startsWith('--')) {
33
- // const paramName = arg.slice(2);
34
-
35
- // // 检查下一个参数是否是值(不是以--开头)
36
- // if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
37
- // result[paramName] = args[i + 1];
38
- // i++; // 跳过下一个参数(值)
39
- // } else {
40
- // result[paramName] = paramName === 'help' ? true : undefined;
41
- // }
42
- // }
43
- // }
44
- // return result;
45
- // }
46
-
47
25
  export function parseArguments() {
48
26
  const args = process.argv.slice(2);
49
27
  const result: Record<string, string | boolean | undefined> = {};
@@ -68,36 +46,33 @@ export function parseArguments() {
68
46
 
69
47
  const key = fullKey;
70
48
 
71
- // 检查下一个参数是否是值
49
+ // 检查下一个参数是否是值(不以 - 开头)
72
50
  const nextArg = args[i + 1];
73
51
  const isNextArgValue =
74
52
  i + 1 < args.length &&
75
53
  !nextArg.startsWith('--') &&
76
- !nextArg.startsWith('-'); // 也防止捕获短参数作为值
54
+ !nextArg.startsWith('-');
77
55
 
78
56
  if (isNextArgValue) {
79
57
  result[key] = nextArg;
80
- i += 2; // 跳过 key 和 value
58
+ i += 2;
81
59
  } else {
82
- // 没有值,视为布尔标志 (flag)
83
- // 特殊处理:如果用户显式想要 undefined 行为,可以在这里调整,但通常 CLI 中 flag 存在即为 true
60
+ // 没有值,视为布尔标志
84
61
  result[key] = true;
85
62
  i++;
86
63
  }
87
64
  } else if (arg.startsWith('-') && arg.length > 1 && !/^-?\d+$/.test(arg)) {
88
- // --- 处理短参数 (Short flags, e.g., -h, -v) ---
89
- // 注意:排除负数数字的情况
65
+ // --- 处理短参数 (e.g., -h, -v),排除负数 ---
90
66
  const key = arg.slice(1);
91
67
 
92
- // 简单处理:短参数通常不带长值,或者支持 -f value
93
68
  const nextArg = args[i + 1];
94
69
  const isNextArgValue =
95
70
  i + 1 < args.length &&
96
71
  !nextArg.startsWith('--') &&
97
72
  !nextArg.startsWith('-');
98
73
 
99
- if (isNextArgValue && key.length === 1) {
100
- // 只有单字符短参才自动吞并下一个值 ( -o output.txt),多字符连写 (如 -abc) 通常视为多个布尔旗标
74
+ // 只有单字符短参才吞并下一个值 ( -o output.txt)
75
+ if (isNextArgValue && key.length === 1) {
101
76
  result[key] = nextArg;
102
77
  i += 2;
103
78
  } else {
@@ -116,7 +91,6 @@ export function parseArguments() {
116
91
  }
117
92
  }
118
93
 
119
-
120
94
  return result;
121
95
  }
122
96
 
@@ -136,13 +110,13 @@ function validateRequiredParams(
136
110
 
137
111
  /**
138
112
  * 尝试解析JSON字符串
113
+ * 先将真实控制字符转义为 JSON 合法形式,处理 shell/AI 传入的原始换行等情况
139
114
  * @param str 要解析的字符串
140
115
  * @returns 解析后的对象或原始字符串
141
116
  */
142
117
  function tryParseJSON(str: string | boolean | undefined): any {
143
118
  if (typeof str !== 'string') return str;
144
119
 
145
- // 先将真实控制字符转义为 JSON 合法形式,处理 shell/AI 传入的原始换行等情况
146
120
  const escaped = str.replace(/[\x00-\x1F\x7F]/g, (ch) => {
147
121
  const map = { '\n': '\\n', '\r': '\\r', '\t': '\\t', '\b': '\\b', '\f': '\\f' };
148
122
  return map[ch] || '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0');
@@ -155,16 +129,9 @@ function tryParseJSON(str: string | boolean | undefined): any {
155
129
  }
156
130
 
157
131
  /**
158
- * 尝试从文件引用读取内容(支持 @filepath 语法,类似 curl -d @file
159
- * @param str 要检查的字符串
160
- * @returns 文件内容或原始字符串
161
- */
162
- /**
163
- * 从文件引用或 stdin 读取内容(类似 curl 的 @file / @- 语法)
164
- * @param str 要检查的字符串
165
- * @returns 文件/stdin 内容或原始字符串
132
+ * 尝试从文件引用或 stdin 读取内容(类似 curl @file / @- 语法)
166
133
  */
167
- function tryReadFileRef(str) {
134
+ function tryReadFileRef(str: string | boolean | undefined) {
168
135
  if (typeof str !== 'string' || !str.startsWith('@')) return str;
169
136
 
170
137
  const ref = str.slice(1);
@@ -188,31 +155,92 @@ function tryReadFileRef(str) {
188
155
  }
189
156
 
190
157
  /**
191
- * 格式化参数对象
158
+ * 获取 tool 的参数定义(用于自动分发 --key value 到 path/query)
159
+ */
160
+ function getToolParamDefs(moduleName: string, toolName: string) {
161
+ const toolHelp = helpData.modulesHelp?.[moduleName]?.[toolName];
162
+ if (!toolHelp) return null;
163
+ return toolHelp.help?.parameter || {};
164
+ }
165
+
166
+ /**
167
+ * 格式化参数
168
+ * 支持新的 --key value 扁平格式,同时向后兼容旧的 --path/--query JSON 格式。
169
+ * 根据 help.json 中的参数定义,自动将扁平参数分发到 path 或 query。
192
170
  * @param params 原始参数对象
193
- * @returns 格式化后的参数对象
171
+ * @returns 格式化后的参数对象,包含 module, tool, path, query, data 等
194
172
  */
195
173
  function formatParams(
196
174
  params: Record<string, string | boolean | undefined>,
197
175
  ): Record<string, any> {
198
- const formatted: Record<string, any> = {};
199
-
200
- // 处理每个参数
201
- for (const [key, value] of Object.entries(params)) {
202
- if (typeof value === 'string') {
203
- formatted[key] = tryParseJSON(tryReadFileRef(value));
204
- } else if (typeof value === 'boolean') {
205
- formatted[key] = value;
176
+ const formatted: Record<string, any> = {
177
+ module: params.module,
178
+ tool: params.tool,
179
+ };
180
+
181
+ // 保留控制标志
182
+ if (params.help) formatted.help = true;
183
+ if (params.short) formatted.short = true;
184
+ if (params.verbose) formatted.verbose = true;
185
+
186
+ // 旧格式兼容:--path / --query / --data 是 JSON 字符串
187
+ if (typeof params.path === 'string') {
188
+ formatted.path = tryParseJSON(tryReadFileRef(params.path));
189
+ }
190
+ if (typeof params.query === 'string') {
191
+ formatted.query = tryParseJSON(tryReadFileRef(params.query));
192
+ }
193
+ if (typeof params.data === 'string') {
194
+ formatted.data = tryParseJSON(tryReadFileRef(params.data));
195
+ }
196
+
197
+ // 新格式:将其他 --key value 根据 help.json 自动分发到 path/query
198
+ const paramDefs = getToolParamDefs(
199
+ params.module as string,
200
+ params.tool as string,
201
+ );
202
+
203
+ if (paramDefs) {
204
+ const pathDef = paramDefs.path || {};
205
+ const queryDef = paramDefs.query || {};
206
+ const reservedKeys = new Set([
207
+ 'module', 'tool', 'help', 'short', 'verbose',
208
+ 'path', 'query', 'data', 'h', 'v',
209
+ ]);
210
+
211
+ for (const [key, value] of Object.entries(params)) {
212
+ if (reservedKeys.has(key)) continue;
213
+ if (typeof value === 'boolean') continue;
214
+
215
+ if (pathDef[key]) {
216
+ // 归入 path
217
+ if (!formatted.path) formatted.path = {};
218
+ formatted.path[key] = value;
219
+ } else if (queryDef[key]) {
220
+ // 归入 query
221
+ if (!formatted.query) formatted.query = {};
222
+ // 自动转数字
223
+ const paramType = queryDef[key].type;
224
+ if (paramType === 'number' && typeof value === 'string' && !isNaN(Number(value))) {
225
+ formatted.query[key] = Number(value);
226
+ } else {
227
+ formatted.query[key] = value;
228
+ }
229
+ } else {
230
+ // 未知参数,尝试放入 query(兼容)
231
+ if (!formatted.query) formatted.query = {};
232
+ formatted.query[key] = typeof value === 'string' && !isNaN(Number(value)) ? Number(value) : value;
233
+ }
206
234
  }
207
235
  }
208
236
 
209
- // 当没有传递query时,要判断当前tool是否支持query
237
+ // 当没有传递 query 时,要判断当前 tool 是否支持 query
210
238
  if (formatted.query === undefined) {
211
- const { module, tool } = formatted;
212
- const toolsHelp = helpData.modulesHelp?.[module]?.[tool];
213
- const toolsParam = toolsHelp?.help?.parameter || {};
214
-
215
- if (toolsParam.query) {
239
+ const paramDefs2 = getToolParamDefs(
240
+ formatted.module,
241
+ formatted.tool,
242
+ );
243
+ if (paramDefs2?.query) {
216
244
  formatted.query = {};
217
245
  }
218
246
  }
@@ -220,9 +248,70 @@ function formatParams(
220
248
  return formatted;
221
249
  }
222
250
 
251
+ /**
252
+ * 精简响应对象(非 verbose 模式使用)
253
+ * - 去掉 trace(仅错误时保留)
254
+ * - 去掉 header(原始 x-cnb-* 头)
255
+ * - 仅列表 API 时保留分页信息
256
+ */
257
+ function compactResponse(response: any): any {
258
+ if (!response || typeof response !== 'object') return response;
259
+
260
+ const compact: any = { status: response.status };
261
+
262
+ // 仅错误时保留 trace
263
+ if (response.status >= 300 && response.trace) {
264
+ compact.trace = response.trace;
265
+ }
266
+
267
+ // 仅列表 API 时保留分页信息
268
+ if (response.total != null) {
269
+ compact.page = response.page;
270
+ compact.pageSize = response.pageSize;
271
+ compact.total = response.total;
272
+ compact.totalPages = response.totalPages;
273
+ }
274
+
275
+ compact.data = response.data;
276
+ return compact;
277
+ }
278
+
279
+ /**
280
+ * 判断是否是标准响应结构(有 status + data 字段)
281
+ * cag.config.js 中的 responseConverter 可能返回裸 data(不含 status),需要区分处理
282
+ */
283
+ function isStandardResponse(response: any): boolean {
284
+ return response && typeof response === 'object' && typeof response.status === 'number' && 'data' in response;
285
+ }
286
+
287
+ /**
288
+ * 格式化 CLI 输出
289
+ * - verbose 模式:完整 JSON(含 trace、header 等全部字段)
290
+ * - 非标准响应(converter 返回裸 data):直接 JSON 输出
291
+ * - 标准响应:精简 JSON(去掉 trace、header)
292
+ */
293
+ function formatOutput(response: any, verbose: boolean): string {
294
+ // --verbose 模式:输出完整原始信息
295
+ if (verbose) {
296
+ return JSON.stringify(response, null, 2);
297
+ }
298
+
299
+ // converter 可能返回裸 data(没有标准 {status, data} 结构),直接输出
300
+ if (!isStandardResponse(response)) {
301
+ return JSON.stringify(response, null, 2);
302
+ }
303
+
304
+ // 精简响应
305
+ const compact = compactResponse(response);
306
+
307
+ return JSON.stringify(compact, null, 2);
308
+ }
309
+
223
310
  /**
224
311
  * 显示帮助文档
225
- * @param moduleName 模块名称,如果指定则显示模块帮助
312
+ * - 无参数:显示顶层帮助(模块列表 + 参数说明 + 用法示例)
313
+ * - 指定 module:显示模块帮助(工具列表)
314
+ * - 指定 module + tool:显示工具帮助(参数详情 + 示例)
226
315
  */
227
316
  function showHelp(moduleName?: string, tool?: string): void {
228
317
  if (moduleName && tool) {
@@ -230,32 +319,49 @@ function showHelp(moduleName?: string, tool?: string): void {
230
319
  } else if (moduleName) {
231
320
  showModuleHelp(helpData, moduleName);
232
321
  } else {
233
- let moduleListMsg = ``;
234
- for (const [module, count] of Object.entries(helpData.mainHelp)) {
235
- moduleListMsg += `- ${module}, tool数量(${count})\n `;
322
+ const cliCmd = process.env.CNB_CLI_CMD || 'cnb';
323
+
324
+ // 紧凑的模块列表
325
+ const moduleParts: string[] = [];
326
+ for (const [mod, count] of Object.entries(helpData.mainHelp)) {
327
+ moduleParts.push(`${mod}(${count})`);
236
328
  }
237
329
 
238
- const helpMeg = `
239
- CNB OpenAPI CLI 工具\n
240
- 可用模块:
241
- ${moduleListMsg}
242
- 参数说明:
243
- <module> (必须) 模块名称 (例如: issues),可直接配合 --help 查看该模块帮助
244
- <tool> (必须) 工具/动作名称 (例如: list-issues)
245
- --path (可选) 路径参数,JSON字符串
246
- --query (可选) 查询参数,JSON字符串
247
- --data (可选) 数据参数,JSON字符串
248
- --help (可选) 显示此帮助文档
249
- --short (可选) 显示操作当前仓库(Issue/PR)的快捷命令
250
-
251
- 使用示例:
252
- ${process.env.CNB_CLI_CMD} --help
253
- ${process.env.CNB_CLI_CMD} --short
254
- ${process.env.CNB_CLI_CMD} issues --help
255
- ${process.env.CNB_CLI_CMD} issues list-issues --help
256
- ${process.env.CNB_CLI_CMD} issues list-issues --path '{"repo": "my-project"}' --query '{"page": 1, "pageSize": 10}'
330
+ // 每行放几个模块
331
+ const lines: string[] = [];
332
+ let currentLine = ' ';
333
+ for (const part of moduleParts) {
334
+ if (currentLine.length + part.length > 78) {
335
+ lines.push(currentLine);
336
+ currentLine = ' ' + part + ' ';
337
+ } else {
338
+ currentLine += part + ' ';
339
+ }
340
+ }
341
+ if (currentLine.trim()) lines.push(currentLine);
342
+
343
+ const helpMsg = `
344
+ CNB OpenAPI CLI
345
+
346
+ 模块(tool数量):
347
+ ${lines.join('\n')}
348
+
349
+ 参数:
350
+ <module> 模块名称 (如: issues, pulls, git)
351
+ <tool> 工具名称 (如: list-issues, get-issue)
352
+ --key value 路径或查询参数,CLI 自动识别归类
353
+ --data 'JSON' 请求体参数,JSON 字符串
354
+ --verbose 输出完整响应(含 trace、header)
355
+ --help 显示帮助文档
356
+ --short 显示当前仓库的快捷命令
357
+
358
+ 用法: ${cliCmd} issues list-issues --repo my-org/my-repo --page 1 --pageSize 10
359
+ ${cliCmd} issues create-issue --repo my-org/my-repo --data '{"title":"Bug"}'
360
+ 帮助: ${cliCmd} <module> --help
361
+ ${cliCmd} <module> <tool> --help
362
+ 快捷: ${cliCmd} --short
257
363
  `;
258
- console.log(helpMeg);
364
+ console.log(helpMsg);
259
365
  }
260
366
  }
261
367
 
@@ -263,49 +369,55 @@ CNB OpenAPI CLI 工具\n
263
369
  * 主函数
264
370
  */
265
371
  async function main() {
266
- // 解析命令行参数
267
372
  const params = parseArguments();
268
373
 
269
- // 处理 --short:显示当前场景的快捷命令
374
+ // 处理 --short
270
375
  if (params.short) {
271
376
  showShort();
272
377
  process.exit(0);
273
378
  }
274
379
 
275
- // 尝试解析快捷命令(如 cnb issues get → get-issue)
380
+ // 尝试解析快捷命令
276
381
  const shortcut = resolveShortcut(
277
382
  params.module as string | undefined,
278
383
  params.tool as string | undefined,
279
384
  );
280
385
  if (shortcut) {
281
- // 快捷命令:替换为实际 tool 名
282
386
  params.tool = shortcut.tool;
283
387
 
284
- // 自动注入 path 参数(如果用户没有手动传 --path)
285
- if (!params.path) {
286
- if (Object.keys(shortcut.autoPath).length > 0) {
287
- params.path = JSON.stringify(shortcut.autoPath);
388
+ // 自动注入 path 参数(兼容新旧格式)
389
+ // 新格式:用户可能直接传了 --repo xxx,检查扁平参数是否已存在
390
+ const autoPathKeys = Object.keys(shortcut.autoPath);
391
+ const hasPathAlready = params.path || autoPathKeys.some(k => params[k]);
392
+
393
+ if (!hasPathAlready) {
394
+ if (autoPathKeys.length > 0) {
395
+ // 新格式:直接注入为扁平参数
396
+ for (const [key, value] of Object.entries(shortcut.autoPath)) {
397
+ if (!params[key]) {
398
+ params[key] = value;
399
+ }
400
+ }
288
401
  } else {
289
- // 环境变量未设置,提示用户
290
402
  const envHint =
291
403
  params.module === 'issues'
292
404
  ? 'CNB_REPO_SLUG 和 CNB_ISSUE_IID'
293
405
  : 'CNB_REPO_SLUG 和 CNB_PULL_REQUEST_IID';
294
406
  console.error(
295
- `快捷命令需要环境变量 ${envHint},或手动传 --path 参数。\n` +
407
+ `快捷命令需要环境变量 ${envHint},或手动传参数。\n` +
296
408
  `提示:运行 ${process.env.CNB_CLI_CMD || 'cnb'} --short 查看快捷命令详情。`,
297
409
  );
298
410
  process.exit(1);
299
411
  }
300
412
  }
301
413
 
302
- // 自动注入 data 参数(如 close → {"state":"closed"})
414
+ // 自动注入 data 参数
303
415
  if (shortcut.autoData && !params.data) {
304
416
  params.data = JSON.stringify(shortcut.autoData);
305
417
  }
306
418
  }
307
419
 
308
- // 验证必须参数(当没有请求帮助时)
420
+ // 验证必须参数
309
421
  if (!validateRequiredParams(params)) {
310
422
  showHelp(
311
423
  params.module as string | undefined,
@@ -317,6 +429,22 @@ async function main() {
317
429
  // 格式化参数
318
430
  const formattedParams = formatParams(params);
319
431
 
432
+ // 参数预检:缺少必填 path 参数时自动输出 tool help
433
+ const toolHelpData = helpData.modulesHelp?.[formattedParams.module]?.[formattedParams.tool];
434
+ if (toolHelpData) {
435
+ const pathDef = toolHelpData.help?.parameter?.path;
436
+ if (pathDef) {
437
+ const missingRequired = Object.entries(pathDef)
438
+ .filter(([, p]: [string, any]) => p.required && !formattedParams.path?.[p.name])
439
+ .map(([k]) => `--${k}`);
440
+ if (missingRequired.length > 0) {
441
+ console.error(`缺少必填参数: ${missingRequired.join(', ')}\n`);
442
+ showToolHelp(helpData, formattedParams.module, formattedParams.tool);
443
+ process.exit(1);
444
+ }
445
+ }
446
+ }
447
+
320
448
  // 动态引入模块
321
449
  const toolPath = path.join(
322
450
  __dirname,
@@ -371,7 +499,7 @@ async function main() {
371
499
  }
372
500
 
373
501
  const data = await toolFunction(...toolsParam);
374
- console.log(util.inspect(data, { showHidden: false, depth: null }));
502
+ console.log(formatOutput(data, !!formattedParams.verbose));
375
503
  }
376
504
 
377
505
  main();
@@ -1,3 +1,15 @@
1
+ /**
2
+ * 去除 summary 中的英文部分(保留中文句号前的内容)
3
+ */
4
+ function trimSummary(summary: string): string {
5
+ if (!summary) return '';
6
+ // 匹配中文句号"。"后面跟英文的模式,只保留中文部分
7
+ const match = summary.match(/^(.*?[\u4e00-\u9fff].*?)[。.]\s*[A-Z]/);
8
+ if (match) return match[1];
9
+ // 没有匹配到则返回原文
10
+ return summary;
11
+ }
12
+
1
13
  /**
2
14
  * 显示模块帮助文档
3
15
  * @param helpData 帮助数据
@@ -10,27 +22,23 @@ export function showModuleHelp(helpData: any, moduleName: string): void {
10
22
  process.exit(1);
11
23
  }
12
24
 
13
- let toolListMsg = ``;
14
- for (const [tool, info] of Object.entries(moduleHelpData)) {
15
- toolListMsg += `- ${(info as any).filename}, ${(info as any).summary}\n `;
25
+ const cliCmd = process.env.CNB_CLI_CMD || 'cnb';
26
+
27
+ let toolListMsg = '';
28
+ for (const [, info] of Object.entries(moduleHelpData)) {
29
+ const name = (info as any).filename;
30
+ const summary = (info as any).summary;
31
+ toolListMsg += ` ${name.padEnd(30)} ${summary}\n`;
16
32
  }
17
33
 
18
- const helpMeg = `
19
- 模块${moduleName}帮助文档\n
20
- 可用工具:
21
- ${toolListMsg}
22
- 参数说明:
23
- <module> (必须) 模块名称 (例如: issues),可直接配合 --help 查看该模块帮助
24
- <tool> (必须) 工具/动作名称 (例如: list-issues)
25
- --path (可选) 路径参数,JSON字符串
26
- --query (可选) 查询参数,JSON字符串
27
- --data (可选) 数据参数,JSON字符串
28
- --help (可选) 显示此帮助文档
34
+ const helpMsg = `
35
+ ${moduleName} 模块
29
36
 
30
- 使用示例:
31
- ${process.env.CNB_CLI_CMD} issues --help
32
- ${process.env.CNB_CLI_CMD} issues list-issues --help
33
- ${process.env.CNB_CLI_CMD} issues list-issues --path '{"repo": "my-project"}' --query '{"page": 1, "pageSize": 10}'
37
+ 工具:
38
+ ${toolListMsg}
39
+ 用法: ${cliCmd} ${moduleName} <tool> --help
34
40
  `;
35
- console.log(helpMeg);
41
+ console.log(helpMsg);
36
42
  }
43
+
44
+ export { trimSummary };
@@ -1,7 +1,10 @@
1
- import util from 'util';
2
- import { schemaToJson } from './schemaToJson';
1
+ import { trimSummary } from './modules.help';
2
+
3
3
  /**
4
4
  * 显示工具帮助文档
5
+ * - path 和 query 参数混合展示为统一的 --key value 格式(LLM 不需要区分来源)
6
+ * - body 参数展示顶层字段列表,从 schema.required 数组读取必填标记
7
+ * - 权限直接读 help.json 中预提取的 permission 字段
5
8
  * @param helpData 帮助数据
6
9
  * @param moduleName 模块名称
7
10
  * @param tool 工具名称
@@ -17,88 +20,105 @@ export function showToolHelp(
17
20
  process.exit(1);
18
21
  }
19
22
 
20
- const { summary, description, help } = toolHelpData;
23
+ const cliCmd = process.env.CNB_CLI_CMD || 'cnb';
24
+ const { summary, permission, help } = toolHelpData;
21
25
  const { parameter } = help;
22
26
  const { path, query, body } = parameter;
23
27
 
24
- // path参数说明
25
- let pathMsg = '';
26
- const pathParamsExample: Record<string, string> = {};
28
+ // 收集所有扁平参数(path + query 混合展示)
29
+ const paramLines: string[] = [];
30
+ const exampleParts: string[] = [];
31
+
32
+ // path 参数
27
33
  if (path) {
28
- pathMsg = `--path参数详细说明:
29
- ${Object.keys(path)
30
- .map((key) => {
31
- if (path[key].required) {
32
- pathParamsExample[key] = `<${path[key].type}>`;
34
+ for (const [key, param] of Object.entries(path) as any) {
35
+ const req = param.required ? ',必填' : '';
36
+ paramLines.push(` --${key.padEnd(14)} (${param.type}${req}) ${param.description || ''}`);
37
+ if (param.required) {
38
+ exampleParts.push(`--${key} <${param.type}>`);
39
+ }
33
40
  }
34
- return ` - ${key} (${path[key].type}) - ${path[key].description}(${path[key].required ? '必填' : '选填'})`;
35
- })
36
- .join('\n')}
37
- `;
38
41
  }
39
42
 
40
- // query参数说明
41
- let queryMsg = '';
42
- const queryParamsExample: Record<string, string> = {};
43
+ // query 参数
43
44
  if (query) {
44
- queryMsg = `--query详细参数:
45
- ${Object.keys(query)
46
- .map((key) => {
47
- if (query[key].required) {
48
- queryParamsExample[key] = `<${query[key].type}>`;
49
- } else if (query[key].default !== undefined) {
50
- queryParamsExample[key] = query[key].default;
45
+ for (const [key, param] of Object.entries(query) as any) {
46
+ const req = param.required ? ',必填' : '';
47
+ const enumStr = param.enum ? ` [${param.enum.join(',')}]` : '';
48
+ paramLines.push(` --${key.padEnd(14)} (${param.type}${req}) ${param.description || ''}${enumStr}`);
49
+ if (param.required) {
50
+ exampleParts.push(`--${key} <${param.type}>`);
51
+ }
51
52
  }
52
- return ` - ${key} (${query[key].type}) - ${query[key].description}(${query[key].required ? '必填' : '选填'})${query[key].enum ? `, [枚举: ${query[key].enum.join(', ')}]` : ''}`;
53
- })
54
- .join('\n')}
55
- `;
56
53
  }
57
54
 
58
- // data参数说明
55
+ // body 参数
59
56
  let bodyMsg = '';
60
- let bodyParamsExample: Record<string, string> = {};
57
+ let bodyExample = '';
61
58
  if (body) {
62
- const { type, description, required, schema } = body;
59
+ const { schema } = body;
63
60
  if (schema) {
64
- bodyParamsExample = schemaToJson(schema, {});
65
- bodyMsg = `--data参数详细说明:
66
- ${util.inspect(schema, { showHidden: false, depth: null })}`;
67
- } else {
68
- bodyMsg = `--data参数详细说明:
69
- - ${type} - ${description}(${required ? '必填' : '选填'})
70
- `;
61
+ const bodyLines: string[] = [];
62
+ const bodyExampleObj: Record<string, string> = {};
63
+ const props = schema.type === 'array'
64
+ ? schema.items?.properties
65
+ : schema.properties;
66
+
67
+ // Swagger 的 required 是父级数组,不在每个 property 上
68
+ const requiredFields = new Set<string>(
69
+ schema.required || schema.items?.required || []
70
+ );
71
+
72
+ if (props) {
73
+ for (const [key, prop] of Object.entries(props) as any) {
74
+ const type = prop.type || 'object';
75
+ const isRequired = requiredFields.has(key);
76
+ const req = isRequired ? ',必填' : '';
77
+ // body 属性的 description 在生成时未 trim,运行时 trim
78
+ const desc = trimSummary(prop.description || '');
79
+ bodyLines.push(` ${key.padEnd(16)} (${type}${req}) ${desc}`);
80
+ if (isRequired) {
81
+ bodyExampleObj[key] = `<${type}>`;
82
+ }
83
+ }
84
+ }
85
+
86
+ if (bodyLines.length > 0) {
87
+ bodyMsg = `\nBody (--data JSON):\n${bodyLines.join('\n')}`;
88
+ }
89
+ if (Object.keys(bodyExampleObj).length > 0) {
90
+ bodyExample = ` --data '${JSON.stringify(bodyExampleObj)}'`;
91
+ } else if (bodyLines.length > 0) {
92
+ // 没有 required 标记时,取前两个字段作为示例
93
+ const sampleKeys = Object.keys(props).slice(0, 2);
94
+ const sampleObj: Record<string, string> = {};
95
+ for (const k of sampleKeys) {
96
+ sampleObj[k] = `<${props[k].type || 'string'}>`;
97
+ }
98
+ if (sampleKeys.length > 0) {
99
+ bodyExample = ` --data '${JSON.stringify(sampleObj)}'`;
100
+ }
101
+ }
71
102
  }
72
103
  }
73
104
 
74
- const exampleMsg = [
75
- `node ./skills/scripts/core ${moduleName} ${tool}`,
76
- ];
77
- if (Object.keys(pathParamsExample).length > 0) {
78
- exampleMsg.push(`--path '${JSON.stringify(pathParamsExample)}'`);
105
+ // 构建输出
106
+ let output = `${moduleName} ${tool} - ${summary}`;
107
+ if (permission) {
108
+ output += `\n权限: ${permission}`;
79
109
  }
80
- if (Object.keys(queryParamsExample).length > 0) {
81
- exampleMsg.push(`--query '${JSON.stringify(queryParamsExample)}'`);
110
+
111
+ if (paramLines.length > 0) {
112
+ output += `\n\n参数:\n${paramLines.join('\n')}`;
82
113
  }
83
- if (Object.keys(bodyParamsExample).length > 0) {
84
- exampleMsg.push(`--data '${JSON.stringify(bodyParamsExample)}'`);
114
+
115
+ if (bodyMsg) {
116
+ output += bodyMsg;
85
117
  }
86
118
 
87
- const helpMeg = `
88
- 工具${tool}帮助文档\n
89
- 工具说明:
90
- 1. ${summary}
91
- 2. ${description}
92
- \n参数说明:
93
- <module> (必须) 模块名称 (例如: issues),可直接配合 --help 查看该模块帮助
94
- <tool> (必须) 工具/动作名称 (例如: list-issues)
95
- --path (可选) 路径参数,JSON字符串
96
- --query (可选) 查询参数,JSON字符串
97
- --data (可选) 数据参数,JSON字符串
98
- --help (可选) 显示此帮助文档
99
- ${pathMsg && `\n${pathMsg}`}${queryMsg && `\n${queryMsg}`}${bodyMsg && `\n${bodyMsg}`}
100
- \n使用示例:
101
- ${exampleMsg.join(' ')}
102
- `;
103
- console.log(helpMeg);
119
+ // 示例
120
+ const exampleCmd = `${cliCmd} ${moduleName} ${tool}${exampleParts.length > 0 ? ' ' + exampleParts.join(' ') : ''}${bodyExample}`;
121
+ output += `\n\n示例: ${exampleCmd}`;
122
+
123
+ console.log(output);
104
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,100 +1,21 @@
1
1
  ---
2
2
  name: cnb-api
3
- description: 提供与CNB(Cloud Native Build)OpenAPI的完整交互能力,支持项目、组织、代码仓库、Issue、PR、合并请求、流水线,制品库等核心功能的增删改查操作。适用于开发协作、代码管理和CI/CD流程管理场景。关键词:CNB、云原生构建、组织、代码仓库、Issue、PR、合并请求、流水线、制品库,查询、新增、修改、删除、评论、合并、审批。
3
+ description: CNB OpenAPI 交互能力,支持仓库、Issue、PR、流水线、制品库等操作。
4
4
  ---
5
5
 
6
6
  # cnb-api
7
7
 
8
- ## 概括
8
+ 操作 CNB 平台资源的 CLI 工具。
9
9
 
10
- 本Skill提供完整的CNB的Openapi完整交互能力,用户可以使用此skills对CNB上的资源进行操作。
10
+ ## 使用
11
11
 
12
- ## 适用场景
12
+ 1. `<$CNB_CLI_CMD$> --help` 查看所有模块
13
+ 2. `<$CNB_CLI_CMD$> <module> --help` 查看模块下的工具列表
14
+ 3. `<$CNB_CLI_CMD$> <module> <tool> --help` 查看工具参数
15
+ 4. 按参数说明执行
13
16
 
14
- 当用户描述对CNB上的资源进行操作时,应该使用此skills进行操作。例如:
15
- - 查询cnb上某个仓库的Issue列表
16
- - 对cnb上的某个仓库的Issue或者pr进行评论。
17
- - ...
17
+ ## 规则
18
18
 
19
- ## 核心原则
20
-
21
- ### 准确性原则
22
- - **CRITICAL**: 必须先执行 `<$CNB_CLI_CMD$> --help` 获取最新的使用方式
23
- - **CRITICAL**: 必须通过使用 `<$CNB_CLI_CMD$>`命令行工具,按照帮助信息执行操作
24
- - **CRITICAL**: 禁止推测或臆断使用方式,严格基于脚本返回的帮助信息进行操作
25
- - **CRITICAL**: 不要询问用户"是否需要我执行",直接根据帮助信息执行脚本,并返回结果
26
-
27
- ## 脚本使用指南
28
-
29
- ### 第一步:获取帮助信息
30
- 在执行任何任务前,必须先运行以下命令获取最新的使用方式:
31
-
32
- ```bash
33
- <$CNB_CLI_CMD$> --help
34
- ```
35
-
36
- 这将显示所有可用的模块及其工具列表。
37
-
38
- ### 第二步:查看具体模块帮助
39
- 使用 `--module` 参数查看特定模块的详细帮助:
40
-
41
- ```bash
42
- <$CNB_CLI_CMD$> --module <模块名> -help
43
- ```
44
-
45
- ### 第三步:查看工具详细使用
46
- 使用 `--module` 和 `--tool` 参数查看工具的详细参数说明:
47
-
48
- ```bash
49
- <$CNB_CLI_CMD$> --module <模块名> --tool <工具名> --help
50
- ```
51
-
52
- ### 第四步:执行工具
53
- 根据第三步获取的参数说明,执行工具:
54
-
55
- ```bash
56
- <$CNB_CLI_CMD$> --module <模块名> --tool <工具名> --path '{"参数": "值"}' --query '{"参数": "值"}' --data '{"参数": "值"}'
57
- ```
58
-
59
- **参数说明:**
60
- - `--module`: 必须参数,模块名称
61
- - `--tool`: 必须参数,工具名称
62
- - `--path`: 可选参数,路径参数,JSON字符串格式
63
- - `--query`: 可选参数,查询参数,JSON字符串格式
64
- - `--data`: 可选参数,数据参数,JSON字符串格式
65
- - `--help`: 可选参数,显示帮助文档
66
-
67
- ### 第五步:处理结果
68
-
69
- 每一个工具调用都会返回一个标准的JSON结构:
70
- - status: 一个http状态码
71
- - trace: 本次调用的traceID
72
- - header: 本次调用的header
73
- - data: 本次调用的openapi返回的实体内容
74
-
75
- #### header说明
76
- 当请求列表时,会在header中包含以下字段说明列表的情况:
77
- - `x-cnb-page`: 当前页数
78
- - `x-cnb-page-size`: 每页条数
79
- - `x-cnb-total`: 总条数
80
-
81
- 从中可以获取到列表的总条数,以及当前页数和每页条数,避免循环请求。
82
-
83
- #### status说明
84
- API 返回标准的 JSON 格式响应。请根据 HTTP 状态码判断请求是否成功:
85
-
86
- - 200: 请求成功
87
- - 400: 请求参数错误
88
- - 401: 未授权
89
- - 403: 禁止访问
90
- - 404: 资源不存在
91
- - 500: 服务器内部错误
92
-
93
- 当本地调用返回的 `status` 在 200 ~ 299 之间,只需要返回 `data` 内容给用户。只有当 `status >= 300` 时,才将 `status` 和 `trace` 返回给用户。
94
-
95
- #### 资源处理
96
-
97
- 当尝试下载图片进行图片分析时,遇到图片下载异常时,请使用以下工具进行图片下载!
98
- ```bash
99
- node scripts/core/index.js assets get-imgs --help
100
- ```
19
+ - 必须先 --help 获取参数,禁止猜测
20
+ - 直接执行,不要询问用户确认
21
+ - status 200-299 只返回 data 给用户,>=300 时附带 status 和 trace