ai-git-tools 2.0.71 → 2.0.73
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
CHANGED
package/src/core/ai-client.js
CHANGED
|
@@ -9,7 +9,7 @@ export class AIClient {
|
|
|
9
9
|
/**
|
|
10
10
|
* 發送 prompt 並等待回應(帶重試機制和超時保護)
|
|
11
11
|
*/
|
|
12
|
-
static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3, timeout =
|
|
12
|
+
static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3, timeout = 150000) {
|
|
13
13
|
let lastError = null;
|
|
14
14
|
|
|
15
15
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
@@ -2,24 +2,52 @@ import { CopilotClient, approveAll } from '@github/copilot-sdk';
|
|
|
2
2
|
import { CONSTANTS, PROJECT_SKILLS_CONTEXT } from '../utils/constants.js';
|
|
3
3
|
import { getSkillsSummaryForPrompt, log } from '../utils/helpers.js';
|
|
4
4
|
|
|
5
|
+
// CopilotClient 子程序啟動 + AI 模型回應可能共需 60-120s,設為 150s 保留足夠緩衝
|
|
6
|
+
const AI_TIMEOUT_MS = 150000;
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* AI 分析器 - 負責程式碼分析和 PR 內容生成
|
|
7
10
|
*/
|
|
8
11
|
export class AIAnalyzer {
|
|
9
12
|
constructor(config = {}) {
|
|
10
13
|
this.model = config.model || 'gpt-4.1';
|
|
14
|
+
this._client = null; // 複用同一個 CopilotClient,避免重複啟動子程序
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 取得(或建立)共用的 CopilotClient
|
|
19
|
+
*/
|
|
20
|
+
async _getOrCreateClient() {
|
|
21
|
+
if (!this._client) {
|
|
22
|
+
this._client = new CopilotClient();
|
|
23
|
+
}
|
|
24
|
+
return this._client;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
/**
|
|
14
|
-
* 建立 AI
|
|
28
|
+
* 建立 AI Session(複用已有的 client)
|
|
15
29
|
*/
|
|
16
|
-
async
|
|
17
|
-
const client =
|
|
18
|
-
|
|
30
|
+
async _createSession() {
|
|
31
|
+
const client = await this._getOrCreateClient();
|
|
32
|
+
return client.createSession({
|
|
19
33
|
model: this.model,
|
|
20
|
-
onPermissionRequest: approveAll
|
|
34
|
+
onPermissionRequest: approveAll,
|
|
21
35
|
});
|
|
22
|
-
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 釋放 CopilotClient 子程序資源
|
|
40
|
+
*/
|
|
41
|
+
async close() {
|
|
42
|
+
if (this._client) {
|
|
43
|
+
try {
|
|
44
|
+
await this._client.stop();
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// 忽略關閉錯誤
|
|
47
|
+
} finally {
|
|
48
|
+
this._client = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
23
51
|
}
|
|
24
52
|
|
|
25
53
|
/**
|
|
@@ -29,31 +57,23 @@ export class AIAnalyzer {
|
|
|
29
57
|
const skillsSummary = getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT);
|
|
30
58
|
const prompt = this.buildPRPrompt(commits, diff, skillsSummary);
|
|
31
59
|
|
|
32
|
-
const
|
|
60
|
+
const session = await this._createSession();
|
|
33
61
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
62
|
+
// 使用超時保護(150 秒,含子程序啟動 + AI 回應)
|
|
63
|
+
const responsePromise = session.sendAndWait({ prompt });
|
|
64
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
65
|
+
setTimeout(() => reject(new Error(`AI 請求超時 (${AI_TIMEOUT_MS / 1000} 秒)`)), AI_TIMEOUT_MS);
|
|
66
|
+
});
|
|
40
67
|
|
|
41
|
-
|
|
42
|
-
|
|
68
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
69
|
+
const prContent = response?.data.content?.trim() || '';
|
|
43
70
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return this.parsePRContent(prContent);
|
|
49
|
-
} finally {
|
|
50
|
-
// 確保 client 一定會被關閉
|
|
51
|
-
try {
|
|
52
|
-
await client.stop();
|
|
53
|
-
} catch (e) {
|
|
54
|
-
// 忽略關閉錯誤
|
|
55
|
-
}
|
|
71
|
+
if (!prContent) {
|
|
72
|
+
throw new Error('AI 未能生成 PR 內容');
|
|
56
73
|
}
|
|
74
|
+
|
|
75
|
+
return this.parsePRContent(prContent);
|
|
76
|
+
// 注意:不在此 stop() client,改由 close() 統一清理以便複用
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
/**
|
|
@@ -63,15 +83,15 @@ export class AIAnalyzer {
|
|
|
63
83
|
const skillsSummary = getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT);
|
|
64
84
|
const prompt = this.buildAnalysisPrompt(changedFiles, diff, commits, skillsSummary);
|
|
65
85
|
|
|
66
|
-
const
|
|
86
|
+
const session = await this._createSession();
|
|
67
87
|
|
|
68
88
|
try {
|
|
69
89
|
log.info(' 正在使用 AI 深度分析程式碼變更...');
|
|
70
|
-
|
|
71
|
-
//
|
|
90
|
+
|
|
91
|
+
// 使用超時保護(150 秒)
|
|
72
92
|
const responsePromise = session.sendAndWait({ prompt });
|
|
73
93
|
const timeoutPromise = new Promise((_, reject) => {
|
|
74
|
-
setTimeout(() => reject(new Error(
|
|
94
|
+
setTimeout(() => reject(new Error(`AI 請求超時 (${AI_TIMEOUT_MS / 1000} 秒)`)), AI_TIMEOUT_MS);
|
|
75
95
|
});
|
|
76
96
|
|
|
77
97
|
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
@@ -103,14 +123,8 @@ export class AIAnalyzer {
|
|
|
103
123
|
} catch (error) {
|
|
104
124
|
log.warning(` AI 分析失敗 (${error.message}),使用基礎分析...\n`);
|
|
105
125
|
return this.getFallbackAnalysis(changedFiles);
|
|
106
|
-
} finally {
|
|
107
|
-
// 確保 client 一定會被關閉
|
|
108
|
-
try {
|
|
109
|
-
await client.stop();
|
|
110
|
-
} catch (e) {
|
|
111
|
-
// 忽略關閉錯誤
|
|
112
|
-
}
|
|
113
126
|
}
|
|
127
|
+
// 注意:不在此 stop() client,改由 close() 統一清理以便複用
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
/**
|
|
@@ -129,13 +143,25 @@ ${skillsSummary}
|
|
|
129
143
|
> type 必須是以下之一:feat / fix / refactor / style / docs / test / chore / perf
|
|
130
144
|
> **重要**:如果有新增任何功能、新增檔案、新增 API、新增組件,優先使用 **feat**
|
|
131
145
|
|
|
132
|
-
##
|
|
133
|
-
[
|
|
146
|
+
## � 摘要
|
|
147
|
+
[1-2 句話說明這個 PR 做了什麼、解決了什麼問題]
|
|
134
148
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- [
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## ✏️ 做了什麼 (What)
|
|
152
|
+
- [具體變更項目 1]
|
|
153
|
+
- [具體變更項目 2]
|
|
154
|
+
- [具體變更項目 3]
|
|
155
|
+
|
|
156
|
+
## ❓ 為什麼 (Why)
|
|
157
|
+
- [說明這個改動的背景或原因]
|
|
158
|
+
- [使用者痛點或產品需求]
|
|
159
|
+
|
|
160
|
+
## ⚙️ 怎麼做到的 (How)
|
|
161
|
+
- [技術實作重點說明 1]
|
|
162
|
+
- [技術實作重點說明 2]
|
|
163
|
+
|
|
164
|
+
---
|
|
139
165
|
|
|
140
166
|
## 🔀 變更類型
|
|
141
167
|
- [ ] ✨ 新功能 (feat)
|
|
@@ -159,53 +185,51 @@ ${skillsSummary}
|
|
|
159
185
|
>
|
|
160
186
|
> **特別注意**: 如果 diff 中有「新增檔案」或「新增功能」,**務必勾選** ✨ 新功能 (feat)
|
|
161
187
|
|
|
162
|
-
##
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
3. [具體的測試步驟 3]
|
|
166
|
-
|
|
167
|
-
## 💥 Breaking Changes
|
|
168
|
-
[如果有破壞性變更請詳細說明,沒有則填寫「無」]
|
|
169
|
-
|
|
170
|
-
## 📌 注意事項
|
|
171
|
-
[需要特別注意的事項]
|
|
172
|
-
|
|
173
|
-
## 📸 截圖
|
|
174
|
-
[如果是 UI 變更,提醒需要截圖]
|
|
188
|
+
## 🎯 影響範圍
|
|
189
|
+
- 影響模組:[列出受影響的頁面 / 模組 / API]
|
|
190
|
+
- 無影響其他模組(或說明有影響的範圍)
|
|
175
191
|
|
|
176
192
|
---
|
|
177
193
|
|
|
178
|
-
##
|
|
194
|
+
## 🧪 測試項目
|
|
195
|
+
- [ ] [測試項目 1]
|
|
196
|
+
- [ ] [測試項目 2]
|
|
197
|
+
- [ ] [測試項目 3]
|
|
179
198
|
|
|
180
|
-
|
|
199
|
+
## 📸 截圖(UI 變更必填)
|
|
200
|
+
[放截圖,非 UI 變更可填 N/A]
|
|
181
201
|
|
|
182
|
-
|
|
202
|
+
---
|
|
183
203
|
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
## ⚠️ 風險與注意事項
|
|
205
|
+
**Risk Level**: \`LOW\` / \`MEDIUM\` / \`HIGH\`
|
|
186
206
|
|
|
187
|
-
|
|
188
|
-
[列出本次變更中違反的規則]
|
|
207
|
+
[說明潛在風險、破壞性變更(breaking changes)、需要特別小心的地方;沒有則填「無」]
|
|
189
208
|
|
|
190
|
-
|
|
209
|
+
## 👀 Reviewer 重點
|
|
210
|
+
- [請 reviewer 特別關注的邏輯或設計決策 1]
|
|
211
|
+
- [請 reviewer 特別關注的邏輯或設計決策 2]
|
|
191
212
|
|
|
192
|
-
|
|
213
|
+
## 🔗 相關 Issue
|
|
214
|
+
- Closes #[issue 編號](沒有則填「無」)
|
|
193
215
|
|
|
194
216
|
---
|
|
195
217
|
|
|
196
218
|
**規則**:
|
|
197
219
|
1. PR 標題格式:type: 簡短描述(不超過 50 字)
|
|
198
220
|
2. type 必須符合 Conventional Commits
|
|
199
|
-
3.
|
|
200
|
-
4.
|
|
201
|
-
5.
|
|
202
|
-
6.
|
|
221
|
+
3. 摘要用 1-2 句話說明目的,不要流水帳
|
|
222
|
+
4. Why 說明背景原因,How 說明技術手段,兩者不可混淆
|
|
223
|
+
5. 全部使用繁體中文(台灣正體)
|
|
224
|
+
6. 不要在開頭加引導語句,直接輸出 # [type]: [標題]
|
|
203
225
|
7. **變更類型判斷必須準確**:
|
|
204
226
|
- 檢查 diff 中是否有 "new file mode" 或大量 "+++" 行(表示新增檔案)
|
|
205
227
|
- 檢查 commit 訊息是否包含「新增」、「add」、「feat」等關鍵字
|
|
206
|
-
- 檢查主要變更列表,如果提到「新增 xxx」就必須勾選 ✨ 新功能 (feat)
|
|
207
228
|
- 新增配置檔、新增組件、新增 API、新增功能都算 feat
|
|
208
229
|
- 一個 PR 可以同時是多種類型(如:feat + refactor + chore)
|
|
230
|
+
8. **Risk Level 判斷**:HIGH = 影響付款/登入/資料寫入核心流程;MEDIUM = 影響現有功能但有降級保護;LOW = 新增功能或純重構
|
|
231
|
+
9. **Reviewer 重點**:列出最值得仔細看的 1-3 個地方(核心演算法、架構決策、潛在邊界條件)
|
|
232
|
+
10. **相關 Issue**:若 commit 訊息含 #issue 編號或 closes/fixes/resolves,自動帶入;否則填「無」
|
|
209
233
|
|
|
210
234
|
---
|
|
211
235
|
|
|
@@ -32,70 +32,75 @@ export class PRWorkflow {
|
|
|
32
32
|
* 執行完整工作流程
|
|
33
33
|
*/
|
|
34
34
|
async execute() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
try {
|
|
36
|
+
// 0. 確認 gh CLI 已登入(預覽模式可跳過)
|
|
37
|
+
if (!this.config.preview) {
|
|
38
|
+
const auth = this.github.checkAuth();
|
|
39
|
+
if (!auth.authenticated) {
|
|
40
|
+
log.error('GitHub CLI 未登入,請先執行: gh auth login');
|
|
41
|
+
throw new Error('GitHub CLI 未登入');
|
|
42
|
+
}
|
|
41
43
|
}
|
|
42
|
-
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// 1. 驗證環境和分支
|
|
46
|
+
const { baseBranch, headBranch } = await this.detectAndValidateBranches();
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// 2. 檢查是否有變更
|
|
49
|
+
await this.validateChanges(baseBranch, headBranch);
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// 3. 推送到遠端(預覽模式跳過)
|
|
52
|
+
if (!this.config.preview) {
|
|
53
|
+
await this.pushToRemote(headBranch);
|
|
54
|
+
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
// 4. 收集變更資訊
|
|
57
|
+
const changeData = this.collectChangeData(baseBranch, headBranch);
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
// 5. AI 分析和生成 PR 內容
|
|
60
|
+
const prContent = await this.generatePRContent(changeData);
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// 6. 顯示預覽
|
|
63
|
+
this.displayPreview(prContent, changeData.stats);
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
// 預覽模式:僅顯示不創建
|
|
66
|
+
if (this.config.preview) {
|
|
67
|
+
log.info('預覽模式:未創建 PR');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
// 7. 選擇 Reviewers
|
|
72
|
+
const reviewers = await this.selectReviewers(changeData);
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
// 8. 確認創建
|
|
75
|
+
if (!this.config.noConfirm) {
|
|
76
|
+
const confirmed = await this.askConfirmation('是否創建此 Pull Request?');
|
|
77
|
+
if (!confirmed) {
|
|
78
|
+
log.info('已取消創建 PR');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
79
81
|
}
|
|
80
|
-
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
// 9. 創建 PR
|
|
84
|
+
const prUrl = await this.createPR(prContent, baseBranch, headBranch, reviewers);
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
// 10. 添加 Labels(如果啟用)
|
|
87
|
+
if (this.config.github.autoLabels === true && prUrl) {
|
|
88
|
+
try {
|
|
89
|
+
const prNumber = prUrl.split('/').pop();
|
|
90
|
+
await this.addLabels(prNumber, {
|
|
91
|
+
...prContent,
|
|
92
|
+
stats: changeData.stats,
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
log.warning('無法自動添加 Labels: ' + error.message);
|
|
96
|
+
}
|
|
95
97
|
}
|
|
96
|
-
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
this.logger.success('完成!');
|
|
100
|
+
} finally {
|
|
101
|
+
// 確保 AI 子程序一定被釋放
|
|
102
|
+
await this.ai.close();
|
|
103
|
+
}
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
/**
|