ai-git-tools 2.0.8 → 2.0.9
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/package.json +1 -1
- package/src/commands/pr.js +5 -5
- package/src/pr-modules/ai/code-analyzer.js +387 -0
- package/src/pr-modules/ai/label-analyzer.js +113 -0
- package/src/pr-modules/core/config-loader.js +132 -0
- package/src/pr-modules/core/git-operations.js +211 -0
- package/src/pr-modules/core/github-api.js +376 -0
- package/src/pr-modules/core/workflow.js +371 -0
- package/src/pr-modules/reviewers/reviewer-selector.js +232 -0
- package/src/{utils → pr-modules/ui}/interactive-select.js +4 -27
- package/src/pr-modules/ui/logger.js +40 -0
- package/src/pr-modules/utils/constants.js +115 -0
- package/src/pr-modules/utils/helpers.js +75 -0
- package/src/core/github-api.js +0 -161
package/package.json
CHANGED
package/src/commands/pr.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PR 命令 - 完整複製自 scripts/ai-auto-pr.mjs
|
|
3
3
|
*
|
|
4
|
-
* 使用 scripts/ai-pr-modules
|
|
4
|
+
* 使用 pr-modules 的完整邏輯(從 scripts/ai-pr-modules 複製)
|
|
5
5
|
* 確保功能與 scripts 版本完全相同
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { PRWorkflow } from '
|
|
9
|
-
import { loadConfig } from '
|
|
10
|
-
import { handleError } from '
|
|
11
|
-
import { Logger } from '
|
|
8
|
+
import { PRWorkflow } from '../pr-modules/core/workflow.js';
|
|
9
|
+
import { loadConfig } from '../pr-modules/core/config-loader.js';
|
|
10
|
+
import { handleError } from '../pr-modules/utils/helpers.js';
|
|
11
|
+
import { Logger } from '../pr-modules/ui/logger.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* PR 命令主函數(完全照抄 scripts/ai-auto-pr.mjs)
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { CopilotClient } from '@github/copilot-sdk';
|
|
2
|
+
import { CONSTANTS, PROJECT_SKILLS_CONTEXT } from '../utils/constants.js';
|
|
3
|
+
import { getSkillsSummaryForPrompt, log } from '../utils/helpers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AI 分析器 - 負責程式碼分析和 PR 內容生成
|
|
7
|
+
*/
|
|
8
|
+
export class AIAnalyzer {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.model = config.model || 'gpt-4.1';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 建立 AI 客戶端
|
|
15
|
+
*/
|
|
16
|
+
async createClient() {
|
|
17
|
+
const client = new CopilotClient();
|
|
18
|
+
const session = await client.createSession({ model: this.model });
|
|
19
|
+
return { client, session };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 生成 PR 內容
|
|
24
|
+
*/
|
|
25
|
+
async generatePRContent(commits, diff) {
|
|
26
|
+
const skillsSummary = getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT);
|
|
27
|
+
const prompt = this.buildPRPrompt(commits, diff, skillsSummary);
|
|
28
|
+
|
|
29
|
+
const { client, session } = await this.createClient();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const response = await session.sendAndWait({ prompt });
|
|
33
|
+
const prContent = response?.data.content?.trim() || '';
|
|
34
|
+
|
|
35
|
+
await client.stop();
|
|
36
|
+
|
|
37
|
+
if (!prContent) {
|
|
38
|
+
throw new Error('AI 未能生成 PR 內容');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.parsePRContent(prContent);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await client.stop();
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 分析程式碼影響範圍
|
|
50
|
+
*/
|
|
51
|
+
async analyzeImpact(changedFiles, diff, commits) {
|
|
52
|
+
const skillsSummary = getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT);
|
|
53
|
+
const prompt = this.buildAnalysisPrompt(changedFiles, diff, commits, skillsSummary);
|
|
54
|
+
|
|
55
|
+
const { client, session } = await this.createClient();
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
log.info(' 正在使用 AI 深度分析程式碼變更...');
|
|
59
|
+
const response = await session.sendAndWait({ prompt });
|
|
60
|
+
const content = response?.data.content?.trim() || '';
|
|
61
|
+
|
|
62
|
+
await client.stop();
|
|
63
|
+
|
|
64
|
+
// 解析 JSON
|
|
65
|
+
try {
|
|
66
|
+
const jsonMatch =
|
|
67
|
+
content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/```\s*([\s\S]*?)\s*```/);
|
|
68
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : content;
|
|
69
|
+
const analysisResult = JSON.parse(jsonStr);
|
|
70
|
+
|
|
71
|
+
const blastRadius = {
|
|
72
|
+
modules: analysisResult.blastRadius?.modules || [],
|
|
73
|
+
impacts: analysisResult.blastRadius?.impacts || [],
|
|
74
|
+
riskLevel: analysisResult.blastRadius?.riskLevel || '低',
|
|
75
|
+
riskReasons: analysisResult.blastRadius?.riskReasons || [],
|
|
76
|
+
externalBehaviors: analysisResult.blastRadius?.externalBehaviors || [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const warnings = analysisResult.warnings || [];
|
|
80
|
+
|
|
81
|
+
log.success(' AI 分析完成\n');
|
|
82
|
+
return { blastRadius, warnings };
|
|
83
|
+
} catch (parseError) {
|
|
84
|
+
log.warning(' AI 響應解析失敗,使用基礎分析...');
|
|
85
|
+
return this.getFallbackAnalysis(changedFiles);
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
await client.stop();
|
|
89
|
+
log.warning(` AI 分析失敗 (${error.message}),使用基礎分析...\n`);
|
|
90
|
+
return this.getFallbackAnalysis(changedFiles);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 建立 PR 生成 Prompt
|
|
96
|
+
*/
|
|
97
|
+
buildPRPrompt(commits, diff, skillsSummary) {
|
|
98
|
+
return `你是一個專業的前端工程師,熟悉 Next.js、React 效能優化和團隊開發規範。
|
|
99
|
+
請根據以下 commit 訊息和程式碼變更,直接輸出一個清晰的 Pull Request 標題和描述。
|
|
100
|
+
|
|
101
|
+
${skillsSummary}
|
|
102
|
+
|
|
103
|
+
**輸出格式**(不要加任何引導語,直接輸出以下內容):
|
|
104
|
+
|
|
105
|
+
# [type]: [PR 標題]
|
|
106
|
+
|
|
107
|
+
> type 必須是以下之一:feat / fix / refactor / style / docs / test / chore / perf
|
|
108
|
+
|
|
109
|
+
## 📝 變更摘要
|
|
110
|
+
[簡述這個 PR 的主要目的和影響範圍,2-3 句話]
|
|
111
|
+
|
|
112
|
+
## 🎯 主要變更
|
|
113
|
+
- [變更項目 1]
|
|
114
|
+
- [變更項目 2]
|
|
115
|
+
- [變更項目 3]
|
|
116
|
+
|
|
117
|
+
## 🔀 變更類型
|
|
118
|
+
- [ ] ✨ 新功能 (feat)
|
|
119
|
+
- [ ] 🐛 Bug 修復 (fix)
|
|
120
|
+
- [ ] ♻️ 重構 (refactor)
|
|
121
|
+
- [ ] 💄 樣式調整 (style)
|
|
122
|
+
- [ ] 📝 文件更新 (docs)
|
|
123
|
+
- [ ] ⚡ 效能改進 (perf)
|
|
124
|
+
- [ ] 🔧 其他 (chore)
|
|
125
|
+
|
|
126
|
+
> 請根據實際變更勾選對應的類型(可複選)
|
|
127
|
+
|
|
128
|
+
## 🧪 測試方法
|
|
129
|
+
1. [具體的測試步驟 1]
|
|
130
|
+
2. [具體的測試步驟 2]
|
|
131
|
+
3. [具體的測試步驟 3]
|
|
132
|
+
|
|
133
|
+
## 💥 Breaking Changes
|
|
134
|
+
[如果有破壞性變更請詳細說明,沒有則填寫「無」]
|
|
135
|
+
|
|
136
|
+
## 📌 注意事項
|
|
137
|
+
[需要特別注意的事項]
|
|
138
|
+
|
|
139
|
+
## 📸 截圖
|
|
140
|
+
[如果是 UI 變更,提醒需要截圖]
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## ✅ 已套用規則總結
|
|
145
|
+
|
|
146
|
+
> 請根據上方提供的專案規範(React Best Practices 和 Frontend Guidelines),分析本次程式碼變更
|
|
147
|
+
|
|
148
|
+
### React Best Practices 規則
|
|
149
|
+
|
|
150
|
+
**✅ 已正確套用的規則**:
|
|
151
|
+
[列出本次變更中有正確使用的規則]
|
|
152
|
+
|
|
153
|
+
**❌ 需要改善的項目**:
|
|
154
|
+
[列出本次變更中違反的規則]
|
|
155
|
+
|
|
156
|
+
### Frontend Guidelines
|
|
157
|
+
|
|
158
|
+
[根據程式碼實際內容檢查相關項目]
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
**規則**:
|
|
163
|
+
1. PR 標題格式:type: 簡短描述(不超過 50 字)
|
|
164
|
+
2. type 必須符合 Conventional Commits
|
|
165
|
+
3. 變更摘要用 2-3 句話概括整體影響
|
|
166
|
+
4. 全部使用繁體中文(台灣正體)
|
|
167
|
+
5. 不要在開頭加引導語句
|
|
168
|
+
6. 直接開始輸出 # [type]: [標題]
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
**Commit 訊息**:
|
|
173
|
+
${commits}
|
|
174
|
+
|
|
175
|
+
**程式碼變更**:
|
|
176
|
+
${diff}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 建立影響分析 Prompt
|
|
181
|
+
*/
|
|
182
|
+
buildAnalysisPrompt(changedFiles, diff, commits, skillsSummary) {
|
|
183
|
+
return `你是一個資深的程式碼審查專家,精通 React/Next.js 效能優化與前端架構設計。
|
|
184
|
+
請分析以下程式碼變更,提供專業的影響範圍分析與規範合規檢查。
|
|
185
|
+
|
|
186
|
+
${skillsSummary}
|
|
187
|
+
|
|
188
|
+
**變更檔案列表**:
|
|
189
|
+
${changedFiles.slice(0, CONSTANTS.MAX_FILES_IN_PROMPT).join('\n')}
|
|
190
|
+
${
|
|
191
|
+
changedFiles.length > CONSTANTS.MAX_FILES_IN_PROMPT
|
|
192
|
+
? `... 還有 ${changedFiles.length - CONSTANTS.MAX_FILES_IN_PROMPT} 個檔案`
|
|
193
|
+
: ''
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
**Commit 訊息**:
|
|
197
|
+
${commits.split('\n').slice(0, CONSTANTS.MAX_COMMITS_IN_PROMPT).join('\n')}
|
|
198
|
+
|
|
199
|
+
**程式碼變更內容**:
|
|
200
|
+
\`\`\`diff
|
|
201
|
+
${diff.substring(0, CONSTANTS.MAX_DIFF_LENGTH)}
|
|
202
|
+
${diff.length > CONSTANTS.MAX_DIFF_LENGTH ? '\n... (內容過長已截斷)' : ''}
|
|
203
|
+
\`\`\`
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
請以 JSON 格式輸出分析結果(不要加任何其他文字,只輸出 JSON):
|
|
208
|
+
|
|
209
|
+
\`\`\`json
|
|
210
|
+
{
|
|
211
|
+
"blastRadius": {
|
|
212
|
+
"modules": ["影響的模組1", "影響的模組2"],
|
|
213
|
+
"impacts": ["影響層面1", "影響層面2"],
|
|
214
|
+
"riskLevel": "低|中|高",
|
|
215
|
+
"riskReasons": ["風險原因1", "風險原因2"],
|
|
216
|
+
"externalBehaviors": ["對外行為變更說明"]
|
|
217
|
+
},
|
|
218
|
+
"warnings": [
|
|
219
|
+
{
|
|
220
|
+
"level": "⚠️|ℹ️",
|
|
221
|
+
"message": "問題描述",
|
|
222
|
+
"suggestion": "改善建議"
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
\`\`\`
|
|
227
|
+
|
|
228
|
+
**分析重點**:
|
|
229
|
+
|
|
230
|
+
1. **影響範圍 (blastRadius)**:
|
|
231
|
+
- 真實分析程式碼變更內容,不要只看檔案路徑
|
|
232
|
+
- 識別實際影響的模組
|
|
233
|
+
- 分析影響層面(API、資料庫、UI、商業邏輯、效能、安全性等)
|
|
234
|
+
- 評估風險等級,並說明原因
|
|
235
|
+
- 識別對外行為變更
|
|
236
|
+
|
|
237
|
+
2. **規範警告 (warnings)**:
|
|
238
|
+
- 檢查是否有安全風險
|
|
239
|
+
- 檢查是否缺少錯誤處理
|
|
240
|
+
- 檢查是否有效能問題
|
|
241
|
+
- 檢查是否違反最佳實踐
|
|
242
|
+
|
|
243
|
+
**回應格式**:只輸出有效的 JSON,不要有任何前綴或後綴文字。`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 解析 AI 生成的 PR 內容
|
|
248
|
+
*/
|
|
249
|
+
parsePRContent(prContent) {
|
|
250
|
+
let title = '';
|
|
251
|
+
let body = '';
|
|
252
|
+
|
|
253
|
+
const lines = prContent.split('\n');
|
|
254
|
+
let titleLineIndex = -1;
|
|
255
|
+
|
|
256
|
+
// 尋找第一個 # 開頭的標題行
|
|
257
|
+
for (let i = 0; i < lines.length; i++) {
|
|
258
|
+
const line = lines[i].trim();
|
|
259
|
+
|
|
260
|
+
// 跳過引導語句
|
|
261
|
+
if (
|
|
262
|
+
line.match(/^(已根據|請|這是|以下是|根據|PR\s*描述|該描述)/i) ||
|
|
263
|
+
line.match(/適合.*複製/i)
|
|
264
|
+
) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 找到第一個 # 標題
|
|
269
|
+
if (line.match(/^#\s+.+/)) {
|
|
270
|
+
titleLineIndex = i;
|
|
271
|
+
title = line.replace(/^#\s+/, '').trim();
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Body 從標題後一行開始
|
|
277
|
+
if (titleLineIndex >= 0) {
|
|
278
|
+
let bodyStartIndex = titleLineIndex + 1;
|
|
279
|
+
while (bodyStartIndex < lines.length && !lines[bodyStartIndex].trim()) {
|
|
280
|
+
bodyStartIndex++;
|
|
281
|
+
}
|
|
282
|
+
body = lines.slice(bodyStartIndex).join('\n').trim();
|
|
283
|
+
} else {
|
|
284
|
+
body = prContent.trim();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 清理標題和 body
|
|
288
|
+
title = title.replace(/[((]\d+字[))]/g, '').trim();
|
|
289
|
+
body = body.replace(/\n*該描述使用.*$/i, '').trim();
|
|
290
|
+
|
|
291
|
+
return { title, body };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 降級方案:基礎分析
|
|
296
|
+
*/
|
|
297
|
+
getFallbackAnalysis(changedFiles) {
|
|
298
|
+
const modules = [];
|
|
299
|
+
const impacts = [];
|
|
300
|
+
let riskLevel = '低';
|
|
301
|
+
const riskReasons = [];
|
|
302
|
+
|
|
303
|
+
changedFiles.forEach((file) => {
|
|
304
|
+
const lower = file.toLowerCase();
|
|
305
|
+
if (lower.includes('/api/')) {
|
|
306
|
+
if (!impacts.includes('API 層')) impacts.push('API 層');
|
|
307
|
+
if (!modules.includes('API 服務')) modules.push('API 服務');
|
|
308
|
+
}
|
|
309
|
+
if (lower.includes('/components/') || lower.includes('/pages/')) {
|
|
310
|
+
if (!impacts.includes('使用者介面')) impacts.push('使用者介面');
|
|
311
|
+
if (!modules.includes('前端元件')) modules.push('前端元件');
|
|
312
|
+
}
|
|
313
|
+
if (lower.includes('db') || lower.includes('migration') || lower.includes('schema')) {
|
|
314
|
+
if (!impacts.includes('資料庫')) impacts.push('資料庫');
|
|
315
|
+
riskLevel = '高';
|
|
316
|
+
riskReasons.push('涉及資料庫結構變更');
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const warnings = [];
|
|
321
|
+
const hasTestFiles = changedFiles.some((f) => f.includes('test') || f.includes('spec'));
|
|
322
|
+
if (!hasTestFiles && changedFiles.length > 3) {
|
|
323
|
+
warnings.push({
|
|
324
|
+
level: '⚠️',
|
|
325
|
+
message: '未包含測試檔案',
|
|
326
|
+
suggestion: '建議新增測試確保程式碼品質',
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
blastRadius: { modules, impacts, riskLevel, riskReasons, externalBehaviors: [] },
|
|
332
|
+
warnings,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 將分析結果附加到 PR body
|
|
338
|
+
*/
|
|
339
|
+
appendAnalysisToBody(body, blastRadius, warnings) {
|
|
340
|
+
let enhancedBody = body;
|
|
341
|
+
|
|
342
|
+
// 添加影響範圍
|
|
343
|
+
enhancedBody += '\n\n---\n\n## 💥 影響範圍分析\n\n';
|
|
344
|
+
|
|
345
|
+
if (blastRadius.modules.length > 0) {
|
|
346
|
+
enhancedBody += `**影響模組**:${blastRadius.modules.join('、')}\n\n`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (blastRadius.impacts.length > 0) {
|
|
350
|
+
enhancedBody += `**影響層面**:${blastRadius.impacts.join('、')}\n\n`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const riskEmojiMap = { 高: '🔴', 中: '🟡', 低: '🟢' };
|
|
354
|
+
const riskEmoji = riskEmojiMap[blastRadius.riskLevel] || '🟢';
|
|
355
|
+
enhancedBody += `**風險等級**:${riskEmoji} ${blastRadius.riskLevel}\n\n`;
|
|
356
|
+
|
|
357
|
+
if (blastRadius.riskReasons && blastRadius.riskReasons.length > 0) {
|
|
358
|
+
enhancedBody += `**風險因素**:\n`;
|
|
359
|
+
blastRadius.riskReasons.forEach((reason) => {
|
|
360
|
+
enhancedBody += `- ${reason}\n`;
|
|
361
|
+
});
|
|
362
|
+
enhancedBody += '\n';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (blastRadius.externalBehaviors && blastRadius.externalBehaviors.length > 0) {
|
|
366
|
+
enhancedBody += `**對外行為變更**:\n`;
|
|
367
|
+
blastRadius.externalBehaviors.forEach((behavior) => {
|
|
368
|
+
enhancedBody += `- ${behavior}\n`;
|
|
369
|
+
});
|
|
370
|
+
enhancedBody += '\n';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 添加規範警告
|
|
374
|
+
if (warnings.length > 0) {
|
|
375
|
+
enhancedBody += '\n## ⚠️ 注意事項\n\n';
|
|
376
|
+
warnings.forEach((warning) => {
|
|
377
|
+
enhancedBody += `${warning.level} **${warning.message}**\n`;
|
|
378
|
+
if (warning.suggestion) {
|
|
379
|
+
enhancedBody += ` - 💡 ${warning.suggestion}\n`;
|
|
380
|
+
}
|
|
381
|
+
enhancedBody += '\n';
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return enhancedBody;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { log } from '../utils/helpers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Label 分析器 - 自動為 PR 添加合適的標籤
|
|
6
|
+
*/
|
|
7
|
+
export class LabelAnalyzer {
|
|
8
|
+
/**
|
|
9
|
+
* 分析應該添加的 Labels
|
|
10
|
+
*/
|
|
11
|
+
analyzeLabels(prData) {
|
|
12
|
+
const labels = new Set();
|
|
13
|
+
|
|
14
|
+
// 根據 commit type
|
|
15
|
+
const typeLabels = {
|
|
16
|
+
feat: 'feature',
|
|
17
|
+
fix: 'bug',
|
|
18
|
+
refactor: 'refactor',
|
|
19
|
+
perf: 'performance',
|
|
20
|
+
docs: 'documentation',
|
|
21
|
+
test: 'testing',
|
|
22
|
+
style: 'style',
|
|
23
|
+
chore: 'chore',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// 從標題中提取 type
|
|
27
|
+
const titleMatch = prData.title.match(/^(\w+):/);
|
|
28
|
+
if (titleMatch) {
|
|
29
|
+
const type = titleMatch[1];
|
|
30
|
+
if (typeLabels[type]) {
|
|
31
|
+
labels.add(typeLabels[type]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 根據影響範圍
|
|
36
|
+
if (prData.blastRadius) {
|
|
37
|
+
if (prData.blastRadius.impacts.includes('API 層')) {
|
|
38
|
+
labels.add('api-change');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (prData.blastRadius.impacts.includes('資料庫')) {
|
|
42
|
+
labels.add('database');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (prData.blastRadius.impacts.includes('使用者介面')) {
|
|
46
|
+
labels.add('ui');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 根據風險等級
|
|
50
|
+
if (prData.blastRadius.riskLevel === '高') {
|
|
51
|
+
labels.add('high-risk');
|
|
52
|
+
labels.add('needs-careful-review');
|
|
53
|
+
} else if (prData.blastRadius.riskLevel === '中') {
|
|
54
|
+
labels.add('medium-risk');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 根據變更規模
|
|
59
|
+
if (prData.stats) {
|
|
60
|
+
if (prData.stats.filesChanged > 20) {
|
|
61
|
+
labels.add('large-change');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (prData.stats.filesChanged > 50) {
|
|
65
|
+
labels.add('needs-review');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 根據警告
|
|
70
|
+
if (prData.warnings && prData.warnings.length > 0) {
|
|
71
|
+
labels.add('has-warnings');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Array.from(labels);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 應用 Labels 到 PR
|
|
79
|
+
*/
|
|
80
|
+
async applyLabels(prNumber, labels) {
|
|
81
|
+
if (!labels || labels.length === 0) {
|
|
82
|
+
log.info('無需添加 Labels');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
log.info(`正在添加 Labels: ${labels.join(', ')}`);
|
|
87
|
+
|
|
88
|
+
const requestBody = JSON.stringify({ labels });
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
execSync(
|
|
92
|
+
`printf '%s' '${requestBody.replace(
|
|
93
|
+
/'/g,
|
|
94
|
+
"'\\''"
|
|
95
|
+
)}' | gh api repos/:owner/:repo/issues/${prNumber}/labels --input - -X POST`,
|
|
96
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
log.success(`成功添加 ${labels.length} 個 Labels`);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
log.warning('無法自動添加 Labels,請手動操作');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 分析並應用 Labels
|
|
107
|
+
*/
|
|
108
|
+
async analyzeAndApply(prNumber, prData) {
|
|
109
|
+
const labels = this.analyzeLabels(prData);
|
|
110
|
+
await this.applyLabels(prNumber, labels);
|
|
111
|
+
return labels;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 解析命令列參數
|
|
6
|
+
*/
|
|
7
|
+
export function parseCliArgs() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const config = {
|
|
10
|
+
baseBranch: null,
|
|
11
|
+
headBranch: null,
|
|
12
|
+
model: null,
|
|
13
|
+
draft: false,
|
|
14
|
+
preview: false,
|
|
15
|
+
noConfirm: false,
|
|
16
|
+
autoReviewers: false,
|
|
17
|
+
autoLabels: null,
|
|
18
|
+
orgName: null,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
switch (args[i]) {
|
|
23
|
+
case '--base':
|
|
24
|
+
config.baseBranch = args[++i];
|
|
25
|
+
break;
|
|
26
|
+
case '--head':
|
|
27
|
+
config.headBranch = args[++i];
|
|
28
|
+
break;
|
|
29
|
+
case '--model':
|
|
30
|
+
config.model = args[++i];
|
|
31
|
+
break;
|
|
32
|
+
case '--org':
|
|
33
|
+
config.orgName = args[++i];
|
|
34
|
+
break;
|
|
35
|
+
case '--draft':
|
|
36
|
+
config.draft = true;
|
|
37
|
+
break;
|
|
38
|
+
case '--preview':
|
|
39
|
+
config.preview = true;
|
|
40
|
+
break;
|
|
41
|
+
case '--no-confirm':
|
|
42
|
+
config.noConfirm = true;
|
|
43
|
+
break;
|
|
44
|
+
case '--auto-reviewers':
|
|
45
|
+
config.autoReviewers = true;
|
|
46
|
+
break;
|
|
47
|
+
case '--auto-labels':
|
|
48
|
+
config.autoLabels = true;
|
|
49
|
+
break;
|
|
50
|
+
case '--no-labels':
|
|
51
|
+
config.autoLabels = false;
|
|
52
|
+
break;
|
|
53
|
+
case '--help':
|
|
54
|
+
showHelp();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
// 忽略未知的參數
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 載入配置
|
|
68
|
+
*/
|
|
69
|
+
export async function loadConfig() {
|
|
70
|
+
// 1. 載入預設配置
|
|
71
|
+
const configPath = resolve(process.cwd(), '.ai-pr-config.js');
|
|
72
|
+
let config = null;
|
|
73
|
+
|
|
74
|
+
if (existsSync(configPath)) {
|
|
75
|
+
const userConfig = await import(configPath);
|
|
76
|
+
config = userConfig.default;
|
|
77
|
+
} else {
|
|
78
|
+
// 使用內建預設值
|
|
79
|
+
config = {
|
|
80
|
+
ai: { model: 'gpt-4.1', maxDiffLength: 8000, maxRetries: 3 },
|
|
81
|
+
github: { orgName: 'kingsinfo-project', defaultBase: 'release', autoLabels: true },
|
|
82
|
+
reviewers: { autoSelect: false, maxSuggested: 5, gitHistoryDepth: 20, excludeAuthors: [] },
|
|
83
|
+
output: { verbose: false, saveHistory: false },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. 合併命令列參數
|
|
88
|
+
const cliConfig = parseCliArgs();
|
|
89
|
+
|
|
90
|
+
// 合併配置(CLI 參數優先)
|
|
91
|
+
if (cliConfig.model) config.ai.model = cliConfig.model;
|
|
92
|
+
if (cliConfig.orgName) config.github.orgName = cliConfig.orgName;
|
|
93
|
+
if (cliConfig.autoReviewers) config.reviewers.autoSelect = cliConfig.autoReviewers;
|
|
94
|
+
if (cliConfig.autoLabels) config.github.autoLabels = cliConfig.autoLabels;
|
|
95
|
+
|
|
96
|
+
// 其他 CLI 參數直接加入 config
|
|
97
|
+
config.baseBranch = cliConfig.baseBranch;
|
|
98
|
+
config.headBranch = cliConfig.headBranch;
|
|
99
|
+
config.draft = cliConfig.draft;
|
|
100
|
+
config.preview = cliConfig.preview;
|
|
101
|
+
config.noConfirm = cliConfig.noConfirm;
|
|
102
|
+
|
|
103
|
+
return config;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 顯示幫助訊息
|
|
108
|
+
*/
|
|
109
|
+
function showHelp() {
|
|
110
|
+
console.log(`
|
|
111
|
+
使用方式:
|
|
112
|
+
node scripts/ai-auto-pr.mjs [選項]
|
|
113
|
+
|
|
114
|
+
選項:
|
|
115
|
+
--base <branch> 指定目標分支 (預設: 自動偵測最新 release 分支)
|
|
116
|
+
--head <branch> 指定來源分支 (預設: 當前分支)
|
|
117
|
+
--model <model> 指定 AI 模型 (預設: gpt-4.1)
|
|
118
|
+
--org <org-name> 指定 GitHub 組織名稱 (預設: kingsinfo-project)
|
|
119
|
+
--draft 創建草稿 PR
|
|
120
|
+
--preview 僅預覽 PR 內容,不實際創建
|
|
121
|
+
--no-confirm 跳過確認直接創建
|
|
122
|
+
--auto-reviewers 自動選擇 reviewers(不互動)
|
|
123
|
+
--auto-labels 自動添加 Labels(預設啟用)
|
|
124
|
+
--no-labels 不添加 Labels
|
|
125
|
+
--help 顯示此說明
|
|
126
|
+
|
|
127
|
+
範例:
|
|
128
|
+
node scripts/ai-auto-pr.mjs
|
|
129
|
+
node scripts/ai-auto-pr.mjs --base main --draft
|
|
130
|
+
node scripts/ai-auto-pr.mjs --auto-reviewers --auto-labels
|
|
131
|
+
`);
|
|
132
|
+
}
|