ai-git-tools 1.0.4 → 2.0.0
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 +24 -45
- package/package.json +1 -1
- package/src/commands/commit-all.js +250 -179
- package/src/commands/commit.js +92 -76
- package/src/commands/init.js +33 -131
- package/src/commands/pr.js +122 -238
- package/src/core/ai-client.js +19 -84
- package/src/core/config-loader.js +165 -133
- package/src/core/git-operations.js +103 -183
- package/src/utils/helpers.js +43 -54
- package/src/utils/logger.js +28 -100
- package/src/commands/workflow.js +0 -36
- package/src/core/github-api.js +0 -139
- package/src/index.js +0 -16
package/src/commands/pr.js
CHANGED
|
@@ -1,303 +1,187 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PR 命令
|
|
3
|
-
*
|
|
4
|
-
* 生成 PR
|
|
3
|
+
* 基于 scripts/ai-auto-pr.mjs 简化版
|
|
4
|
+
* 生成 PR 标题、描述并创建 Pull Request
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import { loadConfig } from '../core/config-loader.js';
|
|
10
|
-
import { AIClient } from '../core/ai-client.js';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { loadPRConfig } from '../core/config-loader.js';
|
|
11
9
|
import { GitOperations } from '../core/git-operations.js';
|
|
12
|
-
import {
|
|
10
|
+
import { AIClient } from '../core/ai-client.js';
|
|
13
11
|
import { Logger } from '../utils/logger.js';
|
|
14
|
-
import {
|
|
15
|
-
handleError,
|
|
16
|
-
truncateText,
|
|
17
|
-
getProjectTypePrompt,
|
|
18
|
-
} from '../utils/helpers.js';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 決定 base 分支
|
|
22
|
-
*/
|
|
23
|
-
async function determineBaseBranch(config, logger) {
|
|
24
|
-
if (config.baseBranch && config.baseBranch !== 'auto') {
|
|
25
|
-
return config.baseBranch;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 嘗試找最新的 release 分支
|
|
29
|
-
logger.startSpinner('尋找目標分支...');
|
|
30
|
-
const latestRelease = GitHubAPI.getLatestReleaseBranch();
|
|
31
|
-
|
|
32
|
-
if (latestRelease) {
|
|
33
|
-
logger.succeedSpinner(`自動選擇目標分支: ${latestRelease}`);
|
|
34
|
-
return latestRelease;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 預設使用 main 或 master
|
|
38
|
-
const defaultBranch = GitOperations.branchExists('main') ? 'main' : 'master';
|
|
39
|
-
logger.succeedSpinner(`使用預設分支: ${defaultBranch}`);
|
|
40
|
-
return defaultBranch;
|
|
41
|
-
}
|
|
12
|
+
import { handleError, getProjectTypePrompt } from '../utils/helpers.js';
|
|
42
13
|
|
|
43
14
|
/**
|
|
44
|
-
*
|
|
15
|
+
* 生成 PR 内容
|
|
45
16
|
*/
|
|
46
|
-
async function generatePRContent(baseBranch, headBranch, config
|
|
47
|
-
logger
|
|
48
|
-
|
|
49
|
-
// 獲取 diff 和 commits
|
|
50
|
-
const diff = GitOperations.getDiffBetweenBranches(baseBranch, headBranch);
|
|
51
|
-
const commits = GitOperations.getCommitsBetweenBranches(baseBranch, headBranch);
|
|
17
|
+
async function generatePRContent(baseBranch, headBranch, config) {
|
|
18
|
+
const logger = new Logger();
|
|
19
|
+
logger.step('AI 正在分析变更并生成 PR 内容...');
|
|
52
20
|
|
|
53
|
-
|
|
21
|
+
// 获取 diff 和 commits
|
|
22
|
+
const diff = GitOperations.getDiff(baseBranch, headBranch);
|
|
23
|
+
const commits = GitOperations.getCommits(baseBranch, headBranch);
|
|
54
24
|
|
|
55
|
-
|
|
25
|
+
// 智能截断 diff
|
|
26
|
+
const truncatedDiff = GitOperations.truncateDiff(diff, config.ai.maxDiffLength);
|
|
56
27
|
|
|
57
28
|
const prompt = `${getProjectTypePrompt()}
|
|
58
29
|
|
|
59
|
-
|
|
30
|
+
请根据以下资讯生成一个 Pull Request 的标题和描述。
|
|
60
31
|
|
|
61
32
|
**Commits 列表**:
|
|
62
|
-
${commits
|
|
33
|
+
${commits}
|
|
63
34
|
|
|
64
35
|
**Git Diff**:
|
|
65
36
|
${truncatedDiff}
|
|
66
37
|
|
|
67
|
-
|
|
38
|
+
**输出格式**(JSON):
|
|
68
39
|
{
|
|
69
|
-
"title": "PR
|
|
70
|
-
"description": "PR 描述(使用 Markdown
|
|
40
|
+
"title": "PR 标题(简短、繁体中文,50 字内)",
|
|
41
|
+
"description": "PR 描述(使用 Markdown 格式,繁体中文)"
|
|
71
42
|
}
|
|
72
43
|
|
|
73
|
-
**PR
|
|
74
|
-
1. ## 📝
|
|
75
|
-
2. ## ✨
|
|
76
|
-
3. ## 🔧
|
|
77
|
-
4. ## ✅
|
|
44
|
+
**PR 描述应包含**:
|
|
45
|
+
1. ## 📝 变更摘要(简述主要变更)
|
|
46
|
+
2. ## ✨ 主要功能(列出新增功能或修正项目)
|
|
47
|
+
3. ## 🔧 技术细节(选填,如有重要的技术变更)
|
|
48
|
+
4. ## ✅ 测试(如何测试这些变更)
|
|
78
49
|
|
|
79
|
-
|
|
50
|
+
请只输出 JSON,不要其他文字。`;
|
|
80
51
|
|
|
81
|
-
const response = await
|
|
82
|
-
await aiClient.stop();
|
|
52
|
+
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
83
53
|
|
|
84
54
|
try {
|
|
85
55
|
const prContent = AIClient.parseJSON(response);
|
|
86
|
-
logger.
|
|
56
|
+
logger.success('PR 内容生成完成\n');
|
|
87
57
|
return prContent;
|
|
88
58
|
} catch (error) {
|
|
89
|
-
|
|
90
|
-
throw new Error(`無法解析 AI 回應: ${error.message}`);
|
|
59
|
+
throw new Error(`无法解析 AI 回应: ${error.message}`);
|
|
91
60
|
}
|
|
92
61
|
}
|
|
93
62
|
|
|
94
63
|
/**
|
|
95
|
-
*
|
|
64
|
+
* 显示 PR 预览
|
|
96
65
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const contributors = GitOperations.getFileContributors(
|
|
109
|
-
file,
|
|
110
|
-
config.reviewers.gitHistoryDepth
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
for (const { email, name } of contributors) {
|
|
114
|
-
// 排除設定中的作者
|
|
115
|
-
if (config.reviewers.excludeAuthors.some(pattern => email.includes(pattern))) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const count = contributorsMap.get(email) || 0;
|
|
120
|
-
contributorsMap.set(email, count + 1);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 排序並取前 N 位
|
|
125
|
-
const suggested = Array.from(contributorsMap.entries())
|
|
126
|
-
.sort((a, b) => b[1] - a[1])
|
|
127
|
-
.slice(0, config.reviewers.maxSuggested)
|
|
128
|
-
.map(([email]) => email);
|
|
129
|
-
|
|
130
|
-
logger.succeedSpinner(`找到 ${suggested.length} 位潛在 reviewers`);
|
|
131
|
-
|
|
132
|
-
if (suggested.length === 0) {
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 互動式選擇
|
|
137
|
-
const { selectedReviewers } = await inquirer.prompt([
|
|
138
|
-
{
|
|
139
|
-
type: 'checkbox',
|
|
140
|
-
name: 'selectedReviewers',
|
|
141
|
-
message: '選擇 Reviewers:',
|
|
142
|
-
choices: suggested.map(email => ({
|
|
143
|
-
name: email,
|
|
144
|
-
value: email,
|
|
145
|
-
})),
|
|
146
|
-
},
|
|
147
|
-
]);
|
|
148
|
-
|
|
149
|
-
return selectedReviewers;
|
|
66
|
+
function displayPreview(prContent, stats) {
|
|
67
|
+
console.log('\n' + '═'.repeat(60));
|
|
68
|
+
console.log('📋 PR 预览');
|
|
69
|
+
console.log('═'.repeat(60));
|
|
70
|
+
console.log(`\n标题: ${prContent.title}\n`);
|
|
71
|
+
console.log(`统计: ${stats.stats}`);
|
|
72
|
+
console.log(`文件数: ${stats.filesChanged} 个文件\n`);
|
|
73
|
+
console.log('─'.repeat(60));
|
|
74
|
+
console.log('描述:\n');
|
|
75
|
+
console.log(prContent.description);
|
|
76
|
+
console.log('\n' + '═'.repeat(60) + '\n');
|
|
150
77
|
}
|
|
151
78
|
|
|
152
79
|
/**
|
|
153
|
-
*
|
|
80
|
+
* PR 命令主函数
|
|
154
81
|
*/
|
|
155
|
-
function
|
|
156
|
-
const
|
|
157
|
-
const title = prContent.title.toLowerCase();
|
|
158
|
-
const description = prContent.description.toLowerCase();
|
|
159
|
-
|
|
160
|
-
// 根據關鍵字建議 labels
|
|
161
|
-
const labelKeywords = {
|
|
162
|
-
bug: ['fix', 'bug', '修正', '錯誤'],
|
|
163
|
-
enhancement: ['feat', 'feature', '新增', '功能'],
|
|
164
|
-
documentation: ['docs', '文件', 'readme'],
|
|
165
|
-
refactor: ['refactor', '重構'],
|
|
166
|
-
performance: ['perf', 'performance', '效能', '優化'],
|
|
167
|
-
test: ['test', '測試'],
|
|
168
|
-
ui: ['ui', 'style', '樣式', '介面'],
|
|
169
|
-
api: ['api', 'endpoint'],
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
for (const [label, keywords] of Object.entries(labelKeywords)) {
|
|
173
|
-
if (availableLabels.includes(label)) {
|
|
174
|
-
if (keywords.some(kw => title.includes(kw) || description.includes(kw))) {
|
|
175
|
-
suggestions.push(label);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return suggestions;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* PR 命令處理器
|
|
185
|
-
*/
|
|
186
|
-
export async function prCommand(options) {
|
|
187
|
-
const logger = new Logger(options.verbose);
|
|
82
|
+
export async function prCommand() {
|
|
83
|
+
const logger = new Logger();
|
|
188
84
|
|
|
189
85
|
try {
|
|
190
86
|
logger.header('AI Auto PR Generator');
|
|
191
87
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
88
|
+
// 载入配置
|
|
89
|
+
const config = await loadPRConfig();
|
|
90
|
+
|
|
91
|
+
// 获取当前分支
|
|
92
|
+
const currentBranch = GitOperations.getCurrentBranch();
|
|
93
|
+
let headBranch = config.headBranch || currentBranch;
|
|
94
|
+
let baseBranch = config.baseBranch;
|
|
95
|
+
|
|
96
|
+
// 自动侦测 base branch
|
|
97
|
+
if (!baseBranch) {
|
|
98
|
+
logger.step('正在侦测 release 分支...\n');
|
|
99
|
+
const latestRelease = GitOperations.findLatestReleaseBranch();
|
|
100
|
+
|
|
101
|
+
if (latestRelease) {
|
|
102
|
+
baseBranch = latestRelease;
|
|
103
|
+
logger.success(`自动选择目标分支: ${baseBranch}\n`);
|
|
104
|
+
} else {
|
|
105
|
+
logger.error('未侦测到任何 release 分支');
|
|
106
|
+
console.log('💡 使用 --base 参数指定目标分支');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
196
109
|
}
|
|
197
110
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
logger.
|
|
201
|
-
|
|
202
|
-
logger.info(' gh auth login');
|
|
111
|
+
// 检查是否在 base branch
|
|
112
|
+
if (currentBranch === baseBranch) {
|
|
113
|
+
logger.error(`你目前在 ${baseBranch} 分支,请切换到 feature 分支`);
|
|
114
|
+
console.log('💡 切换到 feature 分支: git checkout <feature-branch>');
|
|
203
115
|
process.exit(1);
|
|
204
116
|
}
|
|
205
117
|
|
|
206
|
-
|
|
207
|
-
const config = await loadConfig(options);
|
|
208
|
-
|
|
209
|
-
// 確定分支
|
|
210
|
-
const headBranch = config.headBranch || GitOperations.getCurrentBranch();
|
|
211
|
-
const baseBranch = await determineBaseBranch(config, logger);
|
|
212
|
-
|
|
213
|
-
console.log(chalk.cyan(`\n📌 分支資訊:`));
|
|
118
|
+
console.log('\n📌 分支资讯:');
|
|
214
119
|
console.log(` Base: ${baseBranch}`);
|
|
215
|
-
console.log(` Head: ${headBranch}`);
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
if (
|
|
219
|
-
logger.
|
|
220
|
-
|
|
120
|
+
console.log(` Head: ${headBranch}\n`);
|
|
121
|
+
|
|
122
|
+
// 推送到远端(预览模式跳过)
|
|
123
|
+
if (!config.preview) {
|
|
124
|
+
logger.step(`推送到远端分支: origin/${headBranch}`);
|
|
125
|
+
GitOperations.push(headBranch);
|
|
126
|
+
logger.success('推送成功\n');
|
|
127
|
+
|
|
128
|
+
// 等待 GitHub 同步
|
|
129
|
+
logger.info('等待 GitHub 同步...');
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
131
|
+
GitOperations.fetch();
|
|
132
|
+
logger.success('同步完成\n');
|
|
221
133
|
}
|
|
222
134
|
|
|
223
|
-
//
|
|
224
|
-
const
|
|
135
|
+
// 获取变更统计
|
|
136
|
+
const stats = GitOperations.getChangeStats(baseBranch, headBranch);
|
|
137
|
+
console.log(`📈 变更统计: ${stats.stats}`);
|
|
138
|
+
console.log(`📁 影响文件: ${stats.filesChanged} 个\n`);
|
|
225
139
|
|
|
226
|
-
|
|
227
|
-
|
|
140
|
+
// 生成 PR 内容
|
|
141
|
+
const prContent = await generatePRContent(baseBranch, headBranch, config);
|
|
228
142
|
|
|
229
|
-
|
|
230
|
-
|
|
143
|
+
// 显示预览
|
|
144
|
+
displayPreview(prContent, stats);
|
|
231
145
|
|
|
232
|
-
//
|
|
146
|
+
// 预览模式:仅显示不创建
|
|
233
147
|
if (config.preview) {
|
|
234
|
-
logger.info('
|
|
235
|
-
|
|
148
|
+
logger.info('预览模式:未创建 PR');
|
|
149
|
+
return;
|
|
236
150
|
}
|
|
237
151
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
const { confirm } = await inquirer.prompt([
|
|
241
|
-
{
|
|
242
|
-
type: 'confirm',
|
|
243
|
-
name: 'confirm',
|
|
244
|
-
message: '是否創建 PR?',
|
|
245
|
-
default: true,
|
|
246
|
-
},
|
|
247
|
-
]);
|
|
248
|
-
|
|
249
|
-
if (!confirm) {
|
|
250
|
-
logger.info('已取消');
|
|
251
|
-
process.exit(0);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
152
|
+
// 创建 PR(使用 GitHub CLI)
|
|
153
|
+
logger.step('创建 Pull Request...');
|
|
254
154
|
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
155
|
+
// 准备 PR body
|
|
156
|
+
const prBody = prContent.description;
|
|
157
|
+
|
|
158
|
+
// 构建 gh pr create 命令
|
|
159
|
+
let ghCommand = `gh pr create --base ${baseBranch} --head ${headBranch} --title "${prContent.title.replace(/"/g, '\\"')}"`;
|
|
160
|
+
|
|
161
|
+
// 将 body 写入临时文件
|
|
162
|
+
const { writeFileSync, unlinkSync } = await import('fs');
|
|
163
|
+
const tmpFile = '/tmp/pr-body-temp.md';
|
|
164
|
+
writeFileSync(tmpFile, prBody, 'utf-8');
|
|
165
|
+
ghCommand += ` --body-file ${tmpFile}`;
|
|
166
|
+
|
|
167
|
+
if (config.draft) {
|
|
168
|
+
ghCommand += ' --draft';
|
|
261
169
|
}
|
|
262
170
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
message: '選擇 Labels:',
|
|
275
|
-
choices: suggestedLabels.map(label => ({
|
|
276
|
-
name: label,
|
|
277
|
-
value: label,
|
|
278
|
-
checked: true,
|
|
279
|
-
})),
|
|
280
|
-
},
|
|
281
|
-
]);
|
|
282
|
-
labels = selectedLabels;
|
|
171
|
+
try {
|
|
172
|
+
const result = execSync(ghCommand, { encoding: 'utf-8' });
|
|
173
|
+
unlinkSync(tmpFile);
|
|
174
|
+
|
|
175
|
+
logger.success('PR 创建成功!\n');
|
|
176
|
+
console.log(result);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
try {
|
|
179
|
+
unlinkSync(tmpFile);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// 忽略
|
|
283
182
|
}
|
|
183
|
+
throw new Error(`创建 PR 失败: ${error.message}`);
|
|
284
184
|
}
|
|
285
|
-
|
|
286
|
-
// 創建 PR
|
|
287
|
-
logger.startSpinner('正在創建 PR...');
|
|
288
|
-
|
|
289
|
-
GitHubAPI.createPR({
|
|
290
|
-
title: prContent.title,
|
|
291
|
-
body: prContent.description,
|
|
292
|
-
base: baseBranch,
|
|
293
|
-
head: headBranch,
|
|
294
|
-
draft: config.draft,
|
|
295
|
-
reviewers,
|
|
296
|
-
labels,
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
logger.succeedSpinner('PR 創建成功!');
|
|
300
|
-
|
|
301
185
|
} catch (error) {
|
|
302
186
|
handleError(error);
|
|
303
187
|
process.exit(1);
|
package/src/core/ai-client.js
CHANGED
|
@@ -1,115 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AI
|
|
3
|
-
*
|
|
4
|
-
* 封裝 GitHub Copilot SDK 的 AI 客戶端
|
|
2
|
+
* AI 客户端
|
|
3
|
+
* 基于 @github/copilot-sdk
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { CopilotClient } from '@github/copilot-sdk';
|
|
8
7
|
|
|
9
8
|
export class AIClient {
|
|
10
|
-
constructor(config = {}) {
|
|
11
|
-
this.config = config;
|
|
12
|
-
this.client = null;
|
|
13
|
-
this.session = null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 初始化客戶端
|
|
18
|
-
*/
|
|
19
|
-
async initialize() {
|
|
20
|
-
if (!this.client) {
|
|
21
|
-
this.client = new CopilotClient();
|
|
22
|
-
}
|
|
23
|
-
return this.client;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
9
|
/**
|
|
27
|
-
*
|
|
10
|
+
* 发送 prompt 并等待回应(带重试机制)
|
|
28
11
|
*/
|
|
29
|
-
async
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.session = await this.client.createSession({
|
|
33
|
-
model: options.model || this.config.ai?.model || 'gpt-4.1',
|
|
34
|
-
...options,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
return this.session;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 發送請求並等待回應(帶重試機制)
|
|
42
|
-
*/
|
|
43
|
-
async sendAndWait(prompt, options = {}) {
|
|
44
|
-
const maxRetries = options.maxRetries || this.config.ai?.maxRetries || 3;
|
|
12
|
+
static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3) {
|
|
13
|
+
const client = new CopilotClient();
|
|
45
14
|
let lastError = null;
|
|
46
15
|
|
|
47
16
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
48
17
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
18
|
+
const session = await client.createSession({ model });
|
|
19
|
+
const response = await session.sendAndWait({ prompt });
|
|
20
|
+
await client.stop();
|
|
52
21
|
|
|
53
|
-
const
|
|
54
|
-
return
|
|
22
|
+
const content = response?.data?.content || '';
|
|
23
|
+
return content.trim();
|
|
55
24
|
} catch (error) {
|
|
56
25
|
lastError = error;
|
|
57
|
-
|
|
58
|
-
if (this.config.output?.verbose) {
|
|
59
|
-
console.log(`⚠️ 嘗試 ${attempt}/${maxRetries} 失敗: ${error.message}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 如果還有重試機會,重新創建 session
|
|
63
26
|
if (attempt < maxRetries) {
|
|
64
|
-
|
|
27
|
+
console.log(`⚠️ AI 请求失败,重试第 ${attempt}/${maxRetries} 次...`);
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
65
29
|
continue;
|
|
66
30
|
}
|
|
67
31
|
}
|
|
68
32
|
}
|
|
69
33
|
|
|
70
|
-
throw new Error(`AI
|
|
34
|
+
throw new Error(`AI 请求失败: ${lastError?.message || '未知错误'}`);
|
|
71
35
|
}
|
|
72
36
|
|
|
73
37
|
/**
|
|
74
|
-
*
|
|
38
|
+
* 解析 JSON 响应
|
|
75
39
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.client = null;
|
|
80
|
-
this.session = null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 清理回應內容(移除 markdown 程式碼區塊等)
|
|
86
|
-
*/
|
|
87
|
-
static cleanResponse(response) {
|
|
88
|
-
if (!response) return '';
|
|
89
|
-
|
|
90
|
-
let cleaned = response.trim();
|
|
40
|
+
static parseJSON(content) {
|
|
41
|
+
// 移除可能的 markdown code block 标记
|
|
42
|
+
const jsonContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
91
43
|
|
|
92
|
-
// 移除 markdown 程式碼區塊標記
|
|
93
|
-
cleaned = cleaned.replace(/^```[\w]*\n/gm, '');
|
|
94
|
-
cleaned = cleaned.replace(/\n```$/gm, '');
|
|
95
|
-
cleaned = cleaned.replace(/^```$/gm, '');
|
|
96
|
-
|
|
97
|
-
// 移除開頭和結尾的引號
|
|
98
|
-
cleaned = cleaned.replace(/^["']|["']$/g, '');
|
|
99
|
-
|
|
100
|
-
return cleaned.trim();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 解析 JSON 回應
|
|
105
|
-
*/
|
|
106
|
-
static parseJSON(response) {
|
|
107
|
-
const cleaned = AIClient.cleanResponse(response);
|
|
108
|
-
|
|
109
44
|
try {
|
|
110
|
-
return JSON.parse(
|
|
45
|
+
return JSON.parse(jsonContent);
|
|
111
46
|
} catch (error) {
|
|
112
|
-
throw new Error(
|
|
47
|
+
throw new Error(`无法解析 AI 回应为 JSON: ${error.message}`);
|
|
113
48
|
}
|
|
114
49
|
}
|
|
115
50
|
}
|