@cnbcool/cnb-api-generate 2.7.7 → 2.8.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.
- package/built/utils/flatten-tool-options.js +16 -6
- package/client/lib/format-params.ts +45 -14
- package/client/lib/parsers.ts +15 -21
- package/package.json +1 -1
- package/skills-template/SKILL.md +10 -5
|
@@ -84,12 +84,9 @@ function flattenToolOptions(toolInfo) {
|
|
|
84
84
|
? (0, clean_array_desc_1.cleanArrayDesc)((0, trim_summary_1.trimSummary)(prop.description || ''))
|
|
85
85
|
: (0, trim_summary_1.trimSummary)(prop.description || '');
|
|
86
86
|
const arrayHint = isArray ? ' (可多次传入)' : '';
|
|
87
|
-
const fileRefHint = !isArray && prop.type === 'string' && key === 'body'
|
|
88
|
-
? ' (支持 @file 引用)'
|
|
89
|
-
: '';
|
|
90
87
|
const desc = isRequired
|
|
91
|
-
? `[必填] ${rawDesc}${arrayHint}
|
|
92
|
-
: `${rawDesc}${arrayHint}
|
|
88
|
+
? `[必填] ${rawDesc}${arrayHint}`
|
|
89
|
+
: `${rawDesc}${arrayHint}`;
|
|
93
90
|
const opt = {
|
|
94
91
|
optKey,
|
|
95
92
|
valuePlaceholder,
|
|
@@ -100,13 +97,26 @@ function flattenToolOptions(toolInfo) {
|
|
|
100
97
|
};
|
|
101
98
|
options.push(opt);
|
|
102
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
|
+
}
|
|
103
113
|
}
|
|
104
114
|
// body 存在时添加 --data fallback
|
|
105
115
|
if (bodyDef) {
|
|
106
116
|
options.push({
|
|
107
117
|
optKey: 'data',
|
|
108
118
|
valuePlaceholder: '<json>',
|
|
109
|
-
description: 'Request body (JSON
|
|
119
|
+
description: 'Request body (JSON 字符串,可替代逐个 body 字段)',
|
|
110
120
|
required: false,
|
|
111
121
|
isArray: false,
|
|
112
122
|
source: 'body',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { helpData } from './help-data';
|
|
2
|
-
import { tryParseJSON,
|
|
2
|
+
import { tryParseJSON, readFileContent } from './parsers';
|
|
3
3
|
import { buildNestedFieldMap } from '../utils/build-nested-field-map';
|
|
4
4
|
import type { NestedFieldMapping } from '../utils/build-nested-field-map';
|
|
5
5
|
import { restoreOriginalKeys } from '../utils/restore-original-keys';
|
|
@@ -14,6 +14,27 @@ export function getToolParamDefs(moduleName: string, toolName: string) {
|
|
|
14
14
|
return toolHelp.help?.parameter || {};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* 将 body 字段值按 schema 类型写入 target。
|
|
19
|
+
* - array 值:原样保留
|
|
20
|
+
* - string 字段:保留原始字符串(避免纯数字字符串被转 number)
|
|
21
|
+
* - 其他字段:走 tryParseJSON 以兼容嵌套 JSON
|
|
22
|
+
*/
|
|
23
|
+
function assignBodyValue(
|
|
24
|
+
target: Record<string, any>,
|
|
25
|
+
key: string,
|
|
26
|
+
value: any,
|
|
27
|
+
propType: string | undefined,
|
|
28
|
+
): void {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
target[key] = value;
|
|
31
|
+
} else if (propType === 'string') {
|
|
32
|
+
target[key] = value;
|
|
33
|
+
} else {
|
|
34
|
+
target[key] = tryParseJSON(value as string);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
/**
|
|
18
39
|
* 格式化参数
|
|
19
40
|
* 支持新的 --key value 扁平格式,同时向后兼容旧的 --path/--query JSON 格式。
|
|
@@ -51,8 +72,20 @@ export function formatParams(
|
|
|
51
72
|
'module', 'tool', 'help', 'short', 'verbose', 'h', 'v',
|
|
52
73
|
]);
|
|
53
74
|
|
|
75
|
+
// --body-file: 从指定文件读取内容作为 body 字段正文(适合多行/长文本)。
|
|
76
|
+
// 仅当 tool 存在 string 类型的 body 字段时,flatten-tool-options 才注册该选项。
|
|
77
|
+
// restoreOriginalKeys 已把 commander 的 camelCase(bodyFile)还原为 body-file,这里兼容两种形态。
|
|
78
|
+
// 读取到内容后在最后统一写入 formatted.data.body(优先级最高)。
|
|
79
|
+
reservedKeys.add('body-file');
|
|
80
|
+
reservedKeys.add('bodyFile');
|
|
81
|
+
let bodyFromFile: string | undefined;
|
|
82
|
+
const bodyFileRef = (params['body-file'] ?? (params as any).bodyFile);
|
|
83
|
+
if (typeof bodyFileRef === 'string' && bodyFileRef.length > 0) {
|
|
84
|
+
bodyFromFile = readFileContent(bodyFileRef);
|
|
85
|
+
}
|
|
86
|
+
|
|
54
87
|
// --data 是 CLI 通用 fallback 选项(见 flatten-tool-options.ts,仅在 tool 有 body 时注册),
|
|
55
|
-
// 接收 JSON
|
|
88
|
+
// 接收 JSON 字符串作为 body 整体。
|
|
56
89
|
// 仅当 'data' 不是该 tool 真实的 path/query/body 字段名时才启用 fallback 语义,
|
|
57
90
|
// 否则按 swagger 定义当业务字段处理(保留 commit 120d557 引入的能力)。
|
|
58
91
|
//
|
|
@@ -68,7 +101,7 @@ export function formatParams(
|
|
|
68
101
|
if (dataIsFallback) {
|
|
69
102
|
reservedKeys.add('data');
|
|
70
103
|
if (typeof params.data === 'string' && params.data.length > 0) {
|
|
71
|
-
const parsed = tryParseJSON(
|
|
104
|
+
const parsed = tryParseJSON(params.data);
|
|
72
105
|
if (bodyIsArrayType && Array.isArray(parsed)) {
|
|
73
106
|
dataArrayFromFlag = parsed;
|
|
74
107
|
} else if (!bodyIsArrayType && parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
@@ -177,16 +210,7 @@ export function formatParams(
|
|
|
177
210
|
} else if (bodyProps[key]) {
|
|
178
211
|
// body 字段(无冲突,直接用原 key)
|
|
179
212
|
if (!formatted.data) formatted.data = {};
|
|
180
|
-
|
|
181
|
-
formatted.data[key] = value;
|
|
182
|
-
} else if (bodyProps[key].type === 'string') {
|
|
183
|
-
// schema 定义为 string 类型时,保持原始字符串,不做 JSON 解析
|
|
184
|
-
// 避免纯数字字符串(如 "123")被转为 number
|
|
185
|
-
// 支持 @file 语法:从文件读取长文本内容(避免 shell 长参数截断)
|
|
186
|
-
formatted.data[key] = tryReadFileRef(value as string);
|
|
187
|
-
} else {
|
|
188
|
-
formatted.data[key] = tryParseJSON(value as string);
|
|
189
|
-
}
|
|
213
|
+
assignBodyValue(formatted.data, key, value, bodyProps[key].type);
|
|
190
214
|
} else {
|
|
191
215
|
// 处理 d- 前缀(冲突时 CLI 以 d- 前缀传入)
|
|
192
216
|
const stripped = (key.startsWith('d-') || key.startsWith('d_'))
|
|
@@ -236,7 +260,7 @@ export function formatParams(
|
|
|
236
260
|
const originalKey = key.replace(/^d[-_]/, '');
|
|
237
261
|
if (bodyProps[originalKey]) {
|
|
238
262
|
if (!formatted.data) formatted.data = {};
|
|
239
|
-
formatted.data[originalKey]
|
|
263
|
+
assignBodyValue(formatted.data, originalKey, value, bodyProps[originalKey].type);
|
|
240
264
|
} else {
|
|
241
265
|
if (!formatted.query) formatted.query = {};
|
|
242
266
|
formatted.query[key] = typeof value === 'string' && !isNaN(Number(value)) ? Number(value) : value;
|
|
@@ -287,6 +311,13 @@ export function formatParams(
|
|
|
287
311
|
} else if (dataBaseFromFlag) {
|
|
288
312
|
formatted.data = { ...dataBaseFromFlag, ...(formatted.data || {}) };
|
|
289
313
|
}
|
|
314
|
+
|
|
315
|
+
// --body-file 优先级最高:覆盖 --body 逐字段值与 --data 中的 body,
|
|
316
|
+
// 确保「从文件读取正文」的显式意图最终生效。
|
|
317
|
+
if (bodyFromFile !== undefined) {
|
|
318
|
+
if (!formatted.data) formatted.data = {};
|
|
319
|
+
formatted.data.body = bodyFromFile;
|
|
320
|
+
}
|
|
290
321
|
}
|
|
291
322
|
|
|
292
323
|
// formatted.query 仅在用户真传了 query 值时才存在;toolFunction 第 1 形参形态
|
package/client/lib/parsers.ts
CHANGED
|
@@ -21,37 +21,31 @@ export function tryParseJSON(str: string | boolean | undefined): any {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* 注意:一旦用户显式使用 @ 前缀,就表示其意图是读取文件/stdin。
|
|
27
|
-
* 此时若文件不存在或读取失败,应直接报错退出,避免把 "@/path/xxx"
|
|
28
|
-
* 字面量当作正文发送出去导致数据错误。
|
|
24
|
+
* 从指定路径读取文件内容(用于 --body-file)。
|
|
25
|
+
* 支持 `-` 表示从 stdin 读取,读取失败直接报错退出。
|
|
29
26
|
*/
|
|
30
|
-
export function
|
|
31
|
-
|
|
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) {
|
|
40
|
-
|
|
33
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
34
|
+
console.error(`从 stdin 读取失败: ${msg}`);
|
|
41
35
|
process.exit(1);
|
|
42
36
|
}
|
|
43
37
|
}
|
|
44
38
|
|
|
45
|
-
// @/path/to/file 表示从文件读取
|
|
46
|
-
if (!fs.existsSync(ref)) {
|
|
47
|
-
console.error(`文件不存在: ${ref}`);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
39
|
try {
|
|
52
|
-
return fs.readFileSync(
|
|
40
|
+
return fs.readFileSync(filePath, 'utf8').trim();
|
|
53
41
|
} catch (e) {
|
|
54
|
-
|
|
42
|
+
const { code } = e as NodeJS.ErrnoException;
|
|
43
|
+
if (code === 'ENOENT') {
|
|
44
|
+
console.error(`文件不存在: ${filePath}`);
|
|
45
|
+
} else {
|
|
46
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
47
|
+
console.error(`读取文件失败: ${filePath} - ${msg}`);
|
|
48
|
+
}
|
|
55
49
|
process.exit(1);
|
|
56
50
|
}
|
|
57
51
|
}
|
package/package.json
CHANGED
package/skills-template/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cnb-api
|
|
3
|
-
description: CNB
|
|
3
|
+
description: CNB 平台交互命令,支持 Issue/PR 评论、提交 PR、CI 日志查询、代码仓库/制品库读写等操作。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# cnb-api
|
|
@@ -12,12 +12,17 @@ description: CNB 平台交互命令,支持仓库、Issue、PR、流水线、
|
|
|
12
12
|
<$QUICK_COMMANDS$>
|
|
13
13
|
|
|
14
14
|
注意事项:
|
|
15
|
-
- **链接结构**:Issue 链接格式为 `<host>/<slug>/-/issues/<number>`,PR 链接格式为 `<host>/<slug>/-/pulls/<number>`。在生成或引用链接时请遵循此结构。
|
|
16
15
|
- **参数自动识别**:快捷命令中的 Issue/PR 编号会自动从环境变量识别,无需额外传递。
|
|
17
|
-
-
|
|
18
|
-
-
|
|
16
|
+
- **默认仅需摘要**:默认会精简响应输出结果,添加 `--verbose` 输出完整数据。
|
|
17
|
+
- **单引号传参**:当 bash 的参数为多行文本时,使用单引号可减少防止命令注入攻击。
|
|
19
18
|
- **快捷命令适用范围**: 快捷命令只能操作当前仓库的当前 Issue/PR,跨仓库或跨编号操作请参考 `更多 API`。
|
|
20
|
-
-
|
|
19
|
+
- **关于提及和召唤**: 评论中直接 @npc 会召唤 npc 干活,如果只提及不召唤,应使用反引号包裹 `@npc`。
|
|
20
|
+
|
|
21
|
+
## 常用链接
|
|
22
|
+
|
|
23
|
+
在生成链接时请遵循下面的结构:
|
|
24
|
+
- Issue: `<host>/<slug>/-/issues/<number>`
|
|
25
|
+
- PR: `<host>/<slug>/-/pulls/<number>`
|
|
21
26
|
|
|
22
27
|
## 更多 API
|
|
23
28
|
|