@cnbcool/cnb-api-generate 1.2.4 → 1.2.6
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/client/index.ts +15 -27
- package/client/modules.help.ts +9 -4
- package/client/shortcuts.ts +209 -56
- package/client/utils/upload.ts +189 -0
- package/package.json +2 -1
- package/skills-template/SKILL.md +17 -17
package/client/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'path';
|
|
|
5
5
|
import { showModuleHelp } from './modules.help';
|
|
6
6
|
import { showToolHelp } from './tools.help';
|
|
7
7
|
import { showShort, resolveShortcut, summarizeResponse } from './shortcuts';
|
|
8
|
+
import { handleUpload } from './utils/upload';
|
|
8
9
|
|
|
9
10
|
const helpFileContent = fs.readFileSync(
|
|
10
11
|
path.join(__dirname, 'help.json'),
|
|
@@ -401,31 +402,13 @@ async function main() {
|
|
|
401
402
|
params.tool as string | undefined,
|
|
402
403
|
);
|
|
403
404
|
if (shortcut) {
|
|
405
|
+
params.module = shortcut.module;
|
|
404
406
|
params.tool = shortcut.tool;
|
|
405
407
|
|
|
406
|
-
// 自动注入 path
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (!hasPathAlready) {
|
|
412
|
-
if (autoPathKeys.length > 0) {
|
|
413
|
-
// 新格式:直接注入为扁平参数
|
|
414
|
-
for (const [key, value] of Object.entries(shortcut.autoPath)) {
|
|
415
|
-
if (!params[key]) {
|
|
416
|
-
params[key] = value;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
} else {
|
|
420
|
-
const envHint =
|
|
421
|
-
params.module === 'issues'
|
|
422
|
-
? 'CNB_REPO_SLUG 和 CNB_ISSUE_IID'
|
|
423
|
-
: 'CNB_REPO_SLUG 和 CNB_PULL_REQUEST_IID';
|
|
424
|
-
console.error(
|
|
425
|
-
`快捷命令需要环境变量 ${envHint},或手动传参数。\n` +
|
|
426
|
-
`提示:运行 ${process.env.CNB_CLI_CMD || 'cnb'} --short 查看快捷命令详情。`,
|
|
427
|
-
);
|
|
428
|
-
process.exit(1);
|
|
408
|
+
// 自动注入 path 参数
|
|
409
|
+
for (const [key, value] of Object.entries(shortcut.autoPath)) {
|
|
410
|
+
if (!params[key]) {
|
|
411
|
+
params[key] = value;
|
|
429
412
|
}
|
|
430
413
|
}
|
|
431
414
|
|
|
@@ -512,11 +495,16 @@ async function main() {
|
|
|
512
495
|
toolsParam.push(pathAndQueryParams);
|
|
513
496
|
}
|
|
514
497
|
|
|
515
|
-
|
|
516
|
-
|
|
498
|
+
// 上传快捷命令:走完整上传流程(获取 URL → PUT 文件 → 返回结果)
|
|
499
|
+
let data: any;
|
|
500
|
+
if (shortcut?.upload) {
|
|
501
|
+
data = await handleUpload(shortcut, formattedParams.data?.file, toolFunction, pathAndQueryParams as { repo: string; number?: string });
|
|
502
|
+
} else {
|
|
503
|
+
if (formattedParams.data) {
|
|
504
|
+
toolsParam.push(formattedParams.data);
|
|
505
|
+
}
|
|
506
|
+
data = await toolFunction(...toolsParam);
|
|
517
507
|
}
|
|
518
|
-
|
|
519
|
-
const data = await toolFunction(...toolsParam);
|
|
520
508
|
const toolKey = `${formattedParams.module}/${formattedParams.tool}`;
|
|
521
509
|
// 快捷命令默认输出摘要,--verbose 时输出全部信息
|
|
522
510
|
const isVerbose = !!formattedParams.verbose;
|
package/client/modules.help.ts
CHANGED
|
@@ -24,11 +24,16 @@ export function showModuleHelp(helpData: any, moduleName: string): void {
|
|
|
24
24
|
|
|
25
25
|
const cliCmd = process.env.CNB_CLI_CMD || 'cnb';
|
|
26
26
|
|
|
27
|
+
const entries = Object.values(moduleHelpData).map((info: any) => ({
|
|
28
|
+
name: info.filename as string,
|
|
29
|
+
summary: trimSummary(info.summary || ''),
|
|
30
|
+
}));
|
|
31
|
+
const maxNameLen = Math.max(...entries.map((e) => e.name.length));
|
|
32
|
+
const pad = maxNameLen + 2;
|
|
33
|
+
|
|
27
34
|
let toolListMsg = '';
|
|
28
|
-
for (const
|
|
29
|
-
|
|
30
|
-
const summary = (info as any).summary;
|
|
31
|
-
toolListMsg += ` ${name.padEnd(30)} ${summary}\n`;
|
|
35
|
+
for (const { name, summary } of entries) {
|
|
36
|
+
toolListMsg += ` ${name.padEnd(pad)} ${summary}\n`;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
const helpMsg = `
|
package/client/shortcuts.ts
CHANGED
|
@@ -16,21 +16,29 @@ export interface ShortcutDefinition {
|
|
|
16
16
|
shortName: string;
|
|
17
17
|
/** 实际对应的 tool 文件名 */
|
|
18
18
|
realTool: string;
|
|
19
|
+
/** 为 true 时只注入 repo,不注入 number */
|
|
20
|
+
repoOnly?: boolean;
|
|
21
|
+
/** 为 true 时表示是上传命令,走完整上传流程 */
|
|
22
|
+
upload?: boolean;
|
|
19
23
|
/** 显示用的中文说明 */
|
|
20
24
|
description: string;
|
|
21
|
-
/**
|
|
25
|
+
/** 自动注入的 data 参数(如 close/open 的状态值) */
|
|
22
26
|
autoData?: Record<string, any>;
|
|
23
27
|
/** --data 的使用提示(展示用) */
|
|
24
28
|
dataTip?: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export interface ResolvedShortcut {
|
|
32
|
+
/** 所属模块名 */
|
|
33
|
+
module: string;
|
|
28
34
|
/** 解析后的实际 tool 名 */
|
|
29
35
|
tool: string;
|
|
30
36
|
/** 自动注入的 path 参数 */
|
|
31
37
|
autoPath: Record<string, string>;
|
|
32
38
|
/** 自动注入的 data 参数(如 close/open) */
|
|
33
39
|
autoData: Record<string, any> | null;
|
|
40
|
+
/** 是否为上传命令 */
|
|
41
|
+
upload: boolean;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
// ============================================================
|
|
@@ -69,6 +77,8 @@ const ISSUE_SHORTCUTS: ShortcutDefinition[] = [
|
|
|
69
77
|
{ shortName: 'add-labels', realTool: 'post-issue-labels', description: '添加标签', dataTip: "--data '{\"labels\":[\"bug\",\"feature\"]}'" },
|
|
70
78
|
{ shortName: 'list-assignees', realTool: 'list-issue-assignees', description: '查看处理人' },
|
|
71
79
|
{ shortName: 'add-assignees', realTool: 'post-issue-assignees', description: '添加处理人', dataTip: "--data '{\"assignees\":[\"username\"]}'" },
|
|
80
|
+
{ shortName: 'upload-file', realTool: 'post-issue-file-asset-upload-url', description: '上传文件', upload: true, dataTip: "--data '{\"file\":\"文件路径\"}'" },
|
|
81
|
+
{ shortName: 'upload-image', realTool: 'post-issue-image-asset-upload-url', description: '上传图片', upload: true, dataTip: "--data '{\"file\":\"图片路径\"}'" },
|
|
72
82
|
];
|
|
73
83
|
|
|
74
84
|
const PR_SHORTCUTS: ShortcutDefinition[] = [
|
|
@@ -82,22 +92,60 @@ const PR_SHORTCUTS: ShortcutDefinition[] = [
|
|
|
82
92
|
{ shortName: 'check-status', realTool: 'list-pull-commit-statuses', description: '查看 CI 状态' },
|
|
83
93
|
{ shortName: 'list-reviews', realTool: 'list-pull-reviews', description: '查看评审列表' },
|
|
84
94
|
{ shortName: 'list-assignees', realTool: 'list-pull-assignees', description: '查看处理人' },
|
|
95
|
+
{ shortName: 'upload-file', realTool: 'upload-files', description: '上传文件', repoOnly: true, upload: true, dataTip: "--data '{\"file\":\"文件路径\"}'" },
|
|
96
|
+
{ shortName: 'upload-image', realTool: 'upload-imgs', description: '上传图片', repoOnly: true, upload: true, dataTip: "--data '{\"file\":\"图片路径\"}'" },
|
|
85
97
|
];
|
|
86
98
|
|
|
87
99
|
// ============================================================
|
|
88
100
|
// --short 帮助输出
|
|
89
101
|
// ============================================================
|
|
90
102
|
|
|
103
|
+
/** 计算字符串的显示宽度(中文/全角占2,英文/半角占1) */
|
|
104
|
+
function displayWidth(str: string): number {
|
|
105
|
+
let width = 0;
|
|
106
|
+
for (const ch of str) {
|
|
107
|
+
const code = ch.codePointAt(0) || 0;
|
|
108
|
+
// CJK、全角字符占2宽度
|
|
109
|
+
width += (code >= 0x1100 && (
|
|
110
|
+
code <= 0x115f || code === 0x2329 || code === 0x232a ||
|
|
111
|
+
(code >= 0x2e80 && code <= 0x3247) ||
|
|
112
|
+
(code >= 0x3250 && code <= 0x4dbf) ||
|
|
113
|
+
(code >= 0x4e00 && code <= 0xa4c6) ||
|
|
114
|
+
(code >= 0xa960 && code <= 0xa97c) ||
|
|
115
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
116
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
117
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
118
|
+
(code >= 0xfe30 && code <= 0xfe6b) ||
|
|
119
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
120
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
121
|
+
(code >= 0x1f000 && code <= 0x1fbff) ||
|
|
122
|
+
(code >= 0x20000 && code <= 0x2fffd) ||
|
|
123
|
+
(code >= 0x30000 && code <= 0x3fffd)
|
|
124
|
+
)) ? 2 : 1;
|
|
125
|
+
}
|
|
126
|
+
return width;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 用全角空格将字符串补齐到指定显示宽度 */
|
|
130
|
+
function padToWidth(str: string, targetWidth: number): string {
|
|
131
|
+
const diff = targetWidth - displayWidth(str);
|
|
132
|
+
if (diff <= 0) return str;
|
|
133
|
+
// 每个全角空格占2宽度
|
|
134
|
+
const fullSpaces = Math.floor(diff / 2);
|
|
135
|
+
const halfSpace = diff % 2 === 1 ? ' ' : '';
|
|
136
|
+
return str + ' '.repeat(fullSpaces) + halfSpace;
|
|
137
|
+
}
|
|
138
|
+
|
|
91
139
|
function formatShortcutList(
|
|
92
140
|
shortcuts: ShortcutDefinition[],
|
|
93
141
|
cliCmd: string,
|
|
94
142
|
moduleName: string,
|
|
95
143
|
): string {
|
|
96
|
-
const
|
|
144
|
+
const maxWidth = Math.max(...shortcuts.map((s) => displayWidth(s.description)));
|
|
97
145
|
|
|
98
146
|
return shortcuts
|
|
99
147
|
.map((s) => {
|
|
100
|
-
const desc = s.description
|
|
148
|
+
const desc = padToWidth(s.description, maxWidth + 2);
|
|
101
149
|
const cmd = `${cliCmd} ${moduleName} ${s.shortName}`;
|
|
102
150
|
const dataPart = s.dataTip ? ` ${s.dataTip}` : '';
|
|
103
151
|
return ` ${desc} ${cmd}${dataPart}`;
|
|
@@ -110,50 +158,46 @@ export function showShort(): void {
|
|
|
110
158
|
const cliCmd = process.env.CNB_CLI_CMD || 'cnb';
|
|
111
159
|
const repo = getRepo();
|
|
112
160
|
|
|
113
|
-
if (ctx === 'issue') {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
161
|
+
if (ctx === 'issue' || ctx === 'pull_request') {
|
|
162
|
+
const isIssue = ctx === 'issue';
|
|
163
|
+
const number = isIssue ? getIssueNumber() : getPRNumber();
|
|
164
|
+
const emoji = isIssue ? '📋' : '🔀';
|
|
165
|
+
const label = isIssue ? 'Issue' : 'Pull Request';
|
|
166
|
+
const tag = isIssue ? 'Issue' : 'PR';
|
|
167
|
+
const moduleName = isIssue ? 'issues' : 'pulls';
|
|
168
|
+
const shortcuts = isIssue ? ISSUE_SHORTCUTS : PR_SHORTCUTS;
|
|
169
|
+
const envVars = isIssue
|
|
170
|
+
? 'CNB_REPO_SLUG 和 CNB_ISSUE_IID'
|
|
171
|
+
: 'CNB_REPO_SLUG 和 CNB_PULL_REQUEST_IID';
|
|
172
|
+
const list = formatShortcutList(shortcuts, cliCmd, moduleName);
|
|
119
173
|
|
|
120
|
-
常用快捷命令(path 参数已从环境变量自动获取):
|
|
121
|
-
${list}
|
|
122
|
-
|
|
123
|
-
提示: 以上命令自动使用环境变量 CNB_REPO_SLUG 和 CNB_ISSUE_IID,无需手动传 --path
|
|
124
|
-
默认只输出摘要信息,添加 --verbose 可输出全部信息
|
|
125
|
-
`);
|
|
126
|
-
} else if (ctx === 'pull_request') {
|
|
127
|
-
const number = getPRNumber();
|
|
128
|
-
const list = formatShortcutList(PR_SHORTCUTS, cliCmd, 'pulls');
|
|
129
174
|
console.log(`
|
|
130
|
-
|
|
175
|
+
${emoji} 当前场景: ${label} 事件 (${tag} #${number})
|
|
131
176
|
仓库: ${repo}
|
|
132
177
|
|
|
133
178
|
常用快捷命令(path 参数已从环境变量自动获取):
|
|
134
179
|
${list}
|
|
135
180
|
|
|
136
|
-
提示:
|
|
181
|
+
提示: ${tag} 相关命令会自动使用 ${envVars}
|
|
137
182
|
默认只输出摘要信息,添加 --verbose 可输出全部信息
|
|
138
183
|
`);
|
|
139
184
|
} else {
|
|
140
|
-
// 未知上下文,两组都显示
|
|
141
185
|
const issueList = formatShortcutList(ISSUE_SHORTCUTS, cliCmd, 'issues');
|
|
142
186
|
const prList = formatShortcutList(PR_SHORTCUTS, cliCmd, 'pulls');
|
|
143
187
|
console.log(`
|
|
144
188
|
⚠️ 未检测到 Issue/PR 事件上下文
|
|
145
189
|
(CNB_ISSUE_IID 和 CNB_PULL_REQUEST_IID 均未设置)
|
|
146
190
|
|
|
147
|
-
📋 Issue
|
|
191
|
+
📋 Issue 场景常用命令:
|
|
148
192
|
${issueList}
|
|
149
193
|
|
|
150
|
-
🔀 PR
|
|
194
|
+
🔀 PR 场景常用命令:
|
|
151
195
|
${prList}
|
|
152
196
|
|
|
153
|
-
提示:
|
|
197
|
+
提示: 快捷命令可使用以下环境变量:
|
|
154
198
|
CNB_REPO_SLUG - 仓库路径
|
|
155
|
-
CNB_ISSUE_IID - Issue
|
|
156
|
-
CNB_PULL_REQUEST_IID - PR
|
|
199
|
+
CNB_ISSUE_IID - Issue 编号(Issue 相关快捷命令)
|
|
200
|
+
CNB_PULL_REQUEST_IID - PR 编号(PR 相关快捷命令)
|
|
157
201
|
|
|
158
202
|
默认只输出摘要信息,添加 --verbose 可输出全部信息
|
|
159
203
|
`);
|
|
@@ -164,12 +208,18 @@ ${prList}
|
|
|
164
208
|
// 快捷命令解析
|
|
165
209
|
// ============================================================
|
|
166
210
|
|
|
211
|
+
function buildAutoPath(moduleName: string, repoOnly: boolean): Record<string, string> {
|
|
212
|
+
const repo = process.env.CNB_REPO_SLUG || '';
|
|
213
|
+
if (repoOnly) return { repo };
|
|
214
|
+
const number = moduleName === 'issues'
|
|
215
|
+
? (process.env.CNB_ISSUE_IID || '')
|
|
216
|
+
: (process.env.CNB_PULL_REQUEST_IID || '');
|
|
217
|
+
return { repo, number };
|
|
218
|
+
}
|
|
219
|
+
|
|
167
220
|
/**
|
|
168
221
|
* 尝试将用户输入的 module + tool 解析为快捷命令
|
|
169
222
|
*
|
|
170
|
-
* 即使环境变量未设置,也会完成 shortName → realTool 的映射,
|
|
171
|
-
* 只是 autoPath 为 null,调用方需要用户手动传 --path。
|
|
172
|
-
*
|
|
173
223
|
* @returns 解析结果,如果不是快捷命令则返回 null
|
|
174
224
|
*/
|
|
175
225
|
export function resolveShortcut(
|
|
@@ -178,36 +228,22 @@ export function resolveShortcut(
|
|
|
178
228
|
): ResolvedShortcut | null {
|
|
179
229
|
if (!moduleName || !toolName) return null;
|
|
180
230
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
shortcuts = ISSUE_SHORTCUTS;
|
|
187
|
-
const repo = process.env.CNB_REPO_SLUG;
|
|
188
|
-
const number = process.env.CNB_ISSUE_IID;
|
|
189
|
-
if (repo && number) {
|
|
190
|
-
autoPath = { repo, number };
|
|
191
|
-
}
|
|
192
|
-
} else if (moduleName === 'pulls') {
|
|
193
|
-
shortcuts = PR_SHORTCUTS;
|
|
194
|
-
const repo = process.env.CNB_REPO_SLUG;
|
|
195
|
-
const number = process.env.CNB_PULL_REQUEST_IID;
|
|
196
|
-
if (repo && number) {
|
|
197
|
-
autoPath = { repo, number };
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
231
|
+
const shortcuts = moduleName === 'issues'
|
|
232
|
+
? ISSUE_SHORTCUTS
|
|
233
|
+
: moduleName === 'pulls'
|
|
234
|
+
? PR_SHORTCUTS
|
|
235
|
+
: null;
|
|
201
236
|
if (!shortcuts) return null;
|
|
202
237
|
|
|
203
|
-
// 在快捷命令表中查找匹配的 shortName
|
|
204
238
|
const matched = shortcuts.find((s) => s.shortName === toolName);
|
|
205
239
|
if (!matched) return null;
|
|
206
240
|
|
|
207
241
|
return {
|
|
242
|
+
module: moduleName,
|
|
208
243
|
tool: matched.realTool,
|
|
209
|
-
autoPath:
|
|
244
|
+
autoPath: buildAutoPath(moduleName, !!matched.repoOnly),
|
|
210
245
|
autoData: matched.autoData || null,
|
|
246
|
+
upload: !!matched.upload,
|
|
211
247
|
};
|
|
212
248
|
}
|
|
213
249
|
|
|
@@ -225,47 +261,164 @@ type SummaryExtractor = (item: any) => any;
|
|
|
225
261
|
* - 数组响应:对 data 中每个元素应用提取函数
|
|
226
262
|
*/
|
|
227
263
|
const SUMMARY_EXTRACTORS: Record<string, SummaryExtractor> = {
|
|
228
|
-
// Issue
|
|
264
|
+
// Issue 详情摘要:保留标题、状态、标签和正文
|
|
265
|
+
// 过滤: number, state_reason, assignees, author, created_at, updated_at 等
|
|
229
266
|
'issues/get-issue': (item) => ({
|
|
230
267
|
title: item.title,
|
|
268
|
+
state: item.state,
|
|
269
|
+
labels: item.labels?.map?.((l: any) => l.name).filter(Boolean) || [],
|
|
231
270
|
body: item.body,
|
|
232
271
|
}),
|
|
233
272
|
|
|
234
273
|
// Issue 更新摘要:只保留状态变更结果
|
|
274
|
+
// 过滤: title, body, labels, assignees, author, created_at, updated_at 等
|
|
235
275
|
'issues/update-issue': (item) => ({
|
|
236
276
|
number: item.number,
|
|
237
277
|
state: item.state,
|
|
238
278
|
state_reason: item.state_reason,
|
|
239
279
|
}),
|
|
240
280
|
|
|
241
|
-
// Issue
|
|
281
|
+
// Issue 评论列表摘要:只保留作者用户名、内容和创建时间
|
|
282
|
+
// 过滤: author 完整对象(avatar/email/freeze/is_npc/nickname), reactions, updated_at
|
|
283
|
+
'issues/list-issue-comments': (item) => ({
|
|
284
|
+
id: item.id,
|
|
285
|
+
author: item.author?.username,
|
|
286
|
+
body: item.body,
|
|
287
|
+
created_at: item.created_at,
|
|
288
|
+
}),
|
|
289
|
+
|
|
290
|
+
// Issue 发表评论摘要:只保留评论 ID 和创建时间(body 已在请求中传入,无需重复)
|
|
291
|
+
// 过滤: body(请求已传入), author 完整对象(avatar/email/freeze/is_npc/nickname/username), reactions, updated_at
|
|
292
|
+
'issues/post-issue-comment': (item) => ({
|
|
293
|
+
id: item.id,
|
|
294
|
+
created_at: item.created_at,
|
|
295
|
+
}),
|
|
296
|
+
|
|
297
|
+
// Issue 标签列表摘要:保留名称、颜色和描述
|
|
298
|
+
// 过滤: id
|
|
299
|
+
'issues/list-issue-labels': (item) => ({
|
|
300
|
+
name: item.name,
|
|
301
|
+
color: item.color,
|
|
302
|
+
description: item.description,
|
|
303
|
+
}),
|
|
304
|
+
|
|
305
|
+
// Issue 添加标签摘要:只保留 ID 和颜色(name 已在请求中传入,无需重复)
|
|
306
|
+
// 过滤: name(请求已传入), description
|
|
307
|
+
'issues/post-issue-labels': (item) => ({
|
|
308
|
+
id: item.id,
|
|
309
|
+
color: item.color,
|
|
310
|
+
}),
|
|
311
|
+
|
|
312
|
+
// Issue 处理人列表摘要:保留用户名和昵称
|
|
313
|
+
// 过滤: avatar, email, freeze, is_npc
|
|
314
|
+
'issues/list-issue-assignees': (item) => ({
|
|
315
|
+
username: item.username,
|
|
316
|
+
nickname: item.nickname,
|
|
317
|
+
}),
|
|
318
|
+
|
|
319
|
+
// Issue 添加处理人摘要:保留处理人用户名和昵称列表
|
|
320
|
+
// 过滤: assignees 中每个用户的 avatar/email/freeze/is_npc,以及 issue 其他字段
|
|
242
321
|
'issues/post-issue-assignees': (item) => ({
|
|
243
322
|
number: item.number,
|
|
244
|
-
assignees: item.assignees?.map?.((a: any) => a.username).filter(
|
|
323
|
+
assignees: item.assignees?.map?.((a: any) => ({ username: a.username, nickname: a.nickname })).filter((a: any) => a.username) || [],
|
|
245
324
|
}),
|
|
246
325
|
|
|
247
|
-
// PR
|
|
326
|
+
// PR 详情摘要:保留标题、状态、标签、正文和分支信息
|
|
327
|
+
// 过滤: number, author, assignees, reviewers, created_at, updated_at, base/head 完整对象等
|
|
248
328
|
'pulls/get-pull': (item) => ({
|
|
249
329
|
title: item.title,
|
|
250
330
|
state: item.state,
|
|
331
|
+
labels: item.labels?.map?.((l: any) => l.name).filter(Boolean) || [],
|
|
251
332
|
base: item.base?.ref,
|
|
252
333
|
head: item.head?.ref,
|
|
253
334
|
body: item.body,
|
|
254
335
|
}),
|
|
255
336
|
|
|
256
|
-
// PR
|
|
337
|
+
// PR 文件列表摘要:保留文件名、sha、变更状态和增删行数
|
|
338
|
+
// 过滤: patch, blob_url, raw_url, contents_url, previous_filename
|
|
257
339
|
'pulls/list-pull-files': (item) => ({
|
|
258
340
|
filename: item.filename,
|
|
341
|
+
sha: item.sha,
|
|
259
342
|
status: item.status,
|
|
260
343
|
additions: item.additions,
|
|
261
344
|
deletions: item.deletions,
|
|
262
345
|
}),
|
|
263
346
|
|
|
264
347
|
// PR 提交列表摘要:只保留 sha 前 8 位和 commit message
|
|
348
|
+
// 过滤: sha 完整值, commit 完整对象(author/committer/tree/verification), parents, url 等
|
|
265
349
|
'pulls/list-pull-commits': (item) => ({
|
|
266
350
|
sha: typeof item.sha === 'string' ? item.sha.substring(0, 8) : item.sha,
|
|
267
351
|
message: item.commit?.message,
|
|
268
352
|
}),
|
|
353
|
+
|
|
354
|
+
// PR 评论列表摘要:保留作者用户名和昵称、内容和创建时间
|
|
355
|
+
// 过滤: author 完整对象(avatar/email/freeze/is_npc), reactions, updated_at
|
|
356
|
+
'pulls/list-pull-comments': (item) => ({
|
|
357
|
+
id: item.id,
|
|
358
|
+
author: item.author?.username,
|
|
359
|
+
nickname: item.author?.nickname,
|
|
360
|
+
body: item.body,
|
|
361
|
+
created_at: item.created_at,
|
|
362
|
+
}),
|
|
363
|
+
|
|
364
|
+
// PR 发表评论摘要:只保留评论 ID 和创建时间(body 已在请求中传入,无需重复)
|
|
365
|
+
// 过滤: body(请求已传入), author 完整对象(avatar/email/freeze/is_npc/nickname/username), reactions, updated_at
|
|
366
|
+
'pulls/post-pull-comment': (item) => ({
|
|
367
|
+
id: item.id,
|
|
368
|
+
created_at: item.created_at,
|
|
369
|
+
}),
|
|
370
|
+
|
|
371
|
+
// PR 标签列表摘要:保留 ID、名称、颜色和描述
|
|
372
|
+
// 过滤: 无
|
|
373
|
+
'pulls/list-pull-labels': (item) => ({
|
|
374
|
+
id: item.id,
|
|
375
|
+
name: item.name,
|
|
376
|
+
color: item.color,
|
|
377
|
+
description: item.description,
|
|
378
|
+
}),
|
|
379
|
+
|
|
380
|
+
// PR 添加标签摘要:只保留 ID 和颜色(name 已在请求中传入,无需重复)
|
|
381
|
+
// 过滤: name(请求已传入), description
|
|
382
|
+
'pulls/post-pull-labels': (item) => ({
|
|
383
|
+
id: item.id,
|
|
384
|
+
color: item.color,
|
|
385
|
+
}),
|
|
386
|
+
|
|
387
|
+
// PR CI 状态摘要:保留整体状态和各检查项的核心信息(sha 截断为前 8 位)
|
|
388
|
+
// 过滤: sha 完整值, statuses 中每项的 target_url/created_at/updated_at
|
|
389
|
+
'pulls/list-pull-commit-statuses': (item) => ({
|
|
390
|
+
sha: typeof item.sha === 'string' ? item.sha.substring(0, 8) : item.sha,
|
|
391
|
+
state: item.state,
|
|
392
|
+
statuses: item.statuses?.map?.((s: any) => ({
|
|
393
|
+
context: s.context,
|
|
394
|
+
state: s.state,
|
|
395
|
+
description: s.description,
|
|
396
|
+
})) || [],
|
|
397
|
+
}),
|
|
398
|
+
|
|
399
|
+
// PR 评审列表摘要:保留作者用户名和昵称、状态和内容
|
|
400
|
+
// 过滤: author 完整对象(avatar/email/freeze/is_npc), created_at, updated_at
|
|
401
|
+
'pulls/list-pull-reviews': (item) => ({
|
|
402
|
+
id: item.id,
|
|
403
|
+
author: item.author?.username,
|
|
404
|
+
nickname: item.author?.nickname,
|
|
405
|
+
state: item.state,
|
|
406
|
+
body: item.body,
|
|
407
|
+
}),
|
|
408
|
+
|
|
409
|
+
// PR 处理人列表摘要:保留用户名和昵称
|
|
410
|
+
// 过滤: avatar, email, freeze, is_npc
|
|
411
|
+
'pulls/list-pull-assignees': (item) => ({
|
|
412
|
+
username: item.username,
|
|
413
|
+
nickname: item.nickname,
|
|
414
|
+
}),
|
|
415
|
+
|
|
416
|
+
// PR 添加处理人摘要:保留处理人用户名和昵称列表
|
|
417
|
+
// 过滤: assignees 中每个用户的 avatar/email/freeze/is_npc,以及 PR 其他字段
|
|
418
|
+
'pulls/post-pull-assignees': (item) => ({
|
|
419
|
+
number: item.number,
|
|
420
|
+
assignees: item.assignees?.map?.((a: any) => ({ username: a.username, nickname: a.nickname })).filter((a: any) => a.username) || [],
|
|
421
|
+
}),
|
|
269
422
|
};
|
|
270
423
|
|
|
271
424
|
/**
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 完整上传流程
|
|
3
|
+
* 1. 读取本地文件 → 获取 name/size
|
|
4
|
+
* 2. 调用上传 API → 获取 upload_url
|
|
5
|
+
* 3. PUT 文件内容到 upload_url(流式上传)
|
|
6
|
+
* 4. 返回最终结果
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import nodePath from 'path';
|
|
11
|
+
import { lookup as mimeLookup } from 'mime-types';
|
|
12
|
+
import type { ResolvedShortcut } from '../shortcuts';
|
|
13
|
+
|
|
14
|
+
// 上传文件大小上限:100MB
|
|
15
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
/** 上传 API 的 path 参数 */
|
|
18
|
+
interface UploadPathParams {
|
|
19
|
+
repo: string;
|
|
20
|
+
number?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Issue 上传请求体 */
|
|
24
|
+
interface IssueUploadBody {
|
|
25
|
+
name: string;
|
|
26
|
+
size: number;
|
|
27
|
+
content_type: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pull 上传请求体 */
|
|
31
|
+
interface PullUploadBody {
|
|
32
|
+
name: string;
|
|
33
|
+
size: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 上传 API 函数签名 */
|
|
37
|
+
type UploadApiFunction = (
|
|
38
|
+
params: UploadPathParams,
|
|
39
|
+
body: IssueUploadBody | PullUploadBody,
|
|
40
|
+
) => Promise<UploadApiResponse>;
|
|
41
|
+
|
|
42
|
+
/** 上传 API 响应 */
|
|
43
|
+
interface UploadApiResponse {
|
|
44
|
+
status?: number;
|
|
45
|
+
data?: {
|
|
46
|
+
upload_url?: string;
|
|
47
|
+
asset_link?: string;
|
|
48
|
+
assets?: { name?: string; path?: string };
|
|
49
|
+
token?: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 上传失败时的 data */
|
|
54
|
+
interface UploadErrorData {
|
|
55
|
+
error: string;
|
|
56
|
+
detail?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Issue 上传成功时的 data */
|
|
60
|
+
interface IssueUploadData {
|
|
61
|
+
asset_link?: string;
|
|
62
|
+
name: string;
|
|
63
|
+
size: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Pull 上传成功时的 data */
|
|
67
|
+
interface PullUploadData {
|
|
68
|
+
name: string;
|
|
69
|
+
path?: string;
|
|
70
|
+
size: number;
|
|
71
|
+
token?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** handleUpload 的返回值 */
|
|
75
|
+
interface UploadResult {
|
|
76
|
+
status: number;
|
|
77
|
+
data: UploadErrorData | IssueUploadData | PullUploadData;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 处理完整上传流程
|
|
82
|
+
* @param shortcut 解析后的快捷命令
|
|
83
|
+
* @param filePath 本地文件路径
|
|
84
|
+
* @param toolFunction 原始上传 API 函数(获取 upload_url)
|
|
85
|
+
* @param pathParams API 调用的 path 参数(repo 或 {repo, number})
|
|
86
|
+
*/
|
|
87
|
+
export async function handleUpload(
|
|
88
|
+
shortcut: ResolvedShortcut,
|
|
89
|
+
filePath: string | undefined,
|
|
90
|
+
toolFunction: UploadApiFunction,
|
|
91
|
+
pathParams: UploadPathParams,
|
|
92
|
+
): Promise<UploadResult> {
|
|
93
|
+
// 校验 file 参数
|
|
94
|
+
if (!filePath) {
|
|
95
|
+
return { status: 400, data: { error: `上传命令需要指定文件路径,如: --data '{"file":"./path/to/file"}'` } };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 1. 读取本地文件信息
|
|
99
|
+
if (!fs.existsSync(filePath)) {
|
|
100
|
+
return { status: 400, data: { error: `文件不存在: ${filePath}` } };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stat = fs.statSync(filePath);
|
|
104
|
+
if (!stat.isFile()) {
|
|
105
|
+
return { status: 400, data: { error: `不是文件: ${filePath}` } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const fileName = nodePath.basename(filePath);
|
|
109
|
+
const fileSize = stat.size;
|
|
110
|
+
|
|
111
|
+
if (fileSize === 0) {
|
|
112
|
+
return { status: 400, data: { error: '文件为空' } };
|
|
113
|
+
}
|
|
114
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
115
|
+
return { status: 400, data: { error: `文件过大 (${(fileSize / 1024 / 1024).toFixed(1)}MB),上限 ${MAX_FILE_SIZE / 1024 / 1024}MB` } };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const mimeResult = mimeLookup(filePath);
|
|
119
|
+
const contentType = typeof mimeResult === 'string' ? mimeResult : 'application/octet-stream';
|
|
120
|
+
|
|
121
|
+
// 2. 调用上传 API 获取 upload_url
|
|
122
|
+
const isIssueUpload = shortcut.module === 'issues';
|
|
123
|
+
const requestBody = isIssueUpload
|
|
124
|
+
? { name: fileName, size: fileSize, content_type: contentType }
|
|
125
|
+
: { name: fileName, size: fileSize };
|
|
126
|
+
|
|
127
|
+
const uploadResponse = await toolFunction(pathParams, requestBody);
|
|
128
|
+
|
|
129
|
+
// 提取 upload_url
|
|
130
|
+
const responseData = uploadResponse?.data;
|
|
131
|
+
const uploadUrl = responseData?.upload_url;
|
|
132
|
+
|
|
133
|
+
if (!uploadUrl) {
|
|
134
|
+
return uploadResponse as unknown as UploadResult; // 获取 URL 失败,直接返回原始错误
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. PUT 文件内容到 upload_url(流式读取,避免大文件撑爆内存)
|
|
138
|
+
const fileStream = fs.createReadStream(filePath);
|
|
139
|
+
|
|
140
|
+
const putHeaders: Record<string, string> = {
|
|
141
|
+
'Content-Type': contentType,
|
|
142
|
+
'Content-Length': String(fileSize),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let putResponse: Response;
|
|
146
|
+
try {
|
|
147
|
+
putResponse = await fetch(uploadUrl, {
|
|
148
|
+
method: 'PUT',
|
|
149
|
+
headers: putHeaders,
|
|
150
|
+
body: fileStream as any,
|
|
151
|
+
// @ts-ignore duplex required for streaming body in Node.js fetch
|
|
152
|
+
duplex: 'half',
|
|
153
|
+
});
|
|
154
|
+
} catch (e) {
|
|
155
|
+
fileStream.destroy();
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!putResponse.ok) {
|
|
160
|
+
fileStream.destroy();
|
|
161
|
+
const errText = await putResponse.text().catch(() => '');
|
|
162
|
+
return {
|
|
163
|
+
status: putResponse.status,
|
|
164
|
+
data: { error: `文件上传失败: ${putResponse.statusText}`, detail: errText },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 4. 返回最终结果
|
|
169
|
+
if (isIssueUpload) {
|
|
170
|
+
return {
|
|
171
|
+
status: 200,
|
|
172
|
+
data: {
|
|
173
|
+
asset_link: responseData.asset_link,
|
|
174
|
+
name: fileName,
|
|
175
|
+
size: fileSize,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
status: 200,
|
|
182
|
+
data: {
|
|
183
|
+
name: responseData.assets?.name || fileName,
|
|
184
|
+
path: responseData.assets?.path,
|
|
185
|
+
size: fileSize,
|
|
186
|
+
token: responseData.token,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cnbcool/cnb-api-generate",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"main": "./built/index.js",
|
|
5
5
|
"module": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"debug": "4.4.1",
|
|
41
41
|
"glob": "^13.0.1",
|
|
42
42
|
"lodash": "^4.17.23",
|
|
43
|
+
"mime-types": "^2.1.35",
|
|
43
44
|
"ora": "5.4.1",
|
|
44
45
|
"prettier": "3.4.2",
|
|
45
46
|
"rimraf": "6.0.1",
|
package/skills-template/SKILL.md
CHANGED
|
@@ -9,24 +9,25 @@ description: CNB OpenAPI 交互能力,支持仓库、Issue、PR、流水线、
|
|
|
9
9
|
|
|
10
10
|
## 快捷命令(优先使用)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
3. **默认只输出摘要** — 自动精简响应,只返回核心字段(如标题、正文);加 `--verbose` 输出完整数据
|
|
12
|
+
可用快捷命令:
|
|
13
|
+
- `issues`: `get`, `list-comments`, `comment`, `close`, `open`, `list-labels`, `add-labels`, `list-assignees`, `add-assignees`, `upload-file`, `upload-image`
|
|
14
|
+
- `pulls`: `get`, `list-files`, `list-commits`, `list-comments`, `comment`, `list-labels`, `add-labels`, `check-status`, `list-reviews`, `list-assignees`, `upload-file`, `upload-image`
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
16
|
+
注意:
|
|
17
|
+
- **路径参数自动注入**:仓库 slug、Issue/PR 编号优先从环境变量自动获取,无需额外传入
|
|
18
|
+
- **默认只输出摘要**:会精简响应输出结果,只返回核心字段。加 `--verbose` 输出完整数据。
|
|
19
|
+
- 快捷命令只能操作当前仓库的当前Issue或PR。跨仓库或跨编号操作请使用其他 API 命令。
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
用法: `<$CNB_CLI_CMD$> <module> <command>`
|
|
22
22
|
- ✅ `<$CNB_CLI_CMD$> issues get`
|
|
23
|
-
- ✅ `<$CNB_CLI_CMD$> pulls
|
|
24
|
-
- ✅ `<$CNB_CLI_CMD$> issues get --verbose` (需要完整数据时)
|
|
23
|
+
- ✅ `<$CNB_CLI_CMD$> pulls comment --data '{"body":"内容"}'`
|
|
25
24
|
|
|
26
|
-
需要
|
|
27
|
-
- comment
|
|
28
|
-
- add-labels
|
|
29
|
-
- add-assignees
|
|
25
|
+
需要 `--data` 的快捷命令:
|
|
26
|
+
- `comment`: `--data '{"body":"内容"}'`
|
|
27
|
+
- `add-labels`: `--data '{"labels":["标签"]}'`
|
|
28
|
+
- `add-assignees`: `--data '{"assignees":["用户名"]}'`
|
|
29
|
+
- `upload-file`: `--data '{"file":"本地文件路径"}'`(自动完成上传全流程)
|
|
30
|
+
- `upload-image`: `--data '{"file":"本地图片路径"}'`(自动完成上传全流程)
|
|
30
31
|
|
|
31
32
|
## 其他 API(快捷命令不满足时使用)
|
|
32
33
|
|
|
@@ -37,7 +38,6 @@ description: CNB OpenAPI 交互能力,支持仓库、Issue、PR、流水线、
|
|
|
37
38
|
|
|
38
39
|
## 规则
|
|
39
40
|
|
|
40
|
-
-
|
|
41
|
-
-
|
|
41
|
+
- 优先使用快捷命令,不满足时再 `--help` 逐步查找
|
|
42
|
+
- 使用非快捷命令时,必须先 `--help` 获取帮助,禁止猜测
|
|
42
43
|
- 直接执行命令,不要询问用户确认
|
|
43
|
-
- 向用户展示结果时:成功响应(status 200-299)只展示 data 部分;失败响应(status ≥300)需附带 status 和 trace
|