ai-git-tools 2.0.68 → 2.0.70
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/bin/cli.js +14 -90
- package/package.json +1 -1
- package/src/commands/dev-from-issue.js +0 -4
- package/src/core/ai-client.js +1 -1
- package/src/pr-modules/ai/code-analyzer.js +42 -27
- package/src/pr-modules/core/workflow.js +54 -49
- package/src/commands/auto-dev.js +0 -256
- package/src/commands/generate-code.js +0 -115
- package/src/commands/plan-issue.js +0 -91
- package/src/commands/write-and-test.js +0 -469
- package/src/core/test-generator.js +0 -165
- package/src/core/test-runner.js +0 -132
package/bin/cli.js
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* AI Git Tools CLI
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* AI-powered Git automation for commit messages and PR generation
|
|
7
7
|
* 完全重寫版本基於 scripts/ 原始實現
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
// 抑制 @github/copilot-sdk 子程序的 node:sqlite 實驗性警告
|
|
11
|
+
process.env.NODE_NO_WARNINGS = '1';
|
|
12
|
+
|
|
10
13
|
import { Command } from 'commander';
|
|
11
14
|
import { readFileSync } from 'fs';
|
|
12
15
|
import { fileURLToPath } from 'url';
|
|
@@ -15,18 +18,15 @@ import { commitCommand } from '../src/commands/commit.js';
|
|
|
15
18
|
import { commitAllCommand } from '../src/commands/commit-all.js';
|
|
16
19
|
import { prCommand } from '../src/commands/pr.js';
|
|
17
20
|
import { initCommand } from '../src/commands/init.js';
|
|
18
|
-
import { planIssueCommand } from '../src/commands/plan-issue.js';
|
|
19
|
-
import { generateCodeCommand } from '../src/commands/generate-code.js';
|
|
20
|
-
import { devFromIssueCommand } from '../src/commands/dev-from-issue.js';
|
|
21
|
+
// import { planIssueCommand } from '../src/commands/plan-issue.js';
|
|
22
|
+
// import { generateCodeCommand } from '../src/commands/generate-code.js';
|
|
21
23
|
import { writeAndTestCommand } from '../src/commands/write-and-test.js';
|
|
22
24
|
import { autoDevCommand } from '../src/commands/auto-dev.js';
|
|
23
25
|
|
|
24
26
|
// 讀取 package.json 獲取版本號
|
|
25
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
28
|
const __dirname = dirname(__filename);
|
|
27
|
-
const packageJson = JSON.parse(
|
|
28
|
-
readFileSync(join(__dirname, '../package.json'), 'utf-8')
|
|
29
|
-
);
|
|
29
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
30
30
|
|
|
31
31
|
const program = new Command();
|
|
32
32
|
|
|
@@ -39,7 +39,7 @@ program
|
|
|
39
39
|
program
|
|
40
40
|
.command('init')
|
|
41
41
|
.description('初始化配置檔案 (.ai-git-config.mjs)')
|
|
42
|
-
.action(async
|
|
42
|
+
.action(async options => {
|
|
43
43
|
try {
|
|
44
44
|
await initCommand(options);
|
|
45
45
|
process.exit(0);
|
|
@@ -56,7 +56,7 @@ program
|
|
|
56
56
|
.option('-v, --verbose', '顯示詳細輸出')
|
|
57
57
|
.option('--max-diff <number>', '最大 diff 長度')
|
|
58
58
|
.option('--max-retries <number>', '最大重試次數')
|
|
59
|
-
.action(async
|
|
59
|
+
.action(async options => {
|
|
60
60
|
try {
|
|
61
61
|
await commitCommand(options);
|
|
62
62
|
process.exit(0);
|
|
@@ -73,7 +73,7 @@ program
|
|
|
73
73
|
.option('-v, --verbose', '顯示詳細輸出')
|
|
74
74
|
.option('--max-diff <number>', '最大 diff 長度')
|
|
75
75
|
.option('--max-retries <number>', '最大重試次數')
|
|
76
|
-
.action(async
|
|
76
|
+
.action(async options => {
|
|
77
77
|
try {
|
|
78
78
|
await commitAllCommand(options);
|
|
79
79
|
process.exit(0);
|
|
@@ -93,7 +93,7 @@ program
|
|
|
93
93
|
.option('--auto-labels', '自動添加 Labels (預設啟用)')
|
|
94
94
|
.option('--include-impact', '在 PR 中包含影響範圍分析和注意事項 (預設關閉)')
|
|
95
95
|
.option('--force-new', '強制創建新 PR,不更新現有 PR')
|
|
96
|
-
.action(async
|
|
96
|
+
.action(async options => {
|
|
97
97
|
try {
|
|
98
98
|
await prCommand(options);
|
|
99
99
|
process.exit(0);
|
|
@@ -102,99 +102,23 @@ program
|
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
// Plan Issue 命令
|
|
106
|
-
program
|
|
107
|
-
.command('plan-issue')
|
|
108
|
-
.description('AI 讀取 GitHub Issue 並生成結構化的實現計畫')
|
|
109
|
-
.requiredOption('--number <number>', 'Issue 編號')
|
|
110
|
-
.option('--no-confirm', '跳過確認,自動返回計畫')
|
|
111
|
-
.option('--model <model>', '指定 AI 模型')
|
|
112
|
-
.option('-v, --verbose', '顯示 Issue 詳細內容')
|
|
113
|
-
.action(async (options) => {
|
|
114
|
-
try {
|
|
115
|
-
await planIssueCommand(options);
|
|
116
|
-
process.exit(0);
|
|
117
|
-
} catch (error) {
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Generate Code 命令
|
|
123
|
-
program
|
|
124
|
-
.command('generate-code')
|
|
125
|
-
.description('AI 依據 GitHub Issue 生成符合專案風格的代碼')
|
|
126
|
-
.requiredOption('--issue <number>', 'Issue 編號')
|
|
127
|
-
.requiredOption('--file <path>', '目標檔案路徑(不可已存在)')
|
|
128
|
-
.option('--context <description>', '額外說明或補充需求')
|
|
129
|
-
.option('--max-lines <number>', '最大行數限制(預設 500)', '500')
|
|
130
|
-
.option('--no-confirm', '跳過預覽確認,直接寫入')
|
|
131
|
-
.option('--model <model>', '指定 AI 模型')
|
|
132
|
-
.action(async (options) => {
|
|
133
|
-
try {
|
|
134
|
-
await generateCodeCommand(options);
|
|
135
|
-
process.exit(0);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Dev From Issue 命令(plan-issue + generate-code 合體)
|
|
142
|
-
program
|
|
143
|
-
.command('dev-from-issue')
|
|
144
|
-
.description('AI 讀取 Issue → 生成計畫 → 生成代碼(完成後繼續執行 write-and-test)')
|
|
145
|
-
.requiredOption('--issue <number>', 'GitHub Issue 編號')
|
|
146
|
-
.option('--file <path>', '目標檔案路徑(若無則自動推斷)')
|
|
147
|
-
.option('--context <description>', '額外說明或補充需求')
|
|
148
|
-
.option('--max-lines <number>', '最大行數限制(預設 500)', '500')
|
|
149
|
-
.option('--model <model>', '指定 AI 模型')
|
|
150
|
-
.action(async (options) => {
|
|
151
|
-
try {
|
|
152
|
-
await devFromIssueCommand(options);
|
|
153
|
-
process.exit(0);
|
|
154
|
-
} catch (error) {
|
|
155
|
-
console.error(`\n[錯誤] ${error.message}`);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Write And Test 命令
|
|
161
|
-
program
|
|
162
|
-
.command('write-and-test')
|
|
163
|
-
.description('AI 為檔案生成測試(單元/元件),自動執行與修復,完成後發報告到 Issue(將自動讀取 dev-from-issue 的結果)')
|
|
164
|
-
.option('--file <path>', '原始碼路徑(對沒特別指定則自動從上次 dev-from-issue 讀取)')
|
|
165
|
-
.option('--issue <number>', '完成後發佈報告到此 Issue(對沒特別指定則自動從上次 dev-from-issue 讀取)')
|
|
166
|
-
.option('--test-type <type>', '測試類型:auto、unit、component 或 both(可用逗號指定多個)', 'auto')
|
|
167
|
-
.option('--test-dir <path>', '測試輸出目錄(預設 __tests__;指定 tests 則放在專案根目錄)', '__tests__')
|
|
168
|
-
.option('--max-fixes <number>', '最大自動修復次數(預設 3)', '3')
|
|
169
|
-
.option('--model <model>', '指定 AI 模型')
|
|
170
|
-
.action(async (options) => {
|
|
171
|
-
try {
|
|
172
|
-
await writeAndTestCommand(options);
|
|
173
|
-
process.exit(0);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
console.error(`\n[錯誤] ${error.message}`);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
105
|
// Auto Dev 整合命令
|
|
181
106
|
program
|
|
182
107
|
.command('auto-dev')
|
|
183
|
-
.description('一鍵自動化:從 GitHub Issue
|
|
108
|
+
.description('一鍵自動化:從 GitHub Issue 到代碼生成、測試與提交')
|
|
184
109
|
.requiredOption('--issue <number>', 'GitHub Issue 編號')
|
|
185
110
|
.option('--file <path>', '目標檔案路徑(若無則自動推斷)')
|
|
186
111
|
.option('--context <description>', '額外說明或補充需求')
|
|
187
|
-
.option('--test-type <type>', '測試類型:auto、unit
|
|
112
|
+
.option('--test-type <type>', '測試類型:auto、unit 或 component(預設 auto)', 'auto')
|
|
188
113
|
.option('--max-lines <number>', '最大行數限制(預設 500)', '500')
|
|
189
114
|
.option('--max-fixes <number>', '最大自動修復次數(預設 2)', '2')
|
|
190
115
|
.option('--no-confirm', '全自動模式,跳過所有確認')
|
|
191
116
|
.option('--model <model>', '指定 AI 模型')
|
|
192
|
-
.action(async
|
|
117
|
+
.action(async options => {
|
|
193
118
|
try {
|
|
194
119
|
await autoDevCommand(options);
|
|
195
120
|
process.exit(0);
|
|
196
121
|
} catch (error) {
|
|
197
|
-
console.error(`\n[錯誤] ${error.message}`);
|
|
198
122
|
process.exit(1);
|
|
199
123
|
}
|
|
200
124
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dev-from-issue 命令
|
|
3
3
|
* 讀取 GitHub Issue → 生成實現計畫 → 生成代碼
|
|
4
|
-
* 相當於 plan-issue + generate-code 的合體,並引導你下一步執行 write-and-test
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { resolve, join } from 'path';
|
|
@@ -100,9 +99,6 @@ export async function devFromIssueCommand(options = {}) {
|
|
|
100
99
|
logger.section('✅ 開發完成');
|
|
101
100
|
console.log(`Issue:#${issue.number} ${issue.title}`);
|
|
102
101
|
console.log(`檔案 :${filePath}`);
|
|
103
|
-
console.log('');
|
|
104
|
-
console.log('💡 下一步:執行測試並產生報告');
|
|
105
|
-
console.log(' ai-git-tools write-and-test');
|
|
106
102
|
}
|
|
107
103
|
|
|
108
104
|
// ============================================================
|
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 = 120000) {
|
|
13
13
|
let lastError = null;
|
|
14
14
|
|
|
15
15
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
@@ -2,24 +2,51 @@ 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
|
+
const AI_TIMEOUT_MS = 120000; // 2 分鐘:子程序啟動 + AI 回應時間
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* AI 分析器 - 負責程式碼分析和 PR 內容生成
|
|
7
9
|
*/
|
|
8
10
|
export class AIAnalyzer {
|
|
9
11
|
constructor(config = {}) {
|
|
10
12
|
this.model = config.model || 'gpt-4.1';
|
|
13
|
+
this._client = null; // 複用同一個 CopilotClient,避免重複啟動子程序
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 取得(或建立)共用的 CopilotClient
|
|
18
|
+
*/
|
|
19
|
+
async _getOrCreateClient() {
|
|
20
|
+
if (!this._client) {
|
|
21
|
+
this._client = new CopilotClient();
|
|
22
|
+
}
|
|
23
|
+
return this._client;
|
|
11
24
|
}
|
|
12
25
|
|
|
13
26
|
/**
|
|
14
|
-
*
|
|
27
|
+
* 建立新的 AI Session(複用已有的 client)
|
|
15
28
|
*/
|
|
16
|
-
async
|
|
17
|
-
const client =
|
|
18
|
-
|
|
29
|
+
async _createSession() {
|
|
30
|
+
const client = await this._getOrCreateClient();
|
|
31
|
+
return client.createSession({
|
|
19
32
|
model: this.model,
|
|
20
|
-
onPermissionRequest: approveAll
|
|
33
|
+
onPermissionRequest: approveAll,
|
|
21
34
|
});
|
|
22
|
-
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 釋放 CopilotClient 子程序資源
|
|
39
|
+
*/
|
|
40
|
+
async close() {
|
|
41
|
+
if (this._client) {
|
|
42
|
+
try {
|
|
43
|
+
await this._client.stop();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// 忽略關閉錯誤
|
|
46
|
+
} finally {
|
|
47
|
+
this._client = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
23
50
|
}
|
|
24
51
|
|
|
25
52
|
/**
|
|
@@ -29,13 +56,13 @@ export class AIAnalyzer {
|
|
|
29
56
|
const skillsSummary = getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT);
|
|
30
57
|
const prompt = this.buildPRPrompt(commits, diff, skillsSummary);
|
|
31
58
|
|
|
32
|
-
const
|
|
59
|
+
const session = await this._createSession();
|
|
33
60
|
|
|
34
61
|
try {
|
|
35
|
-
//
|
|
62
|
+
// 使用超時保護(2 分鐘:包含子程序啟動時間)
|
|
36
63
|
const responsePromise = session.sendAndWait({ prompt });
|
|
37
64
|
const timeoutPromise = new Promise((_, reject) => {
|
|
38
|
-
setTimeout(() => reject(new Error(
|
|
65
|
+
setTimeout(() => reject(new Error(`AI 請求超時 (${AI_TIMEOUT_MS / 1000} 秒)`)), AI_TIMEOUT_MS);
|
|
39
66
|
});
|
|
40
67
|
|
|
41
68
|
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
@@ -46,14 +73,8 @@ export class AIAnalyzer {
|
|
|
46
73
|
}
|
|
47
74
|
|
|
48
75
|
return this.parsePRContent(prContent);
|
|
49
|
-
} finally {
|
|
50
|
-
// 確保 client 一定會被關閉
|
|
51
|
-
try {
|
|
52
|
-
await client.stop();
|
|
53
|
-
} catch (e) {
|
|
54
|
-
// 忽略關閉錯誤
|
|
55
|
-
}
|
|
56
76
|
}
|
|
77
|
+
// 注意:不在此處 client.stop(),交由 close() 統一清理以便複用
|
|
57
78
|
}
|
|
58
79
|
|
|
59
80
|
/**
|
|
@@ -63,15 +84,15 @@ export class AIAnalyzer {
|
|
|
63
84
|
const skillsSummary = getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT);
|
|
64
85
|
const prompt = this.buildAnalysisPrompt(changedFiles, diff, commits, skillsSummary);
|
|
65
86
|
|
|
66
|
-
const
|
|
87
|
+
const session = await this._createSession();
|
|
67
88
|
|
|
68
89
|
try {
|
|
69
90
|
log.info(' 正在使用 AI 深度分析程式碼變更...');
|
|
70
|
-
|
|
71
|
-
//
|
|
91
|
+
|
|
92
|
+
// 使用超時保護(2 分鐘:包含子程序啟動時間)
|
|
72
93
|
const responsePromise = session.sendAndWait({ prompt });
|
|
73
94
|
const timeoutPromise = new Promise((_, reject) => {
|
|
74
|
-
setTimeout(() => reject(new Error(
|
|
95
|
+
setTimeout(() => reject(new Error(`AI 請求超時 (${AI_TIMEOUT_MS / 1000} 秒)`)), AI_TIMEOUT_MS);
|
|
75
96
|
});
|
|
76
97
|
|
|
77
98
|
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
@@ -103,14 +124,8 @@ export class AIAnalyzer {
|
|
|
103
124
|
} catch (error) {
|
|
104
125
|
log.warning(` AI 分析失敗 (${error.message}),使用基礎分析...\n`);
|
|
105
126
|
return this.getFallbackAnalysis(changedFiles);
|
|
106
|
-
} finally {
|
|
107
|
-
// 確保 client 一定會被關閉
|
|
108
|
-
try {
|
|
109
|
-
await client.stop();
|
|
110
|
-
} catch (e) {
|
|
111
|
-
// 忽略關閉錯誤
|
|
112
|
-
}
|
|
113
127
|
}
|
|
128
|
+
// 注意:不在此處 client.stop(),交由 close() 統一清理以便複用
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
/**
|
|
@@ -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
|
/**
|