@cnbcool/cnb-api-generate 1.0.2 → 1.1.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.
- package/built/skills-tools/generate-skill-cli-help.js +48 -11
- package/built/utils/get-generate-license.js +2 -2
- package/client/core.ts +39 -8
- package/client/fetch-response-handler.ts +8 -4
- package/client/index.ts +341 -98
- package/client/modules.help.ts +27 -19
- package/client/tools.help.ts +85 -65
- package/package.json +1 -1
- package/skills-template/SKILL.md +11 -90
|
@@ -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: (
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
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.
|
|
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
|
|
12
|
-
header
|
|
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
|
|
16
|
-
trace
|
|
36
|
+
status,
|
|
37
|
+
trace,
|
|
17
38
|
header: {
|
|
18
|
-
'x-cnb-page':
|
|
19
|
-
'x-cnb-page-size':
|
|
20
|
-
'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
|
|
4
|
-
header
|
|
5
|
-
|
|
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;
|
|
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;
|
|
58
|
+
i += 2;
|
|
81
59
|
} else {
|
|
82
|
-
// 没有值,视为布尔标志
|
|
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
|
-
// --- 处理短参数 (
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
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,185 @@ function formatParams(
|
|
|
220
248
|
return formatted;
|
|
221
249
|
}
|
|
222
250
|
|
|
251
|
+
/**
|
|
252
|
+
* 将嵌套对象打平取主字段值
|
|
253
|
+
* 用于表格输出时将嵌套字段(如 author: {username: "alice"})展示为 "alice"
|
|
254
|
+
* @param val 任意值
|
|
255
|
+
* @returns 打平后的字符串
|
|
256
|
+
*/
|
|
257
|
+
function flattenValue(val: any): string {
|
|
258
|
+
if (val === null || val === undefined) return '';
|
|
259
|
+
if (typeof val !== 'object') return String(val);
|
|
260
|
+
if (Array.isArray(val)) {
|
|
261
|
+
return val.map(v => typeof v === 'object' ? flattenValue(v) : String(v)).join(',');
|
|
262
|
+
}
|
|
263
|
+
// 对象:取第一个 string 类型的字段值(如 author.login)
|
|
264
|
+
for (const v of Object.values(val)) {
|
|
265
|
+
if (typeof v === 'string') return v;
|
|
266
|
+
if (typeof v === 'number') return String(v);
|
|
267
|
+
}
|
|
268
|
+
return JSON.stringify(val);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 缩短时间戳:2026-04-05T15:50:35Z → 2026-04-05
|
|
273
|
+
*/
|
|
274
|
+
function shortenTimestamp(str: string): string {
|
|
275
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(str)) {
|
|
276
|
+
return str.substring(0, 10);
|
|
277
|
+
}
|
|
278
|
+
return str;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 格式化数组响应为紧凑表格
|
|
283
|
+
* - 过滤低信号字段(url、avatar 等)和全空列
|
|
284
|
+
* - 缩短时间戳为日期
|
|
285
|
+
* - 嵌套对象打平取主字段值
|
|
286
|
+
* - 格式:表头行 + 数据行,用 | 分隔
|
|
287
|
+
*/
|
|
288
|
+
function formatArrayAsTable(response: any): string {
|
|
289
|
+
const { status, page, total, totalPages, data } = response;
|
|
290
|
+
|
|
291
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
292
|
+
return JSON.stringify(response);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 收集所有出现的 key
|
|
296
|
+
const allKeys = new Set<string>();
|
|
297
|
+
for (const item of data) {
|
|
298
|
+
if (typeof item === 'object' && item !== null) {
|
|
299
|
+
for (const key of Object.keys(item)) {
|
|
300
|
+
allKeys.add(key);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (allKeys.size === 0) {
|
|
306
|
+
return JSON.stringify(response);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 低信号字段
|
|
310
|
+
const lowSignalPatterns = [
|
|
311
|
+
/url$/i, /avatar/i, /html_url/i, /href/i, /link/i,
|
|
312
|
+
/^_/, /updated_at/i, /^invisible$/i, /^state_reason$/i,
|
|
313
|
+
/^started_at$/i, /^ended_at$/i, /^last_acted_at$/i,
|
|
314
|
+
/^is_npc$/i, /^email$/i, /^nickname$/i,
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const filteredKeys = [...allKeys].filter(key =>
|
|
318
|
+
!lowSignalPatterns.some(p => p.test(key))
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const keysList = filteredKeys.length >= 3 ? filteredKeys : [...allKeys];
|
|
322
|
+
|
|
323
|
+
// 构建列数据,同时过滤全空列
|
|
324
|
+
const columns: { key: string; values: string[] }[] = [];
|
|
325
|
+
for (const key of keysList) {
|
|
326
|
+
const values: string[] = [];
|
|
327
|
+
let hasNonEmpty = false;
|
|
328
|
+
for (const item of data) {
|
|
329
|
+
let str = flattenValue(item?.[key]);
|
|
330
|
+
str = shortenTimestamp(str);
|
|
331
|
+
str = str.replace(/\|/g, '/');
|
|
332
|
+
if (str.length > 80) str = str.substring(0, 77) + '...';
|
|
333
|
+
if (str !== '' && str !== 'false') hasNonEmpty = true;
|
|
334
|
+
values.push(str);
|
|
335
|
+
}
|
|
336
|
+
if (hasNonEmpty) {
|
|
337
|
+
columns.push({ key, values });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (columns.length === 0) {
|
|
342
|
+
return JSON.stringify(response);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 构建输出(紧凑格式,不对齐)
|
|
346
|
+
let output = `status:${status}`;
|
|
347
|
+
if (total != null) {
|
|
348
|
+
output += ` page:${page}/${totalPages} total:${total}`;
|
|
349
|
+
}
|
|
350
|
+
output += '\n';
|
|
351
|
+
output += columns.map(c => c.key).join('|') + '\n';
|
|
352
|
+
for (let r = 0; r < data.length; r++) {
|
|
353
|
+
output += columns.map(c => c.values[r]).join('|') + '\n';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return output.trimEnd();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 精简响应对象(非 verbose 模式使用)
|
|
361
|
+
* - 去掉 trace(仅错误时保留)
|
|
362
|
+
* - 去掉 header(原始 x-cnb-* 头)
|
|
363
|
+
* - 仅列表 API 时保留分页信息
|
|
364
|
+
*/
|
|
365
|
+
function compactResponse(response: any): any {
|
|
366
|
+
if (!response || typeof response !== 'object') return response;
|
|
367
|
+
|
|
368
|
+
const compact: any = { status: response.status };
|
|
369
|
+
|
|
370
|
+
// 仅错误时保留 trace
|
|
371
|
+
if (response.status >= 300 && response.trace) {
|
|
372
|
+
compact.trace = response.trace;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 仅列表 API 时保留分页信息
|
|
376
|
+
if (response.total != null) {
|
|
377
|
+
compact.page = response.page;
|
|
378
|
+
compact.pageSize = response.pageSize;
|
|
379
|
+
compact.total = response.total;
|
|
380
|
+
compact.totalPages = response.totalPages;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
compact.data = response.data;
|
|
384
|
+
return compact;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 判断是否是标准响应结构(有 status + data 字段)
|
|
389
|
+
* cag.config.js 中的 responseConverter 可能返回裸 data(不含 status),需要区分处理
|
|
390
|
+
*/
|
|
391
|
+
function isStandardResponse(response: any): boolean {
|
|
392
|
+
return response && typeof response === 'object' && typeof response.status === 'number' && 'data' in response;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 格式化 CLI 输出
|
|
397
|
+
* - verbose 模式:完整 JSON(含 trace、header 等全部字段)
|
|
398
|
+
* - 非标准响应(converter 返回裸 data):直接 JSON 输出
|
|
399
|
+
* - 数组响应:紧凑表格格式
|
|
400
|
+
* - 单对象响应:精简 JSON(去掉 trace、header)
|
|
401
|
+
*/
|
|
402
|
+
function formatOutput(response: any, verbose: boolean): string {
|
|
403
|
+
// --verbose 模式:输出完整原始信息
|
|
404
|
+
if (verbose) {
|
|
405
|
+
return JSON.stringify(response, null, 2);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// converter 可能返回裸 data(没有标准 {status, data} 结构),直接输出
|
|
409
|
+
if (!isStandardResponse(response)) {
|
|
410
|
+
return JSON.stringify(response);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 精简响应
|
|
414
|
+
const compact = compactResponse(response);
|
|
415
|
+
|
|
416
|
+
// 数组响应 → 表格格式
|
|
417
|
+
if (compact && Array.isArray(compact.data)) {
|
|
418
|
+
return formatArrayAsTable(compact);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 单对象响应 → 精简 JSON
|
|
422
|
+
return JSON.stringify(compact);
|
|
423
|
+
}
|
|
424
|
+
|
|
223
425
|
/**
|
|
224
426
|
* 显示帮助文档
|
|
225
|
-
*
|
|
427
|
+
* - 无参数:显示顶层帮助(模块列表 + 参数说明 + 用法示例)
|
|
428
|
+
* - 指定 module:显示模块帮助(工具列表)
|
|
429
|
+
* - 指定 module + tool:显示工具帮助(参数详情 + 示例)
|
|
226
430
|
*/
|
|
227
431
|
function showHelp(moduleName?: string, tool?: string): void {
|
|
228
432
|
if (moduleName && tool) {
|
|
@@ -230,32 +434,49 @@ function showHelp(moduleName?: string, tool?: string): void {
|
|
|
230
434
|
} else if (moduleName) {
|
|
231
435
|
showModuleHelp(helpData, moduleName);
|
|
232
436
|
} else {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
437
|
+
const cliCmd = process.env.CNB_CLI_CMD || 'cnb';
|
|
438
|
+
|
|
439
|
+
// 紧凑的模块列表
|
|
440
|
+
const moduleParts: string[] = [];
|
|
441
|
+
for (const [mod, count] of Object.entries(helpData.mainHelp)) {
|
|
442
|
+
moduleParts.push(`${mod}(${count})`);
|
|
236
443
|
}
|
|
237
444
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
445
|
+
// 每行放几个模块
|
|
446
|
+
const lines: string[] = [];
|
|
447
|
+
let currentLine = ' ';
|
|
448
|
+
for (const part of moduleParts) {
|
|
449
|
+
if (currentLine.length + part.length > 78) {
|
|
450
|
+
lines.push(currentLine);
|
|
451
|
+
currentLine = ' ' + part + ' ';
|
|
452
|
+
} else {
|
|
453
|
+
currentLine += part + ' ';
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (currentLine.trim()) lines.push(currentLine);
|
|
457
|
+
|
|
458
|
+
const helpMsg = `
|
|
459
|
+
CNB OpenAPI CLI
|
|
460
|
+
|
|
461
|
+
模块(tool数量):
|
|
462
|
+
${lines.join('\n')}
|
|
463
|
+
|
|
464
|
+
参数:
|
|
465
|
+
<module> 模块名称 (如: issues, pulls, git)
|
|
466
|
+
<tool> 工具名称 (如: list-issues, get-issue)
|
|
467
|
+
--key value 路径或查询参数,CLI 自动识别归类
|
|
468
|
+
--data 'JSON' 请求体参数,JSON 字符串
|
|
469
|
+
--verbose 输出完整响应(含 trace、header)
|
|
470
|
+
--help 显示帮助文档
|
|
471
|
+
--short 显示当前仓库的快捷命令
|
|
472
|
+
|
|
473
|
+
用法: ${cliCmd} issues list-issues --repo my-org/my-repo --page 1 --pageSize 10
|
|
474
|
+
${cliCmd} issues create-issue --repo my-org/my-repo --data '{"title":"Bug"}'
|
|
475
|
+
帮助: ${cliCmd} <module> --help
|
|
476
|
+
${cliCmd} <module> <tool> --help
|
|
477
|
+
快捷: ${cliCmd} --short
|
|
257
478
|
`;
|
|
258
|
-
console.log(
|
|
479
|
+
console.log(helpMsg);
|
|
259
480
|
}
|
|
260
481
|
}
|
|
261
482
|
|
|
@@ -263,49 +484,55 @@ CNB OpenAPI CLI 工具\n
|
|
|
263
484
|
* 主函数
|
|
264
485
|
*/
|
|
265
486
|
async function main() {
|
|
266
|
-
// 解析命令行参数
|
|
267
487
|
const params = parseArguments();
|
|
268
488
|
|
|
269
|
-
// 处理 --short
|
|
489
|
+
// 处理 --short
|
|
270
490
|
if (params.short) {
|
|
271
491
|
showShort();
|
|
272
492
|
process.exit(0);
|
|
273
493
|
}
|
|
274
494
|
|
|
275
|
-
//
|
|
495
|
+
// 尝试解析快捷命令
|
|
276
496
|
const shortcut = resolveShortcut(
|
|
277
497
|
params.module as string | undefined,
|
|
278
498
|
params.tool as string | undefined,
|
|
279
499
|
);
|
|
280
500
|
if (shortcut) {
|
|
281
|
-
// 快捷命令:替换为实际 tool 名
|
|
282
501
|
params.tool = shortcut.tool;
|
|
283
502
|
|
|
284
|
-
// 自动注入 path
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
503
|
+
// 自动注入 path 参数(兼容新旧格式)
|
|
504
|
+
// 新格式:用户可能直接传了 --repo xxx,检查扁平参数是否已存在
|
|
505
|
+
const autoPathKeys = Object.keys(shortcut.autoPath);
|
|
506
|
+
const hasPathAlready = params.path || autoPathKeys.some(k => params[k]);
|
|
507
|
+
|
|
508
|
+
if (!hasPathAlready) {
|
|
509
|
+
if (autoPathKeys.length > 0) {
|
|
510
|
+
// 新格式:直接注入为扁平参数
|
|
511
|
+
for (const [key, value] of Object.entries(shortcut.autoPath)) {
|
|
512
|
+
if (!params[key]) {
|
|
513
|
+
params[key] = value;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
288
516
|
} else {
|
|
289
|
-
// 环境变量未设置,提示用户
|
|
290
517
|
const envHint =
|
|
291
518
|
params.module === 'issues'
|
|
292
519
|
? 'CNB_REPO_SLUG 和 CNB_ISSUE_IID'
|
|
293
520
|
: 'CNB_REPO_SLUG 和 CNB_PULL_REQUEST_IID';
|
|
294
521
|
console.error(
|
|
295
|
-
`快捷命令需要环境变量 ${envHint}
|
|
522
|
+
`快捷命令需要环境变量 ${envHint},或手动传参数。\n` +
|
|
296
523
|
`提示:运行 ${process.env.CNB_CLI_CMD || 'cnb'} --short 查看快捷命令详情。`,
|
|
297
524
|
);
|
|
298
525
|
process.exit(1);
|
|
299
526
|
}
|
|
300
527
|
}
|
|
301
528
|
|
|
302
|
-
// 自动注入 data
|
|
529
|
+
// 自动注入 data 参数
|
|
303
530
|
if (shortcut.autoData && !params.data) {
|
|
304
531
|
params.data = JSON.stringify(shortcut.autoData);
|
|
305
532
|
}
|
|
306
533
|
}
|
|
307
534
|
|
|
308
|
-
//
|
|
535
|
+
// 验证必须参数
|
|
309
536
|
if (!validateRequiredParams(params)) {
|
|
310
537
|
showHelp(
|
|
311
538
|
params.module as string | undefined,
|
|
@@ -317,6 +544,22 @@ async function main() {
|
|
|
317
544
|
// 格式化参数
|
|
318
545
|
const formattedParams = formatParams(params);
|
|
319
546
|
|
|
547
|
+
// 参数预检:缺少必填 path 参数时自动输出 tool help
|
|
548
|
+
const toolHelpData = helpData.modulesHelp?.[formattedParams.module]?.[formattedParams.tool];
|
|
549
|
+
if (toolHelpData) {
|
|
550
|
+
const pathDef = toolHelpData.help?.parameter?.path;
|
|
551
|
+
if (pathDef) {
|
|
552
|
+
const missingRequired = Object.entries(pathDef)
|
|
553
|
+
.filter(([, p]: [string, any]) => p.required && !formattedParams.path?.[p.name])
|
|
554
|
+
.map(([k]) => `--${k}`);
|
|
555
|
+
if (missingRequired.length > 0) {
|
|
556
|
+
console.error(`缺少必填参数: ${missingRequired.join(', ')}\n`);
|
|
557
|
+
showToolHelp(helpData, formattedParams.module, formattedParams.tool);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
320
563
|
// 动态引入模块
|
|
321
564
|
const toolPath = path.join(
|
|
322
565
|
__dirname,
|
|
@@ -371,7 +614,7 @@ async function main() {
|
|
|
371
614
|
}
|
|
372
615
|
|
|
373
616
|
const data = await toolFunction(...toolsParam);
|
|
374
|
-
console.log(
|
|
617
|
+
console.log(formatOutput(data, !!formattedParams.verbose));
|
|
375
618
|
}
|
|
376
619
|
|
|
377
620
|
main();
|
package/client/modules.help.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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(
|
|
41
|
+
console.log(helpMsg);
|
|
36
42
|
}
|
|
43
|
+
|
|
44
|
+
export { trimSummary };
|
package/client/tools.help.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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
|
|
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
|
-
|
|
26
|
-
const
|
|
28
|
+
// 收集所有扁平参数(path + query 混合展示)
|
|
29
|
+
const paramLines: string[] = [];
|
|
30
|
+
const exampleParts: string[] = [];
|
|
31
|
+
|
|
32
|
+
// path 参数
|
|
27
33
|
if (path) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
//
|
|
55
|
+
// body 参数
|
|
59
56
|
let bodyMsg = '';
|
|
60
|
-
let
|
|
57
|
+
let bodyExample = '';
|
|
61
58
|
if (body) {
|
|
62
|
-
const {
|
|
59
|
+
const { schema } = body;
|
|
63
60
|
if (schema) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
exampleMsg.push(`--path '${JSON.stringify(pathParamsExample)}'`);
|
|
105
|
+
// 构建输出
|
|
106
|
+
let output = `${moduleName} ${tool} - ${summary}`;
|
|
107
|
+
if (permission) {
|
|
108
|
+
output += `\n权限: ${permission}`;
|
|
79
109
|
}
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
|
|
111
|
+
if (paramLines.length > 0) {
|
|
112
|
+
output += `\n\n参数:\n${paramLines.join('\n')}`;
|
|
82
113
|
}
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
|
|
115
|
+
if (bodyMsg) {
|
|
116
|
+
output += bodyMsg;
|
|
85
117
|
}
|
|
86
118
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
package/skills-template/SKILL.md
CHANGED
|
@@ -1,100 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cnb-api
|
|
3
|
-
description:
|
|
3
|
+
description: CNB OpenAPI 交互能力,支持仓库、Issue、PR、流水线、制品库等操作。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# cnb-api
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
操作 CNB 平台资源的 CLI 工具。
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|