ai-git-tools 2.0.71 → 2.0.72
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,避免重複啟動子程序
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
|
-
*
|
|
18
|
+
* 取得(或建立)共用的 CopilotClient
|
|
15
19
|
*/
|
|
16
|
-
async
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
async _getOrCreateClient() {
|
|
21
|
+
if (!this._client) {
|
|
22
|
+
this._client = new CopilotClient();
|
|
23
|
+
}
|
|
24
|
+
return this._client;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 建立 AI Session(複用已有的 client)
|
|
29
|
+
*/
|
|
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
|
-
});
|
|
40
|
-
|
|
41
|
-
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
42
|
-
const prContent = response?.data.content?.trim() || '';
|
|
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
|
+
});
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
68
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
69
|
+
const prContent = response?.data.content?.trim() || '';
|
|
47
70
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
/**
|
|
@@ -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
|
/**
|