@dongowu/git-ai-cli 1.0.20 → 1.0.21
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/README.md +158 -153
- package/README_EN.md +165 -141
- package/dist/cli_main.js +66 -0
- package/dist/cli_main.js.map +1 -1
- package/dist/commands/branch.d.ts +11 -0
- package/dist/commands/branch.d.ts.map +1 -0
- package/dist/commands/branch.js +279 -0
- package/dist/commands/branch.js.map +1 -0
- package/dist/commands/commit.d.ts.map +1 -1
- package/dist/commands/commit.js +26 -1
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/config_manage.d.ts.map +1 -1
- package/dist/commands/config_manage.js +212 -3
- package/dist/commands/config_manage.js.map +1 -1
- package/dist/commands/hook.d.ts.map +1 -1
- package/dist/commands/hook.js +76 -10
- package/dist/commands/hook.js.map +1 -1
- package/dist/commands/msg.d.ts.map +1 -1
- package/dist/commands/msg.js +12 -1
- package/dist/commands/msg.js.map +1 -1
- package/dist/commands/pr.d.ts +8 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +96 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/release.d.ts +8 -0
- package/dist/commands/release.d.ts.map +1 -0
- package/dist/commands/release.js +95 -0
- package/dist/commands/release.js.map +1 -0
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +9 -1
- package/dist/commands/report.js.map +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/ai.d.ts +22 -0
- package/dist/utils/ai.d.ts.map +1 -1
- package/dist/utils/ai.js +699 -33
- package/dist/utils/ai.js.map +1 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +93 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/git.d.ts +8 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +88 -0
- package/dist/utils/git.js.map +1 -1
- package/package.json +1 -1
- package/release_notes.md +3 -4
package/dist/utils/ai.js
CHANGED
|
@@ -26,6 +26,13 @@ function getMaxOutputTokens(numChoices) {
|
|
|
26
26
|
const base = Number.isFinite(parsed) && parsed > 0 ? parsed : 500;
|
|
27
27
|
return base * Math.max(numChoices, 1);
|
|
28
28
|
}
|
|
29
|
+
function getModelCandidates(config) {
|
|
30
|
+
const primary = (config.model || '').trim();
|
|
31
|
+
const fallbacks = Array.isArray(config.fallbackModels) ? config.fallbackModels : [];
|
|
32
|
+
const cleaned = fallbacks.map((m) => String(m).trim()).filter(Boolean);
|
|
33
|
+
const unique = [primary, ...cleaned.filter((m) => m && m !== primary)];
|
|
34
|
+
return unique.filter(Boolean);
|
|
35
|
+
}
|
|
29
36
|
function redactSecrets(input) {
|
|
30
37
|
if (!input)
|
|
31
38
|
return input;
|
|
@@ -118,6 +125,454 @@ function tryParseMessagesJson(content) {
|
|
|
118
125
|
}
|
|
119
126
|
return null;
|
|
120
127
|
}
|
|
128
|
+
const DEFAULT_COMMIT_RULES = {
|
|
129
|
+
types: ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'build', 'ci'],
|
|
130
|
+
maxSubjectLength: 50,
|
|
131
|
+
requireScope: false,
|
|
132
|
+
issuePattern: /([A-Z]+-\d+|#\d+)/,
|
|
133
|
+
issuePlacement: 'footer',
|
|
134
|
+
issueFooterPrefix: 'Refs',
|
|
135
|
+
requireIssue: false,
|
|
136
|
+
};
|
|
137
|
+
const RULES_PRESETS = {
|
|
138
|
+
conventional: {
|
|
139
|
+
types: ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'build', 'ci'],
|
|
140
|
+
maxSubjectLength: 50,
|
|
141
|
+
requireScope: false,
|
|
142
|
+
},
|
|
143
|
+
angular: {
|
|
144
|
+
types: ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'],
|
|
145
|
+
maxSubjectLength: 50,
|
|
146
|
+
requireScope: true,
|
|
147
|
+
},
|
|
148
|
+
minimal: {
|
|
149
|
+
types: ['feat', 'fix', 'docs', 'refactor', 'perf', 'test', 'chore'],
|
|
150
|
+
maxSubjectLength: 50,
|
|
151
|
+
requireScope: false,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
export function validateCommitRules(raw) {
|
|
155
|
+
const errors = [];
|
|
156
|
+
const warnings = [];
|
|
157
|
+
if (raw === undefined)
|
|
158
|
+
return { errors, warnings };
|
|
159
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
160
|
+
errors.push('rules must be an object');
|
|
161
|
+
return { errors, warnings };
|
|
162
|
+
}
|
|
163
|
+
const rules = raw;
|
|
164
|
+
if (rules.types !== undefined) {
|
|
165
|
+
if (!Array.isArray(rules.types)) {
|
|
166
|
+
errors.push('rules.types must be an array of strings');
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const cleaned = rules.types.filter((t) => typeof t === 'string' && t.trim());
|
|
170
|
+
if (cleaned.length === 0) {
|
|
171
|
+
errors.push('rules.types must not be empty');
|
|
172
|
+
}
|
|
173
|
+
if (cleaned.length !== rules.types.length) {
|
|
174
|
+
warnings.push('rules.types contains non-string entries');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (rules.scopes !== undefined) {
|
|
179
|
+
if (!Array.isArray(rules.scopes)) {
|
|
180
|
+
errors.push('rules.scopes must be an array of strings');
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const cleaned = rules.scopes.filter((s) => typeof s === 'string' && s.trim());
|
|
184
|
+
if (cleaned.length !== rules.scopes.length) {
|
|
185
|
+
warnings.push('rules.scopes contains non-string entries');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (rules.scopeMap !== undefined) {
|
|
190
|
+
if (!rules.scopeMap || typeof rules.scopeMap !== 'object' || Array.isArray(rules.scopeMap)) {
|
|
191
|
+
errors.push('rules.scopeMap must be an object map of path -> scope');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const entries = Object.entries(rules.scopeMap);
|
|
195
|
+
const valid = entries.filter(([k, v]) => typeof k === 'string' && k.trim() && typeof v === 'string' && v.trim());
|
|
196
|
+
if (valid.length !== entries.length) {
|
|
197
|
+
warnings.push('rules.scopeMap contains invalid entries');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (rules.maxSubjectLength !== undefined) {
|
|
202
|
+
if (typeof rules.maxSubjectLength !== 'number' || !Number.isFinite(rules.maxSubjectLength)) {
|
|
203
|
+
errors.push('rules.maxSubjectLength must be a number');
|
|
204
|
+
}
|
|
205
|
+
else if (rules.maxSubjectLength <= 10) {
|
|
206
|
+
warnings.push('rules.maxSubjectLength should be > 10');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (rules.requireScope !== undefined && typeof rules.requireScope !== 'boolean') {
|
|
210
|
+
errors.push('rules.requireScope must be a boolean');
|
|
211
|
+
}
|
|
212
|
+
if (rules.issuePattern !== undefined) {
|
|
213
|
+
if (typeof rules.issuePattern !== 'string' || !rules.issuePattern.trim()) {
|
|
214
|
+
errors.push('rules.issuePattern must be a non-empty string');
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
try {
|
|
218
|
+
// eslint-disable-next-line no-new
|
|
219
|
+
new RegExp(rules.issuePattern);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
errors.push('rules.issuePattern must be a valid regex string');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (rules.issuePlacement !== undefined) {
|
|
227
|
+
if (rules.issuePlacement !== 'scope' &&
|
|
228
|
+
rules.issuePlacement !== 'subject' &&
|
|
229
|
+
rules.issuePlacement !== 'footer') {
|
|
230
|
+
errors.push('rules.issuePlacement must be scope, subject, or footer');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (rules.issueFooterPrefix !== undefined && typeof rules.issueFooterPrefix !== 'string') {
|
|
234
|
+
errors.push('rules.issueFooterPrefix must be a string');
|
|
235
|
+
}
|
|
236
|
+
if (rules.requireIssue !== undefined && typeof rules.requireIssue !== 'boolean') {
|
|
237
|
+
errors.push('rules.requireIssue must be a boolean');
|
|
238
|
+
}
|
|
239
|
+
return { errors, warnings };
|
|
240
|
+
}
|
|
241
|
+
function normalizeRules(config) {
|
|
242
|
+
const presetName = (config.rulesPreset || '').trim().toLowerCase();
|
|
243
|
+
const preset = presetName ? RULES_PRESETS[presetName] : undefined;
|
|
244
|
+
const custom = config.rules;
|
|
245
|
+
const resolvedTypes = Array.isArray(custom?.types) && custom.types.length > 0
|
|
246
|
+
? custom.types.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim())
|
|
247
|
+
: Array.isArray(preset?.types) && preset?.types?.length
|
|
248
|
+
? preset.types
|
|
249
|
+
: DEFAULT_COMMIT_RULES.types;
|
|
250
|
+
const types = resolvedTypes.length ? resolvedTypes : DEFAULT_COMMIT_RULES.types;
|
|
251
|
+
const scopes = Array.isArray(custom?.scopes) && custom.scopes.length > 0
|
|
252
|
+
? custom.scopes.filter((s) => typeof s === 'string' && s.trim()).map((s) => s.trim())
|
|
253
|
+
: Array.isArray(preset?.scopes) && preset?.scopes?.length
|
|
254
|
+
? preset.scopes
|
|
255
|
+
: undefined;
|
|
256
|
+
const presetScopeMap = preset?.scopeMap && typeof preset.scopeMap === 'object'
|
|
257
|
+
? Object.fromEntries(Object.entries(preset.scopeMap).filter(([k, v]) => typeof k === 'string' && typeof v === 'string' && k.trim() && v.trim()))
|
|
258
|
+
: undefined;
|
|
259
|
+
const customScopeMap = custom?.scopeMap && typeof custom.scopeMap === 'object'
|
|
260
|
+
? Object.fromEntries(Object.entries(custom.scopeMap).filter(([k, v]) => typeof k === 'string' && typeof v === 'string' && k.trim() && v.trim()))
|
|
261
|
+
: undefined;
|
|
262
|
+
const scopeMap = presetScopeMap || customScopeMap ? { ...(presetScopeMap || {}), ...(customScopeMap || {}) } : undefined;
|
|
263
|
+
const maxSubjectLength = typeof custom?.maxSubjectLength === 'number' && custom.maxSubjectLength > 10
|
|
264
|
+
? Math.floor(custom.maxSubjectLength)
|
|
265
|
+
: typeof preset?.maxSubjectLength === 'number' && preset.maxSubjectLength > 10
|
|
266
|
+
? Math.floor(preset.maxSubjectLength)
|
|
267
|
+
: DEFAULT_COMMIT_RULES.maxSubjectLength;
|
|
268
|
+
const requireScope = typeof custom?.requireScope === 'boolean'
|
|
269
|
+
? custom.requireScope
|
|
270
|
+
: typeof preset?.requireScope === 'boolean'
|
|
271
|
+
? preset.requireScope
|
|
272
|
+
: DEFAULT_COMMIT_RULES.requireScope;
|
|
273
|
+
const issuePatternRaw = typeof custom?.issuePattern === 'string' && custom.issuePattern.trim()
|
|
274
|
+
? custom.issuePattern.trim()
|
|
275
|
+
: typeof preset?.issuePattern === 'string' && preset.issuePattern.trim()
|
|
276
|
+
? preset.issuePattern.trim()
|
|
277
|
+
: undefined;
|
|
278
|
+
let issuePattern = DEFAULT_COMMIT_RULES.issuePattern;
|
|
279
|
+
if (issuePatternRaw) {
|
|
280
|
+
try {
|
|
281
|
+
issuePattern = new RegExp(issuePatternRaw);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
issuePattern = DEFAULT_COMMIT_RULES.issuePattern;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const issuePlacement = custom?.issuePlacement === 'scope' ||
|
|
288
|
+
custom?.issuePlacement === 'subject' ||
|
|
289
|
+
custom?.issuePlacement === 'footer'
|
|
290
|
+
? custom.issuePlacement
|
|
291
|
+
: preset?.issuePlacement === 'scope' ||
|
|
292
|
+
preset?.issuePlacement === 'subject' ||
|
|
293
|
+
preset?.issuePlacement === 'footer'
|
|
294
|
+
? preset.issuePlacement
|
|
295
|
+
: DEFAULT_COMMIT_RULES.issuePlacement;
|
|
296
|
+
const issueFooterPrefix = typeof custom?.issueFooterPrefix === 'string' && custom.issueFooterPrefix.trim()
|
|
297
|
+
? custom.issueFooterPrefix.trim()
|
|
298
|
+
: typeof preset?.issueFooterPrefix === 'string' && preset.issueFooterPrefix.trim()
|
|
299
|
+
? preset.issueFooterPrefix.trim()
|
|
300
|
+
: DEFAULT_COMMIT_RULES.issueFooterPrefix;
|
|
301
|
+
const requireIssue = typeof custom?.requireIssue === 'boolean'
|
|
302
|
+
? custom.requireIssue
|
|
303
|
+
: typeof preset?.requireIssue === 'boolean'
|
|
304
|
+
? preset.requireIssue
|
|
305
|
+
: DEFAULT_COMMIT_RULES.requireIssue;
|
|
306
|
+
return {
|
|
307
|
+
types,
|
|
308
|
+
scopes,
|
|
309
|
+
scopeMap,
|
|
310
|
+
maxSubjectLength,
|
|
311
|
+
requireScope,
|
|
312
|
+
issuePattern,
|
|
313
|
+
issuePlacement,
|
|
314
|
+
issueFooterPrefix,
|
|
315
|
+
requireIssue,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function normalizePath(path) {
|
|
319
|
+
return path.replace(/\\/g, '/');
|
|
320
|
+
}
|
|
321
|
+
function inferTypeFromBranch(branchName) {
|
|
322
|
+
const branch = branchName || '';
|
|
323
|
+
if (/^feature\//.test(branch))
|
|
324
|
+
return 'feat';
|
|
325
|
+
if (/^bugfix\//.test(branch))
|
|
326
|
+
return 'fix';
|
|
327
|
+
if (/^hotfix\//.test(branch))
|
|
328
|
+
return 'fix';
|
|
329
|
+
if (/^release\//.test(branch))
|
|
330
|
+
return 'chore';
|
|
331
|
+
if (/^docs\//.test(branch))
|
|
332
|
+
return 'docs';
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
function inferScopeFromBranch(branchName) {
|
|
336
|
+
if (!branchName)
|
|
337
|
+
return null;
|
|
338
|
+
const patterns = [
|
|
339
|
+
/^(?:feature|bugfix|hotfix|release|dev)\/([a-zA-Z0-9_-]+)/,
|
|
340
|
+
/^(?:feat|fix|chore|docs)\/([a-zA-Z0-9_-]+)/,
|
|
341
|
+
/^[A-Z]+-\d+/,
|
|
342
|
+
];
|
|
343
|
+
for (const pattern of patterns) {
|
|
344
|
+
const match = branchName.match(pattern);
|
|
345
|
+
if (match) {
|
|
346
|
+
return match[1].toLowerCase().replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
function inferScopeFromPaths(stagedFiles, scopeMap) {
|
|
352
|
+
if (!stagedFiles || stagedFiles.length === 0)
|
|
353
|
+
return null;
|
|
354
|
+
if (scopeMap) {
|
|
355
|
+
const normalizedMap = Object.entries(scopeMap)
|
|
356
|
+
.map(([prefix, scope]) => [normalizePath(prefix), scope])
|
|
357
|
+
.sort((a, b) => b[0].length - a[0].length);
|
|
358
|
+
for (const file of stagedFiles) {
|
|
359
|
+
const normalized = normalizePath(file);
|
|
360
|
+
for (const [prefix, scope] of normalizedMap) {
|
|
361
|
+
if (!prefix)
|
|
362
|
+
continue;
|
|
363
|
+
const anchor = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
364
|
+
if (normalized.startsWith(prefix) || normalized.includes(anchor)) {
|
|
365
|
+
return scope;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Monorepo auto-scope (packages/apps/services/libs)
|
|
371
|
+
for (const file of stagedFiles) {
|
|
372
|
+
const normalized = normalizePath(file);
|
|
373
|
+
const match = normalized.match(/^(?:packages|apps|services|libs)\/([^/]+)/);
|
|
374
|
+
if (match) {
|
|
375
|
+
return match[1];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
function pickScope(inputScope, rules, branchName, stagedFiles) {
|
|
381
|
+
let scope = inputScope?.trim();
|
|
382
|
+
if (!scope) {
|
|
383
|
+
scope = inferScopeFromBranch(branchName) || inferScopeFromPaths(stagedFiles, rules.scopeMap) || undefined;
|
|
384
|
+
}
|
|
385
|
+
if (scope && rules.scopes && !rules.scopes.includes(scope)) {
|
|
386
|
+
scope = undefined;
|
|
387
|
+
}
|
|
388
|
+
if (!scope && rules.requireScope) {
|
|
389
|
+
scope = inferScopeFromBranch(branchName) || inferScopeFromPaths(stagedFiles, rules.scopeMap) || 'core';
|
|
390
|
+
}
|
|
391
|
+
return scope || null;
|
|
392
|
+
}
|
|
393
|
+
function normalizeType(inputType, rules, branchName) {
|
|
394
|
+
const trimmed = inputType?.trim();
|
|
395
|
+
if (trimmed && rules.types.includes(trimmed))
|
|
396
|
+
return trimmed;
|
|
397
|
+
const inferred = inferTypeFromBranch(branchName);
|
|
398
|
+
if (inferred && rules.types.includes(inferred))
|
|
399
|
+
return inferred;
|
|
400
|
+
return rules.types[0] || 'chore';
|
|
401
|
+
}
|
|
402
|
+
function cleanSubject(subject, rules, locale) {
|
|
403
|
+
let out = (subject || '').trim();
|
|
404
|
+
out = out.replace(/[。.]$/, '').trim();
|
|
405
|
+
if (!out) {
|
|
406
|
+
out = locale === 'zh' ? '更新代码' : 'update code';
|
|
407
|
+
}
|
|
408
|
+
if (out.length > rules.maxSubjectLength) {
|
|
409
|
+
out = out.slice(0, rules.maxSubjectLength).trim();
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
function findIssue(text, pattern) {
|
|
414
|
+
if (!text)
|
|
415
|
+
return null;
|
|
416
|
+
const match = text.match(pattern);
|
|
417
|
+
return match ? match[0] : null;
|
|
418
|
+
}
|
|
419
|
+
function resolveIssueId(message, branchName, rules) {
|
|
420
|
+
const fromMessage = findIssue(message, rules.issuePattern);
|
|
421
|
+
if (fromMessage)
|
|
422
|
+
return { issue: fromMessage, fromMessage: true };
|
|
423
|
+
const fromBranch = findIssue(branchName, rules.issuePattern);
|
|
424
|
+
if (fromBranch)
|
|
425
|
+
return { issue: fromBranch, fromMessage: false };
|
|
426
|
+
return { issue: null, fromMessage: false };
|
|
427
|
+
}
|
|
428
|
+
function normalizeIssueId(issue, placement) {
|
|
429
|
+
if (placement === 'scope') {
|
|
430
|
+
return issue.replace(/^#/, '');
|
|
431
|
+
}
|
|
432
|
+
return issue;
|
|
433
|
+
}
|
|
434
|
+
function formatCommitMessage(type, scope, subject, locale, body, risks, footer) {
|
|
435
|
+
const head = `${type}${scope ? `(${scope})` : ''}: ${subject}`;
|
|
436
|
+
const bodyParts = [];
|
|
437
|
+
if (body && body.trim())
|
|
438
|
+
bodyParts.push(body.trim());
|
|
439
|
+
if (risks && risks.trim()) {
|
|
440
|
+
bodyParts.push(`${locale === 'zh' ? '风险' : 'Risks'}: ${risks.trim()}`);
|
|
441
|
+
}
|
|
442
|
+
if (footer && footer.trim()) {
|
|
443
|
+
bodyParts.push(footer.trim());
|
|
444
|
+
}
|
|
445
|
+
if (bodyParts.length === 0)
|
|
446
|
+
return head;
|
|
447
|
+
return `${head}\n\n${bodyParts.join('\n\n')}`;
|
|
448
|
+
}
|
|
449
|
+
function enforceRulesOnTextMessage(message, rules, locale, branchName, stagedFiles) {
|
|
450
|
+
const lines = message.split('\n');
|
|
451
|
+
const head = lines[0]?.trim() || '';
|
|
452
|
+
const body = lines.slice(1).join('\n').trim() || undefined;
|
|
453
|
+
const match = head.match(/^(\w+)(?:\(([^)]+)\))?:\s*(.+)$/);
|
|
454
|
+
const type = normalizeType(match?.[1], rules, branchName);
|
|
455
|
+
let scope = pickScope(match?.[2], rules, branchName, stagedFiles);
|
|
456
|
+
let subject = cleanSubject(match?.[3] || head, rules, locale);
|
|
457
|
+
const issueInfo = resolveIssueId(message, branchName, rules);
|
|
458
|
+
let footer;
|
|
459
|
+
if (issueInfo.issue && !issueInfo.fromMessage) {
|
|
460
|
+
const issue = normalizeIssueId(issueInfo.issue, rules.issuePlacement);
|
|
461
|
+
if (rules.issuePlacement === 'scope') {
|
|
462
|
+
if (!scope || scope === 'core') {
|
|
463
|
+
if (!rules.scopes || rules.scopes.includes(issue)) {
|
|
464
|
+
scope = issue;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
footer = `${rules.issueFooterPrefix}: ${issueInfo.issue}`;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
footer = `${rules.issueFooterPrefix}: ${issueInfo.issue}`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else if (rules.issuePlacement === 'subject') {
|
|
475
|
+
subject = cleanSubject(`${issue} ${subject}`, rules, locale);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
footer = `${rules.issueFooterPrefix}: ${issueInfo.issue}`;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return formatCommitMessage(type, scope, subject, locale, body, undefined, footer);
|
|
482
|
+
}
|
|
483
|
+
export function validateCommitMessage(message, config, context) {
|
|
484
|
+
const errors = [];
|
|
485
|
+
const warnings = [];
|
|
486
|
+
const rules = normalizeRules(config);
|
|
487
|
+
const lines = message.split('\n');
|
|
488
|
+
const head = lines[0]?.trim() || '';
|
|
489
|
+
const match = head.match(/^(\w+)(?:\(([^)]+)\))?:\s*(.+)$/);
|
|
490
|
+
if (!match) {
|
|
491
|
+
errors.push('Commit message must match "<type>(<scope>): <subject>"');
|
|
492
|
+
return { errors, warnings };
|
|
493
|
+
}
|
|
494
|
+
const type = match[1];
|
|
495
|
+
const scope = match[2];
|
|
496
|
+
const subject = match[3]?.trim() || '';
|
|
497
|
+
if (!rules.types.includes(type)) {
|
|
498
|
+
errors.push(`type "${type}" is not allowed`);
|
|
499
|
+
}
|
|
500
|
+
if (rules.requireScope && (!scope || !scope.trim())) {
|
|
501
|
+
errors.push('scope is required');
|
|
502
|
+
}
|
|
503
|
+
if (scope && rules.scopes && !rules.scopes.includes(scope)) {
|
|
504
|
+
errors.push(`scope "${scope}" is not in allowed scopes`);
|
|
505
|
+
}
|
|
506
|
+
if (!subject) {
|
|
507
|
+
errors.push('subject must not be empty');
|
|
508
|
+
}
|
|
509
|
+
if (subject.length > rules.maxSubjectLength) {
|
|
510
|
+
errors.push(`subject exceeds max length ${rules.maxSubjectLength}`);
|
|
511
|
+
}
|
|
512
|
+
if (rules.requireIssue) {
|
|
513
|
+
const issue = findIssue(message, rules.issuePattern) ||
|
|
514
|
+
findIssue(context?.branchName, rules.issuePattern);
|
|
515
|
+
if (!issue) {
|
|
516
|
+
errors.push('issue id is required but not found');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { errors, warnings };
|
|
520
|
+
}
|
|
521
|
+
function parseCommitJson(normalized) {
|
|
522
|
+
try {
|
|
523
|
+
const parsed = JSON.parse(normalized);
|
|
524
|
+
if (Array.isArray(parsed)) {
|
|
525
|
+
return parsed
|
|
526
|
+
.map((item) => (typeof item === 'object' && item ? item : null))
|
|
527
|
+
.filter(Boolean);
|
|
528
|
+
}
|
|
529
|
+
if (parsed && typeof parsed === 'object') {
|
|
530
|
+
return [parsed];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// ignore
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
function formatFromCommitJson(items, rules, locale, branchName, stagedFiles) {
|
|
539
|
+
return items.map((item) => {
|
|
540
|
+
if (item.message && typeof item.message === 'string') {
|
|
541
|
+
return enforceRulesOnTextMessage(item.message, rules, locale, branchName, stagedFiles);
|
|
542
|
+
}
|
|
543
|
+
const type = normalizeType(item.type, rules, branchName);
|
|
544
|
+
let scope = pickScope(item.scope, rules, branchName, stagedFiles);
|
|
545
|
+
let subject = cleanSubject(item.subject, rules, locale);
|
|
546
|
+
const raw = [item.type, item.scope, item.subject, item.body, item.risks]
|
|
547
|
+
.filter((v) => typeof v === 'string' && v.trim())
|
|
548
|
+
.join('\n');
|
|
549
|
+
const issueInfo = resolveIssueId(raw, branchName, rules);
|
|
550
|
+
let footer;
|
|
551
|
+
if (issueInfo.issue && !issueInfo.fromMessage) {
|
|
552
|
+
const issue = normalizeIssueId(issueInfo.issue, rules.issuePlacement);
|
|
553
|
+
if (rules.issuePlacement === 'scope') {
|
|
554
|
+
if (!scope || scope === 'core') {
|
|
555
|
+
if (!rules.scopes || rules.scopes.includes(issue)) {
|
|
556
|
+
scope = issue;
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
footer = `${rules.issueFooterPrefix}: ${issueInfo.issue}`;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
footer = `${rules.issueFooterPrefix}: ${issueInfo.issue}`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else if (rules.issuePlacement === 'subject') {
|
|
567
|
+
subject = cleanSubject(`${issue} ${subject}`, rules, locale);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
footer = `${rules.issueFooterPrefix}: ${issueInfo.issue}`;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return formatCommitMessage(type, scope, subject, locale, item.body, item.risks, footer);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
121
576
|
function normalizeAIError(error) {
|
|
122
577
|
if (error instanceof Error) {
|
|
123
578
|
const safe = redactSecrets(error.message || '');
|
|
@@ -151,6 +606,28 @@ function shouldFallbackFromAgent(error) {
|
|
|
151
606
|
// Default: keep previous behavior (fallback), unless it's clearly an auth/endpoint issue.
|
|
152
607
|
return true;
|
|
153
608
|
}
|
|
609
|
+
function shouldFallbackModel(error) {
|
|
610
|
+
const err = error;
|
|
611
|
+
const status = typeof err?.status === 'number' ? err.status : undefined;
|
|
612
|
+
const type = typeof err?.type === 'string' ? err.type : '';
|
|
613
|
+
if (status === 401 || status === 403 || type === 'authentication_error')
|
|
614
|
+
return false;
|
|
615
|
+
if (status === 404)
|
|
616
|
+
return false;
|
|
617
|
+
if (status === 429)
|
|
618
|
+
return true;
|
|
619
|
+
if (status && status >= 500)
|
|
620
|
+
return true;
|
|
621
|
+
const msg = (typeof err?.error?.message === 'string' && err.error.message) ||
|
|
622
|
+
(typeof err?.message === 'string' && err.message) ||
|
|
623
|
+
'';
|
|
624
|
+
const lowered = String(msg).toLowerCase();
|
|
625
|
+
if (lowered.includes('timeout') || lowered.includes('timed out'))
|
|
626
|
+
return true;
|
|
627
|
+
if (lowered.includes('rate limit'))
|
|
628
|
+
return true;
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
154
631
|
const DEFAULT_SYSTEM_PROMPT_EN = `You are an expert at writing Git commit messages following the Conventional Commits specification.
|
|
155
632
|
|
|
156
633
|
Based on the git diff provided, generate a concise and descriptive commit message.
|
|
@@ -225,6 +702,8 @@ export async function generateCommitMessage(client, input, config, numChoices =
|
|
|
225
702
|
let diff = input.diff;
|
|
226
703
|
let ignoredFiles = input.ignoredFiles;
|
|
227
704
|
let truncated = input.truncated;
|
|
705
|
+
const rules = normalizeRules(config);
|
|
706
|
+
const outputFormat = config.outputFormat || 'text';
|
|
228
707
|
const ensureDiff = async () => {
|
|
229
708
|
if (diff !== undefined)
|
|
230
709
|
return;
|
|
@@ -267,7 +746,9 @@ export async function generateCommitMessage(client, input, config, numChoices =
|
|
|
267
746
|
const agentMessage = agentStrategy === 'tools'
|
|
268
747
|
? await runAgentLoop(client, config, stats, input.branchName, input.quiet, agentModel)
|
|
269
748
|
: await runAgentLite(client, config, stats, input.branchName, input.quiet, agentModel);
|
|
270
|
-
|
|
749
|
+
const enforced = enforceRulesOnTextMessage(agentMessage, rules, config.locale, input.branchName, input.stagedFiles);
|
|
750
|
+
const out = config.enableFooter ? `${enforced}\n\n🤖 Generated by git-ai 🚀` : enforced;
|
|
751
|
+
return [out];
|
|
271
752
|
}
|
|
272
753
|
}
|
|
273
754
|
catch (error) {
|
|
@@ -297,13 +778,38 @@ export async function generateCommitMessage(client, input, config, numChoices =
|
|
|
297
778
|
}
|
|
298
779
|
const isZh = config.locale === 'zh';
|
|
299
780
|
const lines = [];
|
|
781
|
+
const rulesHeader = isZh ? 'Commit 规则:' : 'Commit rules:';
|
|
782
|
+
const rulesLines = [
|
|
783
|
+
`${rulesHeader}`,
|
|
784
|
+
`types: ${rules.types.join(', ')}`,
|
|
785
|
+
`maxSubjectLength: ${rules.maxSubjectLength}`,
|
|
786
|
+
`requireScope: ${rules.requireScope ? 'true' : 'false'}`,
|
|
787
|
+
`issuePlacement: ${rules.issuePlacement}`,
|
|
788
|
+
`requireIssue: ${rules.requireIssue ? 'true' : 'false'}`,
|
|
789
|
+
`issuePattern: ${rules.issuePattern.source}`,
|
|
790
|
+
`issueFooterPrefix: ${rules.issueFooterPrefix}`,
|
|
791
|
+
];
|
|
792
|
+
if (rules.scopes && rules.scopes.length) {
|
|
793
|
+
rulesLines.push(`scopes: ${rules.scopes.join(', ')}`);
|
|
794
|
+
}
|
|
795
|
+
lines.push(rulesLines.join('\n'));
|
|
300
796
|
if (numChoices > 1) {
|
|
301
797
|
// Add instruction for multiple choices (JSON array for robustness)
|
|
302
|
-
const multiInstruction =
|
|
303
|
-
?
|
|
304
|
-
|
|
798
|
+
const multiInstruction = outputFormat === 'json'
|
|
799
|
+
? isZh
|
|
800
|
+
? `\n请仅输出 JSON 数组,包含 ${numChoices} 个对象,每个对象字段为 type, scope, subject, body?, risks?;不要输出其他内容。`
|
|
801
|
+
: `\nRespond with a JSON array of ${numChoices} objects with fields type, scope, subject, body?, risks?. Output JSON only.`
|
|
802
|
+
: isZh
|
|
803
|
+
? `\n请仅输出 JSON 数组,包含 ${numChoices} 条不同的 commit message(字符串数组),不要输出其他内容。`
|
|
804
|
+
: `\nRespond with a JSON array of ${numChoices} distinct commit message strings. Output JSON only.`;
|
|
305
805
|
systemPrompt += multiInstruction;
|
|
306
806
|
}
|
|
807
|
+
else if (outputFormat === 'json') {
|
|
808
|
+
const singleInstruction = isZh
|
|
809
|
+
? `\n请仅输出一个 JSON 对象,字段为 type, scope, subject, body?, risks?;不要输出其他内容。`
|
|
810
|
+
: `\nRespond with a single JSON object with fields type, scope, subject, body?, risks?. Output JSON only.`;
|
|
811
|
+
systemPrompt += singleInstruction;
|
|
812
|
+
}
|
|
307
813
|
if (input.recentCommits?.length) {
|
|
308
814
|
const header = isZh
|
|
309
815
|
? '参考历史提交风格 (请模仿以下风格):'
|
|
@@ -342,41 +848,75 @@ export async function generateCommitMessage(client, input, config, numChoices =
|
|
|
342
848
|
}
|
|
343
849
|
const diffHeader = isZh ? 'Git Diff:' : 'Git diff:';
|
|
344
850
|
lines.push(`${diffHeader}\n\n${diff || '(empty)'}`);
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
851
|
+
const modelCandidates = getModelCandidates(config);
|
|
852
|
+
let response = null;
|
|
853
|
+
let lastError = null;
|
|
854
|
+
for (const model of modelCandidates) {
|
|
855
|
+
try {
|
|
856
|
+
response = await client.chat.completions.create({
|
|
857
|
+
model,
|
|
858
|
+
messages: [
|
|
859
|
+
{ role: 'system', content: systemPrompt },
|
|
860
|
+
{ role: 'user', content: lines.join('\n\n') },
|
|
861
|
+
],
|
|
862
|
+
temperature: 0.7,
|
|
863
|
+
max_tokens: getMaxOutputTokens(numChoices),
|
|
864
|
+
});
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
lastError = error;
|
|
869
|
+
if (!shouldFallbackModel(error) || model === modelCandidates[modelCandidates.length - 1]) {
|
|
870
|
+
throw normalizeAIError(error);
|
|
871
|
+
}
|
|
872
|
+
if (process.env.GIT_AI_DEBUG === '1') {
|
|
873
|
+
console.error(chalk.yellow(`⚠️ Model ${model} failed, trying fallback...`));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (!response) {
|
|
878
|
+
throw normalizeAIError(lastError);
|
|
879
|
+
}
|
|
354
880
|
const content = response.choices[0]?.message?.content?.trim();
|
|
355
881
|
if (!content) {
|
|
356
882
|
throw new Error('Failed to generate commit message: empty response');
|
|
357
883
|
}
|
|
358
884
|
const normalized = stripCodeFences(content);
|
|
359
885
|
let messages = [];
|
|
360
|
-
if (
|
|
361
|
-
const parsed =
|
|
886
|
+
if (outputFormat === 'json') {
|
|
887
|
+
const parsed = parseCommitJson(normalized);
|
|
362
888
|
if (parsed && parsed.length) {
|
|
363
|
-
messages = parsed;
|
|
889
|
+
messages = formatFromCommitJson(parsed, rules, config.locale, input.branchName, input.stagedFiles);
|
|
364
890
|
}
|
|
365
891
|
else {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
.filter(Boolean);
|
|
892
|
+
const fallback = tryParseMessagesJson(normalized);
|
|
893
|
+
const rawMessages = fallback && fallback.length ? fallback : [normalized];
|
|
894
|
+
messages = rawMessages.map((msg) => enforceRulesOnTextMessage(msg, rules, config.locale, input.branchName, input.stagedFiles));
|
|
370
895
|
}
|
|
371
896
|
}
|
|
372
897
|
else {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
898
|
+
if (numChoices > 1) {
|
|
899
|
+
const parsed = tryParseMessagesJson(normalized);
|
|
900
|
+
if (parsed && parsed.length) {
|
|
901
|
+
messages = parsed;
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
messages = normalized
|
|
905
|
+
.split('---')
|
|
906
|
+
.map((msg) => msg.trim())
|
|
907
|
+
.filter(Boolean);
|
|
908
|
+
}
|
|
376
909
|
}
|
|
377
910
|
else {
|
|
378
|
-
|
|
911
|
+
const parsed = tryParseMessagesJson(normalized);
|
|
912
|
+
if (parsed && parsed.length === 1) {
|
|
913
|
+
messages = parsed;
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
messages = [normalized];
|
|
917
|
+
}
|
|
379
918
|
}
|
|
919
|
+
messages = messages.map((msg) => enforceRulesOnTextMessage(msg, rules, config.locale, input.branchName, input.stagedFiles));
|
|
380
920
|
}
|
|
381
921
|
if (config.enableFooter) {
|
|
382
922
|
return messages.map((msg) => `${msg}\n\n🤖 Generated by git-ai 🚀`);
|
|
@@ -418,20 +958,146 @@ Rules:
|
|
|
418
958
|
4. **Filter Noise**: Ignore trivial or "wip" commits.
|
|
419
959
|
|
|
420
960
|
Output structured markdown text only.`;
|
|
961
|
+
const PR_PROMPT_EN = `You are a senior engineer writing a pull request description.
|
|
962
|
+
|
|
963
|
+
Based on the commit list and diff summary, produce a concise PR description.
|
|
964
|
+
|
|
965
|
+
Rules:
|
|
966
|
+
1) Use Markdown
|
|
967
|
+
2) Include sections: Summary, Changes, Testing, Risks
|
|
968
|
+
3) Be concise and value-focused
|
|
969
|
+
4) If testing info is unknown, write "Not run"
|
|
970
|
+
|
|
971
|
+
Output Markdown only.`;
|
|
972
|
+
const PR_PROMPT_ZH = `你是资深工程师,负责撰写 PR 描述。
|
|
973
|
+
|
|
974
|
+
根据提交记录和 diff 概要,生成简洁的 PR 描述。
|
|
975
|
+
|
|
976
|
+
规则:
|
|
977
|
+
1) 使用 Markdown
|
|
978
|
+
2) 包含板块:Summary, Changes, Testing, Risks
|
|
979
|
+
3) 聚焦价值与影响,避免啰嗦
|
|
980
|
+
4) 如未知测试情况,写 "Not run"
|
|
981
|
+
|
|
982
|
+
仅输出 Markdown。`;
|
|
983
|
+
const RELEASE_PROMPT_EN = `You are a release manager writing release notes.
|
|
984
|
+
|
|
985
|
+
Based on the commit list, generate structured release notes.
|
|
986
|
+
|
|
987
|
+
Rules:
|
|
988
|
+
1) Use Markdown
|
|
989
|
+
2) Group changes by type (Features, Fixes, Improvements, Docs/Chore)
|
|
990
|
+
3) Be concise and user-facing when possible
|
|
991
|
+
4) Exclude trivial/wip commits
|
|
992
|
+
|
|
993
|
+
Output Markdown only.`;
|
|
994
|
+
const RELEASE_PROMPT_ZH = `你是发布负责人,负责撰写 Release Notes。
|
|
995
|
+
|
|
996
|
+
根据提交记录生成结构化的发布说明。
|
|
997
|
+
|
|
998
|
+
规则:
|
|
999
|
+
1) 使用 Markdown
|
|
1000
|
+
2) 按类型分组(Features / Fixes / Improvements / Docs/Chore)
|
|
1001
|
+
3) 尽量面向用户价值表达
|
|
1002
|
+
4) 过滤无意义提交
|
|
1003
|
+
|
|
1004
|
+
仅输出 Markdown。`;
|
|
1005
|
+
export async function generatePullRequestDescription(client, input, config) {
|
|
1006
|
+
const isZh = config.locale === 'zh';
|
|
1007
|
+
const systemPrompt = isZh ? PR_PROMPT_ZH : PR_PROMPT_EN;
|
|
1008
|
+
const diffBlock = input.diffStat ? `\nDiff Summary:\n${input.diffStat}` : '';
|
|
1009
|
+
const modelCandidates = getModelCandidates(config);
|
|
1010
|
+
let result = null;
|
|
1011
|
+
let lastError = null;
|
|
1012
|
+
for (const model of modelCandidates) {
|
|
1013
|
+
try {
|
|
1014
|
+
result = await client.chat.completions.create({
|
|
1015
|
+
model,
|
|
1016
|
+
messages: [
|
|
1017
|
+
{ role: 'system', content: systemPrompt },
|
|
1018
|
+
{
|
|
1019
|
+
role: 'user',
|
|
1020
|
+
content: `Base: ${input.base}\nHead: ${input.head}\nCommits:\n${input.commits.join('\n')}${diffBlock}`,
|
|
1021
|
+
},
|
|
1022
|
+
],
|
|
1023
|
+
temperature: 0.4,
|
|
1024
|
+
});
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
lastError = error;
|
|
1029
|
+
if (!shouldFallbackModel(error) || model === modelCandidates[modelCandidates.length - 1]) {
|
|
1030
|
+
throw normalizeAIError(error);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (!result)
|
|
1035
|
+
throw normalizeAIError(lastError);
|
|
1036
|
+
return result.choices[0]?.message?.content?.trim() || '';
|
|
1037
|
+
}
|
|
1038
|
+
export async function generateReleaseNotes(client, input, config) {
|
|
1039
|
+
const isZh = config.locale === 'zh';
|
|
1040
|
+
const systemPrompt = isZh ? RELEASE_PROMPT_ZH : RELEASE_PROMPT_EN;
|
|
1041
|
+
const modelCandidates = getModelCandidates(config);
|
|
1042
|
+
let result = null;
|
|
1043
|
+
let lastError = null;
|
|
1044
|
+
for (const model of modelCandidates) {
|
|
1045
|
+
try {
|
|
1046
|
+
result = await client.chat.completions.create({
|
|
1047
|
+
model,
|
|
1048
|
+
messages: [
|
|
1049
|
+
{ role: 'system', content: systemPrompt },
|
|
1050
|
+
{
|
|
1051
|
+
role: 'user',
|
|
1052
|
+
content: `Range: ${input.from} → ${input.to}\nCommits:\n${input.commits.join('\n')}`,
|
|
1053
|
+
},
|
|
1054
|
+
],
|
|
1055
|
+
temperature: 0.4,
|
|
1056
|
+
});
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
lastError = error;
|
|
1061
|
+
if (!shouldFallbackModel(error) || model === modelCandidates[modelCandidates.length - 1]) {
|
|
1062
|
+
throw normalizeAIError(error);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (!result)
|
|
1067
|
+
throw normalizeAIError(lastError);
|
|
1068
|
+
return result.choices[0]?.message?.content?.trim() || '';
|
|
1069
|
+
}
|
|
421
1070
|
export async function generateWeeklyReport(client, commits, config) {
|
|
422
1071
|
const isZh = config.locale === 'zh';
|
|
423
1072
|
const systemPrompt = isZh ? REPORT_PROMPT_ZH : REPORT_PROMPT_EN;
|
|
424
1073
|
if (commits.length === 0) {
|
|
425
1074
|
return isZh ? '这段时间没有找到您的提交记录。' : 'No commits found for this period.';
|
|
426
1075
|
}
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1076
|
+
const modelCandidates = getModelCandidates(config);
|
|
1077
|
+
let result = null;
|
|
1078
|
+
let lastError = null;
|
|
1079
|
+
for (const model of modelCandidates) {
|
|
1080
|
+
try {
|
|
1081
|
+
result = await client.chat.completions.create({
|
|
1082
|
+
model,
|
|
1083
|
+
messages: [
|
|
1084
|
+
{ role: 'system', content: systemPrompt },
|
|
1085
|
+
{ role: 'user', content: `Commit History:\n${commits.join('\n')}` },
|
|
1086
|
+
],
|
|
1087
|
+
temperature: 0.7,
|
|
1088
|
+
});
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
lastError = error;
|
|
1093
|
+
if (!shouldFallbackModel(error) || model === modelCandidates[modelCandidates.length - 1]) {
|
|
1094
|
+
throw normalizeAIError(error);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (!result) {
|
|
1099
|
+
throw normalizeAIError(lastError);
|
|
1100
|
+
}
|
|
1101
|
+
return result.choices[0]?.message?.content?.trim() || '';
|
|
436
1102
|
}
|
|
437
1103
|
//# sourceMappingURL=ai.js.map
|