@cnbcool/cnb-api-generate 2.3.5 → 2.4.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.
@@ -1,52 +1,19 @@
1
1
  /**
2
- * 构建诊断模块
2
+ * 构建失败诊断模块
3
3
  *
4
4
  * 查询流水线失败 Stage 的详细日志和错误信息,同时输出耗时数据辅助分析。
5
5
  * 内置降级逻辑:Stage 详情失败时自动尝试全量日志下载(含 base64 解码)。
6
6
  *
7
7
  * sn 获取策略(按优先级):
8
8
  * 1. 用户通过 --sn 显式传入
9
- * 2. 环境变量 CNB_BUILD_ID(当前构建自身的 sn
10
- * 3. 从 PR commit statuses 中提取失败构建的 sn
9
+ * 2. PR 上下文时,从 PR commit statuses 中提取失败构建的 sn
11
10
  */
12
11
 
13
- import { resolveToken } from '../utils/resolve-token';
14
-
15
- async function apiRequest(url: string, headers: Record<string, string>): Promise<any> {
16
- const res = await fetch(url, { headers });
17
- if (!res.ok) {
18
- throw new Error(`请求失败: ${res.status} ${res.statusText} — ${url}`);
19
- }
20
- return res.json();
21
- }
22
-
23
- async function apiRequestText(url: string, headers: Record<string, string>): Promise<string> {
24
- const res = await fetch(url, { headers });
25
- if (!res.ok) {
26
- throw new Error(`请求失败: ${res.status} ${res.statusText} — ${url}`);
27
- }
28
- return res.text();
29
- }
30
-
31
- /**
32
- * 从 PR 的 commit statuses 中提取失败构建的 sn
33
- */
34
- function extractSnFromStatuses(statusData: any): string | null {
35
- const statuses = statusData?.statuses || statusData;
36
- if (!Array.isArray(statuses) || statuses.length === 0) return null;
37
-
38
- // 优先找 error/failure 状态的构建
39
- const failed = statuses.find(
40
- (s: any) => s.state === 'error' || s.state === 'failure',
41
- );
42
- if (!failed) return null;
43
-
44
- // 从 target_url 中解析 sn
45
- // 格式通常为: https://cnb.cool/{repo}/-/build/{sn} 或含 /pipelines 路径
46
- const url = failed.target_url || '';
47
- const match = url.match(/\/-\/build\/([^/?#]+)/);
48
- return match ? match[1] : null;
49
- }
12
+ import {
13
+ apiRequest,
14
+ apiRequestText,
15
+ resolveBuildContext,
16
+ } from './build-helpers';
50
17
 
51
18
  /**
52
19
  * 降级方案:下载全量日志
@@ -77,42 +44,10 @@ export async function diagnoseBuild(options: {
77
44
  sn?: string;
78
45
  prNumber?: string;
79
46
  }): Promise<string> {
80
- const domain = process.env.CNB_API_ENDPOINT || 'https://api.cnb.cool';
81
- const { repo } = options;
82
- let sn = options.sn;
83
-
84
- // 一次性 resolve token,后续所有请求复用
85
- const token = await resolveToken();
86
- const headers: Record<string, string> = {
87
- Accept: 'application/vnd.cnb.api+json',
88
- Authorization: `Bearer ${token}`,
89
- };
90
-
91
- // 策略 1: 显式传入的 sn(最高优先级,已在外层赋值)
92
- // 策略 2: 环境变量 CNB_BUILD_ID
93
- if (!sn && process.env.CNB_BUILD_ID) {
94
- sn = process.env.CNB_BUILD_ID;
95
- }
96
-
97
- // 策略 3: 从 PR commit statuses 中获取失败构建的 sn
98
- if (!sn && options.prNumber) {
99
- try {
100
- const statusesUrl = `${domain}/${repo}/-/pulls/${options.prNumber}/statuses`;
101
- const statusData = await apiRequest(statusesUrl, headers);
102
- sn = extractSnFromStatuses(statusData);
103
- if (!sn) {
104
- return '未从 PR CI 状态中找到失败的构建,所有构建均为成功状态。';
105
- }
106
- } catch (e: any) {
107
- return `获取 PR CI 状态失败: ${e.message}`;
108
- }
109
- }
110
-
111
- if (!sn) {
112
- return '缺少构建号(sn):未传入 --sn、未设置 CNB_BUILD_ID、且未检测到 PR 上下文。';
113
- }
47
+ const resolved = await resolveBuildContext(options);
48
+ if (!resolved.ok) return resolved.message;
114
49
 
115
- const baseUrl = `${domain}/${repo}`;
50
+ const { sn, baseUrl, headers } = resolved.ctx;
116
51
  const lines: string[] = [];
117
52
  lines.push(`查询构建 sn=${sn} 的流水线状态...\n`);
118
53
 
@@ -0,0 +1,180 @@
1
+ /**
2
+ * 构建诊断 / 性能分析的共用 helper
3
+ *
4
+ * 两个对外自定义命令共享:
5
+ * - cnb pulls get-ci-logs → build-diagnostics.ts:diagnoseBuild
6
+ * - cnb pulls get-ci-timing → build-timing.ts:analyzeBuildTiming
7
+ */
8
+
9
+ import { resolveToken } from '../utils/resolve-token';
10
+
11
+ /** CNB 构建号格式:cnb-xxx-xxxxxxxxx(字母/数字组成的两段) */
12
+ export const SN_PATTERN = /^cnb-[a-z0-9]+-[a-z0-9]+$/i;
13
+
14
+ export function invalidSnMessage(sn: string): string {
15
+ return [
16
+ `参数 --sn 的值不是合法的构建号: "${sn}"`,
17
+ '',
18
+ '构建号(sn)格式应为 cnb-xxx-xxxxxxxxx,例如 cnb-9p9-1jnfh8rv5。',
19
+ '常见错误:把 `cnb pulls check-status` 返回的 context 字段(形如',
20
+ '"cnb/pull_request/common-lint")当成 sn 传入,这是 CI 状态的检查项名,',
21
+ '不是构建号。',
22
+ '',
23
+ '请去掉 --sn 参数重试,工具会自动从当前 PR 的 CI 状态中解析失败构建的 sn。',
24
+ ].join('\n');
25
+ }
26
+
27
+ export async function apiRequest(url: string, headers: Record<string, string>): Promise<any> {
28
+ const res = await fetch(url, { headers });
29
+ if (!res.ok) {
30
+ // 注意:错误消息中暴露 URL 是有意的,便于调试。
31
+ // 本项目所有 API 凭证通过 Authorization header 传递,不走 query string,
32
+ // 因此 URL 中不含敏感信息。未来如需在 URL 拼接 query 参数,请同步审视这里。
33
+ throw new Error(`请求失败: ${res.status} ${res.statusText} — ${url}`);
34
+ }
35
+ return res.json();
36
+ }
37
+
38
+ export async function apiRequestText(url: string, headers: Record<string, string>): Promise<string> {
39
+ const res = await fetch(url, { headers });
40
+ if (!res.ok) {
41
+ // 见 apiRequest 注释
42
+ throw new Error(`请求失败: ${res.status} ${res.statusText} — ${url}`);
43
+ }
44
+ return res.text();
45
+ }
46
+
47
+ /**
48
+ * 从 PR 的 commit statuses 中提取失败构建的 sn
49
+ * 仅选 state=error/failure 的检查项,从 target_url 中解析构建 sn
50
+ * target_url 通常形如 https://cnb.cool/{repo}/-/build/logs/{sn}
51
+ */
52
+ export function extractSnFromStatuses(statusData: any): string | null {
53
+ const statuses = statusData?.statuses || statusData;
54
+ if (!Array.isArray(statuses) || statuses.length === 0) return null;
55
+
56
+ const failed = statuses.find(
57
+ (s: any) => s.state === 'error' || s.state === 'failure',
58
+ );
59
+ if (!failed) return null;
60
+
61
+ return extractSnFromUrl(failed.target_url || '');
62
+ }
63
+
64
+ /**
65
+ * 从构建链接 URL 中提取 sn
66
+ * 支持两种格式:
67
+ * .../-/build/logs/{sn} ← check-status 的 target_url
68
+ * .../-/build/{sn} ← 兼容老路径
69
+ */
70
+ export function extractSnFromUrl(url: string): string | null {
71
+ if (!url) return null;
72
+ // 优先匹配 /-/build/logs/{sn}
73
+ const logsMatch = url.match(/\/-\/build\/logs\/([^/?#]+)/);
74
+ if (logsMatch && SN_PATTERN.test(logsMatch[1])) {
75
+ return logsMatch[1];
76
+ }
77
+ // 回退匹配 /-/build/{sn}
78
+ const buildMatch = url.match(/\/-\/build\/([^/?#]+)/);
79
+ if (buildMatch && SN_PATTERN.test(buildMatch[1])) {
80
+ return buildMatch[1];
81
+ }
82
+ return null;
83
+ }
84
+
85
+ export interface ResolveSnOptions {
86
+ repo: string;
87
+ sn?: string;
88
+ prNumber?: string;
89
+ /** 当未找到失败构建、但想回退到"最近一个构建"时使用(例如 timing 分析场景) */
90
+ fallbackToLatestStatus?: boolean;
91
+ }
92
+
93
+ export interface ResolvedContext {
94
+ /** 校验/解析出来的最终 sn;若为空字符串则表示应立即返回 errorMessage */
95
+ sn: string;
96
+ domain: string;
97
+ baseUrl: string;
98
+ headers: Record<string, string>;
99
+ }
100
+
101
+ /**
102
+ * 统一解析 sn + token + baseUrl
103
+ *
104
+ * 返回格式:
105
+ * - { ok: true, ctx } 成功解析
106
+ * - { ok: false, message } 解析失败,调用方应直接把 message 返回给用户
107
+ */
108
+ export async function resolveBuildContext(
109
+ options: ResolveSnOptions,
110
+ ): Promise<
111
+ | { ok: true; ctx: ResolvedContext }
112
+ | { ok: false; message: string }
113
+ > {
114
+ const domain = process.env.CNB_API_ENDPOINT || 'https://api.cnb.cool';
115
+ const { repo } = options;
116
+ let sn = options.sn;
117
+
118
+ // 策略 1: 显式传入的 sn — 需先校验格式,尽早失败
119
+ if (sn && !SN_PATTERN.test(sn)) {
120
+ return { ok: false, message: invalidSnMessage(sn) };
121
+ }
122
+
123
+ const token = await resolveToken();
124
+ const headers: Record<string, string> = {
125
+ Accept: 'application/vnd.cnb.api+json',
126
+ Authorization: `Bearer ${token}`,
127
+ };
128
+
129
+ // 策略 2: 有 PR 上下文时,从 PR commit statuses 中取失败(或最近)构建 sn。
130
+ if (!sn && options.prNumber) {
131
+ try {
132
+ // 对应 swagger: GET /{repo}/-/pulls/{number}/commit-statuses
133
+ const statusesUrl = `${domain}/${repo}/-/pulls/${options.prNumber}/commit-statuses`;
134
+ const statusData = await apiRequest(statusesUrl, headers);
135
+ sn = extractSnFromStatuses(statusData) ?? undefined;
136
+
137
+ if (!sn && options.fallbackToLatestStatus) {
138
+ // timing 场景:没失败也允许分析,回退到第一个能解析出合法 sn 的 status
139
+ const statuses = statusData?.statuses || statusData;
140
+ if (Array.isArray(statuses)) {
141
+ for (const s of statuses) {
142
+ const parsed = extractSnFromUrl(s?.target_url || '');
143
+ if (parsed) {
144
+ sn = parsed;
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ } catch (e: any) {
151
+ return { ok: false, message: `获取 PR CI 状态失败: ${e.message}` };
152
+ }
153
+ }
154
+
155
+ if (!sn) {
156
+ if (options.prNumber) {
157
+ return {
158
+ ok: false,
159
+ message: options.fallbackToLatestStatus
160
+ ? '未从 PR CI 状态中找到任何可分析的构建。'
161
+ : '未从 PR CI 状态中找到失败的构建,所有构建均为成功状态。',
162
+ };
163
+ }
164
+ return {
165
+ ok: false,
166
+ message:
167
+ '缺少构建号(sn):未传入 --sn 且未检测到 PR 上下文(CNB_PULL_REQUEST_IID)。',
168
+ };
169
+ }
170
+
171
+ return {
172
+ ok: true,
173
+ ctx: {
174
+ sn,
175
+ domain,
176
+ baseUrl: `${domain}/${repo}`,
177
+ headers,
178
+ },
179
+ };
180
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * 构建耗时分析模块
3
+ *
4
+ * 查询流水线所有 Stage 的耗时数据,分析瓶颈并给出优化提示。
5
+ *
6
+ * sn 获取策略(按优先级):
7
+ * 1. 用户通过 --sn 显式传入
8
+ * 2. 有 PR 上下文时,从 PR commit statuses 中提取失败构建的 sn;
9
+ * 若无失败则回退到最近一条可解析的构建
10
+ */
11
+
12
+ import { apiRequest, resolveBuildContext } from './build-helpers';
13
+
14
+ function formatDuration(ms: number | null | undefined): string {
15
+ if (ms == null) return 'N/A';
16
+ if (ms < 1000) return `${ms}ms`;
17
+ const seconds = ms / 1000;
18
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
19
+ const minutes = Math.floor(seconds / 60);
20
+ const remainSec = (seconds % 60).toFixed(0);
21
+ return `${minutes}m${remainSec}s`;
22
+ }
23
+
24
+ function progressBar(ratio: number, width = 20): string {
25
+ const filled = Math.round(ratio * width);
26
+ const empty = width - filled;
27
+ return '█'.repeat(filled) + '░'.repeat(empty);
28
+ }
29
+
30
+ interface StageTiming {
31
+ pipelineName: string;
32
+ pipelineId: string;
33
+ stageId: string;
34
+ stageName: string;
35
+ status: string;
36
+ duration: number;
37
+ needs: string[];
38
+ }
39
+
40
+ /**
41
+ * 分析构建耗时瓶颈
42
+ */
43
+ export async function analyzeBuildTiming(options: {
44
+ repo: string;
45
+ sn?: string;
46
+ prNumber?: string;
47
+ }): Promise<string> {
48
+ const resolved = await resolveBuildContext({
49
+ ...options,
50
+ // 耗时分析场景下,即使没有失败构建也允许分析成功的构建
51
+ fallbackToLatestStatus: true,
52
+ });
53
+ if (!resolved.ok) return resolved.message;
54
+
55
+ const { sn, baseUrl, headers } = resolved.ctx;
56
+ const lines: string[] = [];
57
+ lines.push(`查询构建 sn=${sn} 的流水线耗时数据...\n`);
58
+
59
+ // 获取构建状态
60
+ let buildStatus: any;
61
+ try {
62
+ buildStatus = await apiRequest(`${baseUrl}/-/build/status/${sn}`, headers);
63
+ } catch (e: any) {
64
+ return `获取构建状态失败: ${e.message}`;
65
+ }
66
+
67
+ if (
68
+ !buildStatus.pipelinesStatus ||
69
+ typeof buildStatus.pipelinesStatus !== 'object'
70
+ ) {
71
+ lines.push('未找到流水线状态信息');
72
+ lines.push('返回数据:');
73
+ lines.push(JSON.stringify(buildStatus, null, 2));
74
+ return lines.join('\n');
75
+ }
76
+
77
+ const pipelines = Object.entries(buildStatus.pipelinesStatus) as [string, any][];
78
+ if (pipelines.length === 0) {
79
+ return '该构建没有流水线';
80
+ }
81
+
82
+ const allStages: StageTiming[] = [];
83
+ let totalBuildDuration = 0;
84
+
85
+ lines.push('═══════════════════════════════════════════════════════');
86
+ lines.push(' CI 流水线耗时分析');
87
+ lines.push('═══════════════════════════════════════════════════════\n');
88
+
89
+ for (const [pipelineName, pipeline] of pipelines) {
90
+ const { id: pipelineId, status, stages } = pipeline;
91
+ if (!Array.isArray(stages) || stages.length === 0) continue;
92
+
93
+ let pipelineDuration = 0;
94
+ const stageTimings: StageTiming[] = [];
95
+
96
+ for (const stage of stages) {
97
+ const duration = stage.duration || 0;
98
+ pipelineDuration += duration;
99
+ stageTimings.push({
100
+ pipelineName,
101
+ pipelineId,
102
+ stageId: stage.id,
103
+ stageName: stage.name || stage.id,
104
+ status: stage.status,
105
+ duration,
106
+ needs: stage.needs || [],
107
+ });
108
+ }
109
+
110
+ totalBuildDuration += pipelineDuration;
111
+
112
+ lines.push(
113
+ `━━━ Pipeline: ${pipelineName} (status: ${status}, 总耗时: ${formatDuration(pipelineDuration)}) ━━━\n`,
114
+ );
115
+
116
+ // 按耗时从大到小排序
117
+ const sorted = [...stageTimings].sort((a, b) => b.duration - a.duration);
118
+
119
+ for (const s of sorted) {
120
+ const ratio = pipelineDuration > 0 ? s.duration / pipelineDuration : 0;
121
+ const pct = (ratio * 100).toFixed(1);
122
+ const bar = progressBar(ratio);
123
+ const statusIcon = s.status === 'success' ? '✅' : s.status === 'error' ? '❌' : '⏳';
124
+ lines.push(` ${statusIcon} ${s.stageName}`);
125
+ lines.push(` 耗时: ${formatDuration(s.duration)} (${pct}%) ${bar}`);
126
+ }
127
+
128
+ lines.push('');
129
+ allStages.push(...stageTimings);
130
+ }
131
+
132
+ // 全局 Stage 耗时排行
133
+ lines.push('═══════════════════════════════════════════════════════');
134
+ lines.push(' 全局 Stage 耗时排行 (Top 10)');
135
+ lines.push('═══════════════════════════════════════════════════════\n');
136
+
137
+ const globalSorted = [...allStages].sort((a, b) => b.duration - a.duration);
138
+ const top10 = globalSorted.slice(0, 10);
139
+
140
+ for (let i = 0; i < top10.length; i++) {
141
+ const s = top10[i];
142
+ const ratio = totalBuildDuration > 0 ? s.duration / totalBuildDuration : 0;
143
+ const pct = (ratio * 100).toFixed(1);
144
+ lines.push(` #${i + 1} ${s.pipelineName} → ${s.stageName}`);
145
+ lines.push(` 耗时: ${formatDuration(s.duration)} (占总耗时 ${pct}%)`);
146
+ }
147
+
148
+ lines.push(`\n 总耗时: ${formatDuration(totalBuildDuration)} (所有 Stage 耗时之和)`);
149
+
150
+ // 获取耗时最长 Stage 的详情日志用于分析
151
+ lines.push('\n═══════════════════════════════════════════════════════');
152
+ lines.push(' 耗时最长 Stage 的详情日志(用于分析优化点)');
153
+ lines.push('═══════════════════════════════════════════════════════\n');
154
+
155
+ const topStages = globalSorted.slice(0, 3);
156
+ for (const s of topStages) {
157
+ lines.push(`▸ ${s.pipelineName} → ${s.stageName} (${formatDuration(s.duration)})`);
158
+ try {
159
+ const detail = await apiRequest(
160
+ `${baseUrl}/-/build/logs/stage/${sn}/${s.pipelineId}/${s.stageId}`,
161
+ headers,
162
+ );
163
+
164
+ if (detail.duration != null) {
165
+ lines.push(` Stage 实际耗时: ${formatDuration(detail.duration)}`);
166
+ }
167
+
168
+ if (Array.isArray(detail.content) && detail.content.length > 0) {
169
+ lines.push(' ─── 日志(末尾 50 行)───');
170
+ const logLines = detail.content;
171
+ const tail = logLines.slice(Math.max(0, logLines.length - 50));
172
+ for (const line of tail) {
173
+ lines.push(` ${line}`);
174
+ }
175
+ if (logLines.length > 50) {
176
+ lines.push(` ... (省略了前 ${logLines.length - 50} 行,共 ${logLines.length} 行)`);
177
+ }
178
+ lines.push(' ─── 日志结束 ───');
179
+ } else {
180
+ lines.push(' (无日志内容)');
181
+ }
182
+ } catch (e: any) {
183
+ lines.push(` 查询 Stage 详情失败: ${e.message}`);
184
+ }
185
+ lines.push('');
186
+ }
187
+
188
+ // 统计摘要
189
+ lines.push('═══════════════════════════════════════════════════════');
190
+ lines.push(' 统计摘要');
191
+ lines.push('═══════════════════════════════════════════════════════\n');
192
+
193
+ lines.push(` Pipeline 数量: ${pipelines.length}`);
194
+ lines.push(` Stage 总数: ${allStages.length}`);
195
+ lines.push(` 成功 Stage: ${allStages.filter((s) => s.status === 'success').length}`);
196
+ lines.push(` 失败 Stage: ${allStages.filter((s) => s.status === 'error').length}`);
197
+ lines.push(` 总耗时: ${formatDuration(totalBuildDuration)}`);
198
+
199
+ if (globalSorted.length > 0) {
200
+ const longest = globalSorted[0];
201
+ lines.push(
202
+ ` 最慢 Stage: ${longest.pipelineName} → ${longest.stageName} (${formatDuration(longest.duration)})`,
203
+ );
204
+ }
205
+
206
+ // 检测潜在优化点
207
+ lines.push('\n═══════════════════════════════════════════════════════');
208
+ lines.push(' 潜在优化点提示');
209
+ lines.push('═══════════════════════════════════════════════════════\n');
210
+
211
+ const hints: string[] = [];
212
+
213
+ // 检测: 耗时超过 5 分钟的 Stage
214
+ const slowStages = allStages.filter((s) => s.duration > 300000);
215
+ if (slowStages.length > 0) {
216
+ hints.push(`⚠️ 有 ${slowStages.length} 个 Stage 耗时超过 5 分钟,建议重点优化`);
217
+ for (const s of slowStages) {
218
+ hints.push(` - ${s.pipelineName} → ${s.stageName}: ${formatDuration(s.duration)}`);
219
+ }
220
+ }
221
+
222
+ // 检测: 耗时占比超过 50% 的单个 Stage
223
+ const dominantStages = allStages.filter(
224
+ (s) => totalBuildDuration > 0 && s.duration / totalBuildDuration > 0.5,
225
+ );
226
+ if (dominantStages.length > 0) {
227
+ for (const s of dominantStages) {
228
+ const pct = ((s.duration / totalBuildDuration) * 100).toFixed(1);
229
+ hints.push(`⚠️ Stage "${s.stageName}" 占总耗时 ${pct}%,是主要瓶颈`);
230
+ }
231
+ }
232
+
233
+ // 检测: 单个 Pipeline 内 Stage 数很多但都串行
234
+ for (const [pipelineName, pipeline] of pipelines) {
235
+ if (Array.isArray(pipeline.stages) && pipeline.stages.length > 5) {
236
+ const noDeps = pipeline.stages.filter(
237
+ (s: any) => !s.needs || s.needs.length === 0,
238
+ );
239
+ if (noDeps.length > 3) {
240
+ hints.push(
241
+ `💡 Pipeline "${pipelineName}" 有 ${noDeps.length} 个无依赖的 Stage,可考虑并行执行`,
242
+ );
243
+ }
244
+ }
245
+ }
246
+
247
+ if (hints.length === 0) {
248
+ lines.push(' ✅ 未检测到明显的耗时异常');
249
+ } else {
250
+ for (const hint of hints) {
251
+ lines.push(` ${hint}`);
252
+ }
253
+ }
254
+
255
+ return lines.join('\n');
256
+ }
@@ -4,6 +4,7 @@ import { Command } from 'commander';
4
4
  import { showShort, resolveShortcut, type ResolvedShortcut } from '../shortcuts';
5
5
  import { handleUpload } from '../utils/upload';
6
6
  import { diagnoseBuild } from './build-diagnostics';
7
+ import { analyzeBuildTiming } from './build-timing';
7
8
  import { formatParams } from './format-params';
8
9
  import { formatOutput } from './format-output';
9
10
  // @ts-ignore
@@ -107,6 +108,16 @@ export async function executeAction(
107
108
  return;
108
109
  }
109
110
 
111
+ if (shortcut?.tool === '__get-ci-timing__') {
112
+ const result = await analyzeBuildTiming({
113
+ repo: process.env.CNB_REPO_SLUG || '',
114
+ sn: (opts.sn as string) || undefined,
115
+ prNumber: process.env.CNB_PULL_REQUEST_IID || undefined,
116
+ });
117
+ console.log(result);
118
+ return;
119
+ }
120
+
110
121
  const formattedParams = formatParams(params);
111
122
 
112
123
  const toolFunction = loadToolFunction(formattedParams.module, formattedParams.tool);
@@ -134,7 +134,10 @@ export function registerModuleCommands(program: Command): void {
134
134
  // 上传命令只需要 --file,不复用真实 tool 的 body 选项(name/size/content_type 由上传流程自动获取)
135
135
  if (shortcut.custom) {
136
136
  // 自定义命令注册自己的选项
137
- if (shortcut.realTool === '__get-ci-logs__') {
137
+ if (
138
+ shortcut.realTool === '__get-ci-logs__' ||
139
+ shortcut.realTool === '__get-ci-timing__'
140
+ ) {
138
141
  shortcutCmd.option('--sn <string>', '构建号(可选,默认自动从环境变量或 PR 状态获取)');
139
142
  }
140
143
  } else if (!shortcut.upload) {
@@ -136,7 +136,7 @@ const SUMMARY_EXTRACTORS: Record<string, SummaryExtractor> = {
136
136
  }),
137
137
 
138
138
  // PR CI 状态摘要:保留整体状态和各检查项的核心信息(sha 截断为前 8 位)
139
- // 过滤: sha 完整值, statuses 中每项的 target_url/created_at/updated_at
139
+ // 保留 target_url(用于从中提取构建 sn);过滤 created_at/updated_at
140
140
  'pulls/list-pull-commit-statuses': (item) => ({
141
141
  sha: typeof item.sha === 'string' ? item.sha.substring(0, 8) : item.sha,
142
142
  state: item.state,
@@ -144,6 +144,7 @@ const SUMMARY_EXTRACTORS: Record<string, SummaryExtractor> = {
144
144
  context: s.context,
145
145
  state: s.state,
146
146
  description: s.description,
147
+ target_url: s.target_url,
147
148
  })) || [],
148
149
  }),
149
150
 
@@ -97,6 +97,7 @@ export const PR_SHORTCUTS: ShortcutDefinition[] = [
97
97
  { shortName: 'upload-file', realTool: 'upload-files', description: '上传文件', repoOnly: true, upload: true, dataTip: "--file 文件路径" },
98
98
  { shortName: 'upload-image', realTool: 'upload-imgs', description: '上传图片', repoOnly: true, upload: true, dataTip: "--file 图片路径" },
99
99
  { shortName: 'get-ci-logs', realTool: '__get-ci-logs__', description: '获取 CI 失败日志', repoOnly: true, custom: true, dataTip: "--sn 构建号(可选)" },
100
+ { shortName: 'get-ci-timing', realTool: '__get-ci-timing__', description: '分析 CI 耗时瓶颈', repoOnly: true, custom: true, dataTip: "--sn 构建号(可选)" },
100
101
  ];
101
102
 
102
103
  // ============================================================
@@ -9,6 +9,13 @@ import { refreshAccessToken } from './refresh-token';
9
9
  * 刷新失败或无 refresh_token 时终止进程并提示用户重新登录。
10
10
  */
11
11
  export async function resolveToken(): Promise<string> {
12
+ // 环境变量优先 CodeBuddy
13
+ if (process.env.CNB_TOKEN_FOR_CODEBUDDY) {
14
+ if (!process.env.CNB_NPC_SLUG && process.env.CNB_NPC_NAME === 'CodeBuddy') {
15
+ return process.env.CNB_TOKEN_FOR_CODEBUDDY;
16
+ }
17
+ }
18
+
12
19
  // 环境变量优先,不做过期判断
13
20
  if (process.env.CNB_TOKEN) {
14
21
  return process.env.CNB_TOKEN;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "2.3.5",
3
+ "version": "2.4.1",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -10,49 +10,44 @@ description: CNB OpenAPI 交互能力,支持仓库、Issue、PR、流水线、
10
10
  ## 快捷命令
11
11
 
12
12
  issues:
13
- - `<$CNB_CLI_CMD$> issues get` — 获取 Issue 详情
14
- - `<$CNB_CLI_CMD$> issues list-comments` — 列出 Issue 评论
15
- - `<$CNB_CLI_CMD$> issues comment --body '内容'` — 发表 Issue 评论
16
- - `<$CNB_CLI_CMD$> issues close` — 关闭 Issue
17
- - `<$CNB_CLI_CMD$> issues open` — 打开 Issue
18
- - `<$CNB_CLI_CMD$> issues list-labels` — 列出 Issue 标签
19
- - `<$CNB_CLI_CMD$> issues add-labels --labels bug --labels feature` — 添加 Issue 标签
20
- - `<$CNB_CLI_CMD$> issues list-assignees` — 查看 Issue 处理人
21
- - `<$CNB_CLI_CMD$> issues add-assignees --assignees username` — 添加 Issue 处理人
22
- - `<$CNB_CLI_CMD$> issues upload-file --file 文件路径` — 上传文件到 Issue
23
- - `<$CNB_CLI_CMD$> issues upload-image --file 图片路径` — 上传图片到 Issue
13
+ - `<$CNB_CLI_CMD$> issues get` — 获取当前 Issue 详情
14
+ - `<$CNB_CLI_CMD$> issues list-comments` — 列出当前 Issue 评论
15
+ - `<$CNB_CLI_CMD$> issues comment --body '内容'` — 发表评论到当前 Issue
16
+ - `<$CNB_CLI_CMD$> issues close` — 关闭当前 Issue
17
+ - `<$CNB_CLI_CMD$> issues open` — 打开当前 Issue
18
+ - `<$CNB_CLI_CMD$> issues list-labels` — 列出当前 Issue 标签
19
+ - `<$CNB_CLI_CMD$> issues add-labels --labels bug --labels feature` — 添加标签到当前 Issue
20
+ - `<$CNB_CLI_CMD$> issues list-assignees` — 查看当前 Issue 处理人
21
+ - `<$CNB_CLI_CMD$> issues add-assignees --assignees username` — 添加处理人到当前 Issue
22
+ - `<$CNB_CLI_CMD$> issues upload-file --file 文件路径` — 上传文件到当前 Issue
23
+ - `<$CNB_CLI_CMD$> issues upload-image --file 图片路径` — 上传图片到当前 Issue
24
24
 
25
25
  pulls:
26
- - `<$CNB_CLI_CMD$> pulls get` — 获取 PR 详情
27
- - `<$CNB_CLI_CMD$> pulls list-files` — 列出 PR 变更文件
28
- - `<$CNB_CLI_CMD$> pulls list-commits` — 列出 PR 提交记录
29
- - `<$CNB_CLI_CMD$> pulls list-comments` — 列出 PR 评论
26
+ - `<$CNB_CLI_CMD$> pulls get` — 获取当前 PR 详情
27
+ - `<$CNB_CLI_CMD$> pulls list-files` — 列出当前 PR 变更文件
28
+ - `<$CNB_CLI_CMD$> pulls list-commits` — 列出当前 PR 提交记录
29
+ - `<$CNB_CLI_CMD$> pulls list-comments` — 列出当前 PR 评论
30
30
  - `<$CNB_CLI_CMD$> pulls comment --body '内容'` — 发表 PR 评论
31
- - `<$CNB_CLI_CMD$> pulls list-labels` — 列出 PR 标签
32
- - `<$CNB_CLI_CMD$> pulls add-labels --labels ready --labels approved` — 添加 PR 标签
33
- - `<$CNB_CLI_CMD$> pulls check-status` — 查看 CI 状态
34
- - `<$CNB_CLI_CMD$> pulls list-reviews` — 查看评审列表
35
- - `<$CNB_CLI_CMD$> pulls list-assignees` — 查看 PR 处理人
36
- - `<$CNB_CLI_CMD$> pulls upload-file --file 文件路径` 上传文件到 PR
37
- - `<$CNB_CLI_CMD$> pulls upload-image --file 图片路径`上传图片到 PR
38
- - `<$CNB_CLI_CMD$> pulls get-ci-logs`获取 PR 失败构建的 CI 日志和耗时分析(可选 --sn 指定构建号)
31
+ - `<$CNB_CLI_CMD$> pulls list-labels` — 列出当前 PR 标签
32
+ - `<$CNB_CLI_CMD$> pulls add-labels --labels ready --labels approved` — 添加标签到当前 PR
33
+ - `<$CNB_CLI_CMD$> pulls check-status` — 获取当前 PR 的 CI 状态
34
+ - `<$CNB_CLI_CMD$> pulls get-ci-logs` — 获取当前 PR 的 CI 构建日志
35
+ - `<$CNB_CLI_CMD$> pulls list-reviews` — 查看当前当前的评审列表
36
+ - `<$CNB_CLI_CMD$> pulls list-assignees`查看当前 PR 处理人
37
+ - `<$CNB_CLI_CMD$> pulls upload-file --file 文件路径`上传文件到当前 PR
38
+ - `<$CNB_CLI_CMD$> pulls upload-image --file 图片路径` 上传图片到当前 PR
39
39
 
40
40
  注意事项:
41
- - **路径参数自动注入**:仓库 slug、Issue/PR 编号优先从环境变量自动获取,无需额外传入任何其他参数
42
- - **默认只输出摘要**:会精简响应输出结果,只返回核心字段。加 `--verbose` 输出完整数据。
41
+ - **路径参数自动识别**:快捷命令中的 Issue/PR 编号会自动从环境变量识别,无需额外传递。
42
+ - **默认只输出摘要**:默认会精简响应输出结果,只返回核心字段。添加 `--verbose` 输出完整数据。
43
43
  - **多行文本传参使用单引号**:传递多行文本参数时,使用单引号可防止命令注入攻击,并减少不必要的转义。
44
- - **快捷命令范围** 快捷命令只能操作当前仓库的当前IssuePR,跨仓库或跨编号操作请使用其他 API 命令。
44
+ - **快捷命令范围** 快捷命令只能操作当前仓库的当前 Issue/PR,跨仓库或跨编号操作请使用其他 API 命令。
45
45
 
46
- ## 其他 API
46
+ ## 更多 API
47
47
 
48
- 快捷命令不满足时才使用
48
+ 优先使用快捷命令,不满足时才使用以下这些命令
49
49
 
50
50
  1. `<$CNB_CLI_CMD$> --help` 查看所有模块
51
51
  2. `<$CNB_CLI_CMD$> <module> --help` 查看模块下的工具列表
52
52
  3. `<$CNB_CLI_CMD$> <module> <tool> --help` 查看工具参数
53
- 4. 按参数说明执行
54
-
55
- ## 规则
56
-
57
- - 优先使用快捷命令,不满足时再 `--help` 逐步查找
58
- - 使用非快捷命令时,必须先 `--help` 获取帮助,禁止猜测
53
+ 4. 按帮助文档操作,禁止猜测