@dongowu/git-ai-cli 1.0.21 → 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.
Files changed (82) hide show
  1. package/README.md +200 -157
  2. package/bin/git-ai.cjs +57 -0
  3. package/install.cjs +50 -0
  4. package/package.json +27 -28
  5. package/.claude/settings.local.json +0 -17
  6. package/CHANGELOG.md +0 -46
  7. package/README_EN.md +0 -264
  8. package/dist/cli.d.ts +0 -3
  9. package/dist/cli.d.ts.map +0 -1
  10. package/dist/cli.js +0 -17
  11. package/dist/cli.js.map +0 -1
  12. package/dist/cli_main.d.ts +0 -2
  13. package/dist/cli_main.d.ts.map +0 -1
  14. package/dist/cli_main.js +0 -255
  15. package/dist/cli_main.js.map +0 -1
  16. package/dist/commands/branch.d.ts +0 -11
  17. package/dist/commands/branch.d.ts.map +0 -1
  18. package/dist/commands/branch.js +0 -279
  19. package/dist/commands/branch.js.map +0 -1
  20. package/dist/commands/commit.d.ts +0 -9
  21. package/dist/commands/commit.d.ts.map +0 -1
  22. package/dist/commands/commit.js +0 -326
  23. package/dist/commands/commit.js.map +0 -1
  24. package/dist/commands/config.d.ts +0 -2
  25. package/dist/commands/config.d.ts.map +0 -1
  26. package/dist/commands/config.js +0 -164
  27. package/dist/commands/config.js.map +0 -1
  28. package/dist/commands/config_manage.d.ts +0 -14
  29. package/dist/commands/config_manage.d.ts.map +0 -1
  30. package/dist/commands/config_manage.js +0 -394
  31. package/dist/commands/config_manage.js.map +0 -1
  32. package/dist/commands/hook.d.ts +0 -5
  33. package/dist/commands/hook.d.ts.map +0 -1
  34. package/dist/commands/hook.js +0 -528
  35. package/dist/commands/hook.js.map +0 -1
  36. package/dist/commands/msg.d.ts +0 -20
  37. package/dist/commands/msg.d.ts.map +0 -1
  38. package/dist/commands/msg.js +0 -148
  39. package/dist/commands/msg.js.map +0 -1
  40. package/dist/commands/pr.d.ts +0 -8
  41. package/dist/commands/pr.d.ts.map +0 -1
  42. package/dist/commands/pr.js +0 -96
  43. package/dist/commands/pr.js.map +0 -1
  44. package/dist/commands/release.d.ts +0 -8
  45. package/dist/commands/release.d.ts.map +0 -1
  46. package/dist/commands/release.js +0 -95
  47. package/dist/commands/release.js.map +0 -1
  48. package/dist/commands/report.d.ts +0 -9
  49. package/dist/commands/report.d.ts.map +0 -1
  50. package/dist/commands/report.js +0 -162
  51. package/dist/commands/report.js.map +0 -1
  52. package/dist/types.d.ts +0 -46
  53. package/dist/types.d.ts.map +0 -1
  54. package/dist/types.js +0 -86
  55. package/dist/types.js.map +0 -1
  56. package/dist/utils/agent.d.ts +0 -5
  57. package/dist/utils/agent.d.ts.map +0 -1
  58. package/dist/utils/agent.js +0 -308
  59. package/dist/utils/agent.js.map +0 -1
  60. package/dist/utils/agent_lite.d.ts +0 -5
  61. package/dist/utils/agent_lite.d.ts.map +0 -1
  62. package/dist/utils/agent_lite.js +0 -263
  63. package/dist/utils/agent_lite.js.map +0 -1
  64. package/dist/utils/ai.d.ts +0 -43
  65. package/dist/utils/ai.d.ts.map +0 -1
  66. package/dist/utils/ai.js +0 -1103
  67. package/dist/utils/ai.js.map +0 -1
  68. package/dist/utils/config.d.ts +0 -11
  69. package/dist/utils/config.d.ts.map +0 -1
  70. package/dist/utils/config.js +0 -239
  71. package/dist/utils/config.js.map +0 -1
  72. package/dist/utils/git.d.ts +0 -42
  73. package/dist/utils/git.d.ts.map +0 -1
  74. package/dist/utils/git.js +0 -456
  75. package/dist/utils/git.js.map +0 -1
  76. package/dist/utils/update.d.ts +0 -4
  77. package/dist/utils/update.d.ts.map +0 -1
  78. package/dist/utils/update.js +0 -122
  79. package/dist/utils/update.js.map +0 -1
  80. package/release_notes.md +0 -9
  81. package/scripts/release.sh +0 -34
  82. package/test_agent_feature.ts +0 -1
package/dist/utils/ai.js DELETED
@@ -1,1103 +0,0 @@
1
- import OpenAI from 'openai';
2
- import { runAgentLoop } from './agent.js';
3
- import { runAgentLite } from './agent_lite.js';
4
- import { getFileStats } from './git.js';
5
- import chalk from 'chalk';
6
- function getTimeoutMs() {
7
- const raw = process.env.GIT_AI_TIMEOUT_MS;
8
- const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
9
- if (Number.isFinite(parsed) && parsed > 0)
10
- return parsed;
11
- return 120_000; // 2 minutes
12
- }
13
- function parseBooleanEnv(value) {
14
- if (!value)
15
- return undefined;
16
- const normalized = value.trim().toLowerCase();
17
- if (['1', 'true', 'yes', 'y', 'on'].includes(normalized))
18
- return true;
19
- if (['0', 'false', 'no', 'n', 'off'].includes(normalized))
20
- return false;
21
- return undefined;
22
- }
23
- function getMaxOutputTokens(numChoices) {
24
- const raw = process.env.GIT_AI_MAX_OUTPUT_TOKENS || process.env.OCO_TOKENS_MAX_OUTPUT;
25
- const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
26
- const base = Number.isFinite(parsed) && parsed > 0 ? parsed : 500;
27
- return base * Math.max(numChoices, 1);
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
- }
36
- function redactSecrets(input) {
37
- if (!input)
38
- return input;
39
- // Generic "api key: xxxx" patterns.
40
- let out = input.replace(/(api[_ -]?key\s*[:=]\s*)([^\s,;]+)/gi, (_m, p1, p2) => {
41
- const s = String(p2);
42
- if (s.length <= 8)
43
- return `${p1}********`;
44
- return `${p1}${s.slice(0, 2)}****${s.slice(-2)}`;
45
- });
46
- // Common OpenAI-style keys: sk-...
47
- out = out.replace(/\bsk-[A-Za-z0-9]{8,}\b/g, (m) => `${m.slice(0, 4)}****${m.slice(-2)}`);
48
- // Avoid leaking long tokens in error messages.
49
- out = out.replace(/\b[A-Za-z0-9_-]{24,}\b/g, (m) => `${m.slice(0, 3)}****${m.slice(-3)}`);
50
- return out;
51
- }
52
- function formatAgentFailureReason(error) {
53
- const err = error;
54
- const status = typeof err?.status === 'number' ? String(err.status) : '';
55
- const name = typeof err?.name === 'string' ? err.name : '';
56
- const code = typeof err?.code === 'string' ? err.code : '';
57
- const type = typeof err?.type === 'string' ? err.type : '';
58
- const rawMsg = (typeof err?.error?.message === 'string' && err.error.message) ||
59
- (typeof err?.message === 'string' && err.message) ||
60
- '';
61
- const compactMsg = redactSecrets(rawMsg).replace(/\s+/g, ' ').trim();
62
- const shortMsg = compactMsg.length > 180 ? compactMsg.slice(0, 180) + '...' : compactMsg;
63
- const parts = [status || name, code, type, shortMsg].filter(Boolean);
64
- return parts.join(' ');
65
- }
66
- function resolveAgentStrategy(_config) {
67
- const raw = (process.env.GIT_AI_AGENT_STRATEGY || '').trim().toLowerCase();
68
- if (raw === 'tools' || raw === 'tool' || raw === 'function' || raw === 'functions')
69
- return 'tools';
70
- if (raw === 'lite' || raw === 'local' || raw === 'fast')
71
- return 'lite';
72
- // Default: lite (fewer API calls, works for providers without tool calling).
73
- // Users can opt-in to tool calling with GIT_AI_AGENT_STRATEGY=tools.
74
- return 'lite';
75
- }
76
- function resolveAgentModel(config, strategy) {
77
- const envModel = process.env.GIT_AI_AGENT_MODEL;
78
- if (envModel && envModel.trim())
79
- return envModel.trim();
80
- const configured = config.agentModel;
81
- if (configured && configured.trim())
82
- return configured.trim();
83
- // DeepSeek reasoner models are often not tool-capable; switch to a tool-capable model only when needed.
84
- if (strategy === 'tools' && config.provider === 'deepseek') {
85
- const base = (config.model || '').trim();
86
- if (base.toLowerCase().includes('reasoner')) {
87
- return 'deepseek-chat';
88
- }
89
- }
90
- return config.model;
91
- }
92
- function isAgentDisabled() {
93
- return parseBooleanEnv(process.env.GIT_AI_DISABLE_AGENT) === true;
94
- }
95
- function isAutoAgentEnabled() {
96
- const raw = parseBooleanEnv(process.env.GIT_AI_AUTO_AGENT);
97
- if (raw === undefined)
98
- return true;
99
- return raw;
100
- }
101
- function stripCodeFences(text) {
102
- return text.replace(/^```[a-z]*\n?/i, '').replace(/```$/i, '').trim();
103
- }
104
- function tryParseMessagesJson(content) {
105
- try {
106
- const parsed = JSON.parse(content);
107
- if (Array.isArray(parsed)) {
108
- const arr = parsed.filter((item) => typeof item === 'string');
109
- return arr.map((s) => s.trim()).filter(Boolean);
110
- }
111
- if (parsed && typeof parsed === 'object') {
112
- const anyObj = parsed;
113
- if (typeof anyObj.message === 'string') {
114
- const msg = anyObj.message.trim();
115
- return msg ? [msg] : [];
116
- }
117
- if (Array.isArray(anyObj.messages)) {
118
- const arr = anyObj.messages.filter((item) => typeof item === 'string');
119
- return arr.map((s) => s.trim()).filter(Boolean);
120
- }
121
- }
122
- }
123
- catch {
124
- // ignore
125
- }
126
- return null;
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
- }
576
- function normalizeAIError(error) {
577
- if (error instanceof Error) {
578
- const safe = redactSecrets(error.message || '');
579
- const e = new Error(safe);
580
- e.cause = error;
581
- return e;
582
- }
583
- return new Error(redactSecrets(String(error)));
584
- }
585
- function shouldFallbackFromAgent(error) {
586
- const err = error;
587
- const status = typeof err?.status === 'number' ? err.status : undefined;
588
- const type = typeof err?.type === 'string' ? err.type : '';
589
- // If auth/endpoint is wrong, basic mode will fail too: don't spam the user with a second failure.
590
- if (status === 401 || status === 403 || type === 'authentication_error')
591
- return false;
592
- if (status === 404)
593
- return false;
594
- // Rate limits / transient errors: agent uses more calls; basic mode may succeed.
595
- if (status === 429)
596
- return true;
597
- if (status && status >= 500)
598
- return true;
599
- const msg = (typeof err?.error?.message === 'string' && err.error.message) ||
600
- (typeof err?.message === 'string' && err.message) ||
601
- '';
602
- const lowered = String(msg).toLowerCase();
603
- // Tool calling compatibility issues: fall back to basic mode.
604
- if (lowered.includes('tool') || lowered.includes('tool_choice') || lowered.includes('function'))
605
- return true;
606
- // Default: keep previous behavior (fallback), unless it's clearly an auth/endpoint issue.
607
- return true;
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
- }
631
- const DEFAULT_SYSTEM_PROMPT_EN = `You are an expert at writing Git commit messages following the Conventional Commits specification.
632
-
633
- Based on the git diff provided, generate a concise and descriptive commit message.
634
-
635
- Rules:
636
- 1. Use the format: <type>(<scope>): <subject>
637
- 2. Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci
638
- 3. Keep the subject line under 50 characters
639
- 4. Use imperative mood ("add" not "added")
640
- 5. Don't end the subject line with a period
641
- 6. If needed, add a blank line followed by a body for more details
642
- 7. Git Flow Branch Mapping (Priority):
643
- - feature/* -> type: feat
644
- - bugfix/* -> type: fix
645
- - hotfix/* -> type: fix
646
- - release/* -> type: chore
647
- - docs/* -> type: docs
648
- - If branch name matches, infer <scope> from it (e.g. feature/login -> feat(login): ...)
649
- - If branch name doesn't match these patterns, ignore it and infer type/scope strictly from the code changes.
650
-
651
- Only output the commit message, nothing else.`;
652
- const DEFAULT_SYSTEM_PROMPT_ZH = `你是一个专业的 Git commit message 编写专家,遵循 Conventional Commits 规范。
653
-
654
- 根据提供的 git diff,生成简洁且描述性的提交信息。
655
-
656
- 规则:
657
- 1. 使用格式: <type>(<scope>): <subject>
658
- 2. type 类型: feat, fix, docs, style, refactor, perf, test, chore, build, ci
659
- 3. subject 保持在 50 字符以内
660
- 4. 使用祈使语气
661
- 5. subject 末尾不要加句号
662
- 6. 如需要,空一行后添加 body 提供更多细节
663
- 7. Git Flow 分支映射规则 (优先级最高):
664
- - feature/* -> type: feat
665
- - bugfix/* -> type: fix
666
- - hotfix/* -> type: fix
667
- - release/* -> type: chore
668
- - docs/* -> type: docs
669
- - 如果分支名匹配,请从中推断 <scope> (例如: feature/login -> feat(login): ...)
670
- - 如果分支名不符合上述标准前缀,请忽略分支名,仅依据代码变更内容(diff)来决定 type 和 scope。
671
-
672
- 只输出 commit message,不要输出其他内容。`;
673
- const DEEPSEEK_PROMPT_ZH = `你是一个智能编程助手,专注于生成高质量的 Git 提交信息。
674
-
675
- 请仔细分析下方的 Git Diff,理解代码变更的*意图*(不仅仅是修改了什么)。
676
-
677
- 规则:
678
- 1. 严格遵循 Conventional Commits 规范: <type>(<scope>): <subject>
679
- 2. 类型(type)必须是: feat, fix, docs, style, refactor, perf, test, chore, build, ci
680
- 3. 描述(subject)需简洁有力,50字符以内,使用中文。
681
- 4. 如果变更复杂,请在 subject 后空一行,添加详细的 body 说明。
682
- 5. 专注于*为什么*变更,而不仅仅是*改了什么*。
683
- 6. Git Flow 分支映射规则 (优先级最高):
684
- - feature/* -> type: feat
685
- - bugfix/* -> type: fix
686
- - hotfix/* -> type: fix
687
- - release/* -> type: chore
688
- - docs/* -> type: docs
689
- - 如果分支名匹配,请从中推断 <scope> (例如: feature/login -> feat(login): ...)
690
- - 如果分支名不符合上述标准前缀,请忽略分支名,仅依据代码变更内容(diff)来决定 type 和 scope。
691
-
692
- 只输出最终的 Commit Message,不包含 Markdown 代码块或其他解释。`;
693
- export function createAIClient(config) {
694
- return new OpenAI({
695
- apiKey: config.apiKey || 'ollama',
696
- baseURL: config.baseUrl,
697
- timeout: getTimeoutMs(),
698
- maxRetries: 2, // Built-in retry support
699
- });
700
- }
701
- export async function generateCommitMessage(client, input, config, numChoices = 1) {
702
- let diff = input.diff;
703
- let ignoredFiles = input.ignoredFiles;
704
- let truncated = input.truncated;
705
- const rules = normalizeRules(config);
706
- const outputFormat = config.outputFormat || 'text';
707
- const ensureDiff = async () => {
708
- if (diff !== undefined)
709
- return;
710
- if (input.diffLoader) {
711
- const loaded = await input.diffLoader();
712
- diff = loaded.diff;
713
- truncated = loaded.truncated;
714
- ignoredFiles = loaded.ignoredFiles;
715
- return;
716
- }
717
- diff = '';
718
- };
719
- // Auto-enable Agent for critical branches in Git Flow
720
- // Critical: release/hotfix/master/main - always use Agent
721
- // Feature: feature/*/bugfix/*/dev/* - use Agent for impact analysis
722
- const branch = input.branchName || '';
723
- const isCriticalBranch = /^(release|hotfix)\//.test(branch) || /^(master|main)$/.test(branch);
724
- const isFeatureBranch = /^(feature|bugfix|dev)\//.test(branch);
725
- const autoAgentEnabled = isAutoAgentEnabled();
726
- const agentDisabled = isAgentDisabled();
727
- const shouldRunAgent = !agentDisabled &&
728
- (input.forceAgent || (autoAgentEnabled && (input.truncated || isCriticalBranch || isFeatureBranch))) &&
729
- numChoices === 1;
730
- // Trigger Agent Mode if diff is truncated OR forced by user OR critical branch
731
- if (shouldRunAgent) {
732
- try {
733
- const stats = await getFileStats();
734
- if (stats.length > 0) {
735
- const agentStrategy = resolveAgentStrategy(config);
736
- const agentModel = resolveAgentModel(config, agentStrategy);
737
- if (!input.quiet) {
738
- const label = agentStrategy === 'tools' ? 'tools' : 'lite';
739
- if (agentModel !== config.model) {
740
- console.log(chalk.gray(`\n🧠 Agent (${label}) model: ${agentModel} (base model: ${config.model})`));
741
- }
742
- else {
743
- console.log(chalk.gray(`\n🧠 Agent strategy: ${label}`));
744
- }
745
- }
746
- const agentMessage = agentStrategy === 'tools'
747
- ? await runAgentLoop(client, config, stats, input.branchName, input.quiet, agentModel)
748
- : await runAgentLite(client, config, stats, input.branchName, input.quiet, agentModel);
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];
752
- }
753
- }
754
- catch (error) {
755
- if (!shouldFallbackFromAgent(error)) {
756
- throw normalizeAIError(error);
757
- }
758
- if (!input.quiet) {
759
- const reason = formatAgentFailureReason(error);
760
- const suffix = reason ? ` (${reason})` : '';
761
- console.error(chalk.yellow(`\n⚠️ Agent mode failed${suffix}, falling back to basic mode...`));
762
- if (process.env.GIT_AI_DEBUG === '1') {
763
- console.error(error);
764
- }
765
- }
766
- }
767
- }
768
- await ensureDiff();
769
- let systemPrompt = config.customPrompt;
770
- if (!systemPrompt) {
771
- const isZh = config.locale === 'zh';
772
- if (config.provider === 'deepseek' || config.provider === 'qwen') {
773
- systemPrompt = isZh ? DEEPSEEK_PROMPT_ZH : DEFAULT_SYSTEM_PROMPT_EN; // Reuse EN for now or add DeepSeek EN later
774
- }
775
- else {
776
- systemPrompt = isZh ? DEFAULT_SYSTEM_PROMPT_ZH : DEFAULT_SYSTEM_PROMPT_EN;
777
- }
778
- }
779
- const isZh = config.locale === 'zh';
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'));
796
- if (numChoices > 1) {
797
- // Add instruction for multiple choices (JSON array for robustness)
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.`;
805
- systemPrompt += multiInstruction;
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
- }
813
- if (input.recentCommits?.length) {
814
- const header = isZh
815
- ? '参考历史提交风格 (请模仿以下风格):'
816
- : 'Reference recent commits (please mimic the style):';
817
- // Extract subject from "hash date subject" format
818
- // Format is "%h %cd %s", so we take everything after the second space
819
- const cleanCommits = input.recentCommits
820
- .map((line) => {
821
- const parts = line.split(' ');
822
- if (parts.length >= 3) {
823
- return parts.slice(2).join(' ');
824
- }
825
- return line;
826
- })
827
- .slice(0, 10); // Limit to 10 to save tokens
828
- lines.push(`${header}\n${cleanCommits.map((c) => `- ${c}`).join('\n')}`);
829
- }
830
- if (input.branchName) {
831
- const header = isZh ? '当前分支:' : 'Current branch:';
832
- lines.push(`${header} ${input.branchName}`);
833
- }
834
- if (input.stagedFiles?.length) {
835
- const header = isZh ? '已暂存文件:' : 'Staged files:';
836
- lines.push(`${header}\n${input.stagedFiles.map((f) => `- ${f}`).join('\n')}`);
837
- }
838
- if (ignoredFiles?.length) {
839
- const header = isZh
840
- ? '以下文件为节省 Token 已忽略 Diff:'
841
- : 'Ignored files (diff omitted for token optimization):';
842
- lines.push(`${header}\n${ignoredFiles.map((f) => `- ${f}`).join('\n')}`);
843
- }
844
- if (truncated) {
845
- lines.push(isZh
846
- ? '注意:Diff 内容已因长度限制被截断。'
847
- : 'Note: The diff was truncated due to size limits.');
848
- }
849
- const diffHeader = isZh ? 'Git Diff:' : 'Git diff:';
850
- lines.push(`${diffHeader}\n\n${diff || '(empty)'}`);
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
- }
880
- const content = response.choices[0]?.message?.content?.trim();
881
- if (!content) {
882
- throw new Error('Failed to generate commit message: empty response');
883
- }
884
- const normalized = stripCodeFences(content);
885
- let messages = [];
886
- if (outputFormat === 'json') {
887
- const parsed = parseCommitJson(normalized);
888
- if (parsed && parsed.length) {
889
- messages = formatFromCommitJson(parsed, rules, config.locale, input.branchName, input.stagedFiles);
890
- }
891
- else {
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));
895
- }
896
- }
897
- else {
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
- }
909
- }
910
- else {
911
- const parsed = tryParseMessagesJson(normalized);
912
- if (parsed && parsed.length === 1) {
913
- messages = parsed;
914
- }
915
- else {
916
- messages = [normalized];
917
- }
918
- }
919
- messages = messages.map((msg) => enforceRulesOnTextMessage(msg, rules, config.locale, input.branchName, input.stagedFiles));
920
- }
921
- if (config.enableFooter) {
922
- return messages.map((msg) => `${msg}\n\n🤖 Generated by git-ai 🚀`);
923
- }
924
- return messages;
925
- }
926
- const REPORT_PROMPT_ZH = `你是一位资深技术专家,擅长撰写高质量的周报/日报。
927
-
928
- 请根据提供的 Git Commit 记录,整理出一份结构清晰、重点突出的工作汇报。
929
-
930
- 规则:
931
- 1. **分类汇总**:将提交记录归类(例如:✨ 新特性、🐛 问题修复、⚡️ 性能优化、📝 文档与其他)。
932
- 2. **价值导向**:不要只罗列代码变更,尝试用简练的语言描述业务价值或技术成果。
933
- 3. **格式美观**:使用 Markdown 格式,利用列表和 emoji 让阅读体验更佳。
934
- 4. **过滤噪音**:忽略无意义的测试提交或临时提交。
935
-
936
- 输出格式示例:
937
- ## 📅 工作汇报 (Time Range)
938
-
939
- ### ✨ 核心产出
940
- - **功能 A**: 完成了...逻辑,提升了...体验
941
- - **功能 B**: ...
942
-
943
- ### 🐛 问题修复
944
- - 修复了...导致的崩溃问题
945
-
946
- ### 📝 其他
947
- - ...
948
-
949
- (结尾可加一句下周计划建议)`;
950
- const REPORT_PROMPT_EN = `You are a senior technical lead expert at writing professional progress reports.
951
-
952
- Based on the provided Git Commit logs, generate a structured and high-quality status report.
953
-
954
- Rules:
955
- 1. **Categorize**: Group commits logically (e.g., ✨ Features, 🐛 Bug Fixes, ⚡️ Improvements, 📝 Other).
956
- 2. **Value-Driven**: Don't just list technical changes; briefly emphasize the value or outcome.
957
- 3. **Formatting**: Use Markdown with bullet points and emojis.
958
- 4. **Filter Noise**: Ignore trivial or "wip" commits.
959
-
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
- }
1070
- export async function generateWeeklyReport(client, commits, config) {
1071
- const isZh = config.locale === 'zh';
1072
- const systemPrompt = isZh ? REPORT_PROMPT_ZH : REPORT_PROMPT_EN;
1073
- if (commits.length === 0) {
1074
- return isZh ? '这段时间没有找到您的提交记录。' : 'No commits found for this period.';
1075
- }
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() || '';
1102
- }
1103
- //# sourceMappingURL=ai.js.map