ai-git-tools 2.1.9 → 2.1.11
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
CHANGED
|
@@ -372,6 +372,35 @@ Issue 的基本責任分工如下:`description` 保存原始需求與未來其
|
|
|
372
372
|
|
|
373
373
|
同一個 Issue 若已經有先前的 notes,後續再次執行更新仍會新增一筆新的 Redmine journal,不會因為已有 notes 而略過;只有 Issue 的需求內容或狀態在產生草稿後被其他人修改時,才會停下來要求重新確認。
|
|
374
374
|
|
|
375
|
+
AI 產出的 notes 會固定整理成以下架構:
|
|
376
|
+
|
|
377
|
+
```markdown
|
|
378
|
+
## 開發內容
|
|
379
|
+
|
|
380
|
+
### 需求說明
|
|
381
|
+
一句話說明這次為什麼修改。
|
|
382
|
+
|
|
383
|
+
### 實作內容
|
|
384
|
+
- 實際完成事項
|
|
385
|
+
- 實際完成事項
|
|
386
|
+
- 實際完成事項
|
|
387
|
+
|
|
388
|
+
### 行為規則
|
|
389
|
+
| 情境 | 行為 |
|
|
390
|
+
|---|---|
|
|
391
|
+
| ... | ... |
|
|
392
|
+
|
|
393
|
+
### 影響範圍
|
|
394
|
+
受影響的功能、API、頁面或模組。
|
|
395
|
+
|
|
396
|
+
### 待確認
|
|
397
|
+
- [ ] AI 無法確認的項目
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
實作內容最多顯示 6 個項目;沒有待確認事項時不會建立空的 `待確認` 區塊。Git 與流程圖區塊會維持既有格式。
|
|
401
|
+
|
|
402
|
+
所有 Copilot AI 回應會在程式內統一轉換為台灣繁體中文,包含 Redmine notes、需求說明、實作內容、行為規則、影響範圍與 Mermaid 文字;不依賴模型自行遵守語言設定。
|
|
403
|
+
|
|
375
404
|
## ⚙️ 配置
|
|
376
405
|
|
|
377
406
|
配置檔案範例(\`.ai-git-config.js\`):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-git-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.11",
|
|
4
4
|
"description": "AI-powered Git automation tools for commit messages and PR generation",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"commander": "^12.0.0",
|
|
46
46
|
"inquirer": "^9.2.0",
|
|
47
|
+
"opencc-js": "^1.4.2",
|
|
47
48
|
"ora": "^8.0.1"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { buildRedmineConfig } from '../redmine/config.js';
|
|
13
13
|
import { RedmineClient } from '../redmine/redmine-client.js';
|
|
14
14
|
import { Logger } from '../utils/logger.js';
|
|
15
|
+
import { toTraditionalChinese } from '../utils/traditional-chinese.js';
|
|
15
16
|
import {
|
|
16
17
|
applyRedmineDraft,
|
|
17
18
|
generateRedmineDraft,
|
|
@@ -32,7 +33,7 @@ function printProgress(progress) {
|
|
|
32
33
|
logger.step(`[${progress.index}/${progress.total}] 讀取 Redmine Issue #${progress.issueId}...`);
|
|
33
34
|
break;
|
|
34
35
|
case 'issue-read':
|
|
35
|
-
logger.success(`Issue #${progress.issueId} 已取得:${progress.subject || '無標題'}`);
|
|
36
|
+
logger.success(`Issue #${progress.issueId} 已取得:${toTraditionalChinese(progress.subject || '無標題')}`);
|
|
36
37
|
break;
|
|
37
38
|
case 'analyze-issue':
|
|
38
39
|
logger.step(`[${progress.index}/${progress.total}] 使用 Copilot 分析 Issue #${progress.issueId}...`);
|
|
@@ -161,7 +162,7 @@ export async function redmineUpdateCommand(options = {}) {
|
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
if (pullRequest) {
|
|
164
|
-
logger.success(`已找到 PR #${pullRequest.number}:${pullRequest.title || '無標題'}`);
|
|
165
|
+
logger.success(`已找到 PR #${pullRequest.number}:${toTraditionalChinese(pullRequest.title || '無標題')}`);
|
|
165
166
|
console.log(`🔗 ${pullRequest.url}`);
|
|
166
167
|
} else {
|
|
167
168
|
logger.info('目前 branch 沒有找到對應的 GitHub PR,略過 PR 資訊');
|
package/src/core/ai-client.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { CopilotClient, approveAll } from '@github/copilot-sdk';
|
|
7
|
+
import { toTraditionalChinese } from '../utils/traditional-chinese.js';
|
|
7
8
|
|
|
8
9
|
export class AIClient {
|
|
9
10
|
/**
|
|
@@ -75,7 +76,7 @@ export class AIClient {
|
|
|
75
76
|
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
76
77
|
|
|
77
78
|
const content = response?.data?.content || '';
|
|
78
|
-
return content.trim();
|
|
79
|
+
return toTraditionalChinese(content.trim());
|
|
79
80
|
} catch (error) {
|
|
80
81
|
lastError = error;
|
|
81
82
|
if (attempt < maxRetries) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { AIClient } from '../core/ai-client.js';
|
|
2
|
+
import { normalizeAnalysisLanguage } from '../utils/traditional-chinese.js';
|
|
2
3
|
|
|
3
4
|
const ANALYSIS_FIELDS = [
|
|
4
5
|
'developmentSummary',
|
|
5
6
|
'requirementDescription',
|
|
7
|
+
'implementationDetails',
|
|
8
|
+
'impactScope',
|
|
6
9
|
'codeChanges',
|
|
7
10
|
'apiChanges',
|
|
8
11
|
'technicalDetails',
|
|
@@ -15,9 +18,11 @@ const ANALYSIS_FIELDS = [
|
|
|
15
18
|
function normalizeFlowcharts(value) {
|
|
16
19
|
const flowcharts = Array.isArray(value)
|
|
17
20
|
? value
|
|
18
|
-
: value
|
|
21
|
+
: typeof value === 'string'
|
|
19
22
|
? [{ source: value }]
|
|
20
|
-
:
|
|
23
|
+
: value
|
|
24
|
+
? [value]
|
|
25
|
+
: [];
|
|
21
26
|
|
|
22
27
|
const usedIds = new Set();
|
|
23
28
|
return flowcharts
|
|
@@ -37,7 +42,9 @@ function normalizeFlowcharts(value) {
|
|
|
37
42
|
return {
|
|
38
43
|
id: uniqueId,
|
|
39
44
|
title,
|
|
40
|
-
source: typeof flowchart.source
|
|
45
|
+
source: typeof (flowchart.source || flowchart.mermaid) === 'string'
|
|
46
|
+
? (flowchart.source || flowchart.mermaid).trim()
|
|
47
|
+
: '',
|
|
41
48
|
evidence: asStringArray(flowchart.evidence),
|
|
42
49
|
};
|
|
43
50
|
})
|
|
@@ -80,13 +87,15 @@ export function buildIssueAnalysisPrompt({ issue, evidence }) {
|
|
|
80
87
|
|
|
81
88
|
重要規則:
|
|
82
89
|
- 這次只分析指定的 Issue #${issue.id},不得把結果指派給其他 Issue。
|
|
90
|
+
- 所有自然語言欄位必須使用台灣繁體中文,禁止輸出簡體中文。
|
|
83
91
|
- 只能描述下方 Issue、Git commits、changed files、diff 或 PR evidence 中有明確證據的內容。
|
|
84
92
|
- 不得捏造 API endpoint、參數、檔案、function、class、測試或部署結果。
|
|
93
|
+
- requirementDescription 必須是一句話,說明這次為什麼修改。
|
|
94
|
+
- implementationDetails 必須整理 3~6 個實際完成事項。
|
|
95
|
+
- behaviorRules 必須使用 scenario/behavior 物件描述不同情境的行為。
|
|
96
|
+
- impactScope 必須描述受到影響的功能、API、頁面或模組。
|
|
97
|
+
- 若能從實際流程推導出可靠的流程,才產生 flowcharts;每張流程圖必須有穩定 id、title、source 與 evidence。
|
|
85
98
|
- 沒有明確測試或驗證證據時,verification 必須是空陣列。
|
|
86
|
-
- 沒有可靠 API 變更證據時,apiChanges 必須是空陣列。
|
|
87
|
-
- requirementDescription 應整理指定 Issue 的需求重點;behaviorRules 使用 scenario/behavior 物件陣列。
|
|
88
|
-
- 若能從實際流程推導出可靠的複製流程,才產生 flowcharts;否則為空陣列。每張流程圖必須有穩定、描述用途的 id,source 必須以 graph TD 開始,不要加入 Mermaid wrapper 或 code fence。
|
|
89
|
-
- developmentSummary、codeChanges、apiChanges、technicalDetails 中的程式路徑、API path、function 與 class 使用雙反引號標示;需要多行程式片段時使用 code block。
|
|
90
99
|
- 不要產生 suggestedStatus、coverage 或 confidence,也不要決定 Redmine status。
|
|
91
100
|
|
|
92
101
|
## 指定 Redmine Issue
|
|
@@ -100,6 +109,8 @@ ${JSON.stringify(evidence, null, 2)}
|
|
|
100
109
|
"issueId": ${issue.id},
|
|
101
110
|
"developmentSummary": [],
|
|
102
111
|
"requirementDescription": "",
|
|
112
|
+
"implementationDetails": [],
|
|
113
|
+
"impactScope": "",
|
|
103
114
|
"behaviorRules": [],
|
|
104
115
|
"codeChanges": [],
|
|
105
116
|
"apiChanges": [],
|
|
@@ -110,8 +121,7 @@ ${JSON.stringify(evidence, null, 2)}
|
|
|
110
121
|
"unresolvedItems": [],
|
|
111
122
|
"evidence": []
|
|
112
123
|
}
|
|
113
|
-
|
|
114
|
-
developmentSummary、codeChanges、apiChanges、technicalDetails、modifiedFiles、verification、unresolvedItems 與 evidence 必須是字串陣列;requirementDescription 必須是字串;behaviorRules 必須是包含 scenario 與 behavior 字串的物件陣列;flowcharts 必須是包含 id、title、source 與 evidence 的物件陣列。`;
|
|
124
|
+
requirementDescription 與 impactScope 必須是字串;implementationDetails 必須是 3~6 個字串的陣列;behaviorRules 必須是包含 scenario 與 behavior 字串的物件陣列;flowcharts 必須是包含 id、title、source 與 evidence 的物件陣列;其它清單欄位必須是字串陣列。`;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
/**
|
|
@@ -121,11 +131,11 @@ developmentSummary、codeChanges、apiChanges、technicalDetails、modifiedFiles
|
|
|
121
131
|
* @returns {object}
|
|
122
132
|
*/
|
|
123
133
|
export function parseIssueAnalysis(content, issueId) {
|
|
124
|
-
const parsed = AIClient.parseJSON(content);
|
|
134
|
+
const parsed = normalizeAnalysisLanguage(AIClient.parseJSON(content));
|
|
125
135
|
const result = { issueId };
|
|
126
136
|
|
|
127
137
|
for (const field of ANALYSIS_FIELDS) {
|
|
128
|
-
result[field] = field === 'requirementDescription'
|
|
138
|
+
result[field] = field === 'requirementDescription' || field === 'impactScope'
|
|
129
139
|
? (typeof parsed[field] === 'string' ? parsed[field].trim() : '')
|
|
130
140
|
: asStringArray(parsed[field]);
|
|
131
141
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { normalizeAnalysisLanguage, toTraditionalChinese } from '../utils/traditional-chinese.js';
|
|
3
4
|
|
|
4
5
|
function bulletSection(title, items) {
|
|
5
6
|
if (!Array.isArray(items) || items.length === 0) return '';
|
|
@@ -11,6 +12,23 @@ function paragraphSection(title, content) {
|
|
|
11
12
|
return `### ${title}\n\n${content}\n`;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
function formatRequirement(content) {
|
|
16
|
+
if (!content) return '';
|
|
17
|
+
return content.replace(/\s+/g, ' ').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getImplementationDetails(analysis = {}) {
|
|
21
|
+
if (analysis.implementationDetails?.length) {
|
|
22
|
+
return analysis.implementationDetails.slice(0, 6);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
...(analysis.codeChanges || []),
|
|
27
|
+
...(analysis.apiChanges || []),
|
|
28
|
+
...(analysis.technicalDetails || []),
|
|
29
|
+
].slice(0, 6);
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
function formatBehaviorRules(rules = []) {
|
|
15
33
|
if (!Array.isArray(rules) || rules.length === 0) return '';
|
|
16
34
|
const rows = rules
|
|
@@ -152,39 +170,29 @@ export function formatRedmineDescription(description = '', source = null) {
|
|
|
152
170
|
* @returns {string}
|
|
153
171
|
*/
|
|
154
172
|
export function formatRedmineNote({ issue = {}, analysis = {}, evidence = {}, pullRequest = null }) {
|
|
173
|
+
const localizedIssue = normalizeAnalysisLanguage(issue);
|
|
174
|
+
const localizedAnalysis = normalizeAnalysisLanguage(analysis);
|
|
155
175
|
const sections = ['## 開發內容', ''];
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
: analysis.developmentSummary || '';
|
|
159
|
-
const requirementDescription = analysis.requirementDescription || issue.description || '';
|
|
160
|
-
const behaviorRules = formatBehaviorRules(analysis.behaviorRules);
|
|
161
|
-
const codeChanges = bulletSection('程式修改重點', analysis.codeChanges);
|
|
162
|
-
const apiChanges = bulletSection('API 變更', analysis.apiChanges);
|
|
163
|
-
const technicalDetails = bulletSection('重要技術細節', analysis.technicalDetails);
|
|
164
|
-
const modifiedFiles = bulletSection(
|
|
165
|
-
'修改檔案',
|
|
166
|
-
analysis.modifiedFiles?.length
|
|
167
|
-
? analysis.modifiedFiles.map(file => `\`\`${file}\`\``)
|
|
168
|
-
: evidence.changedFiles?.map(file => `\`\`${file}\`\``)
|
|
176
|
+
const requirementDescription = formatRequirement(
|
|
177
|
+
localizedAnalysis.requirementDescription || localizedAnalysis.developmentSummary?.[0] || localizedIssue.description || ''
|
|
169
178
|
);
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
179
|
+
const implementationDetails = getImplementationDetails(localizedAnalysis);
|
|
180
|
+
const behaviorRules = formatBehaviorRules(localizedAnalysis.behaviorRules);
|
|
181
|
+
const implementation = bulletSection('實作內容', implementationDetails);
|
|
182
|
+
const impactScope = paragraphSection('影響範圍', localizedAnalysis.impactScope);
|
|
183
|
+
const unresolved = localizedAnalysis.unresolvedItems?.length
|
|
184
|
+
? `### 待確認\n${localizedAnalysis.unresolvedItems.map(item => `- [ ] ${item}`).join('\n')}\n`
|
|
173
185
|
: '';
|
|
174
|
-
const flowcharts = formatFlowcharts(
|
|
186
|
+
const flowcharts = formatFlowcharts(localizedAnalysis.flowcharts);
|
|
175
187
|
|
|
176
188
|
sections.push(
|
|
177
|
-
development,
|
|
178
189
|
paragraphSection('需求說明', requirementDescription),
|
|
190
|
+
implementation,
|
|
179
191
|
behaviorRules,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
technicalDetails,
|
|
184
|
-
flowcharts,
|
|
185
|
-
verification
|
|
192
|
+
impactScope,
|
|
193
|
+
unresolved,
|
|
194
|
+
flowcharts
|
|
186
195
|
);
|
|
187
|
-
sections.push(unresolved);
|
|
188
196
|
|
|
189
197
|
if (evidence.currentBranch || evidence.baseBranch || pullRequest) {
|
|
190
198
|
const branch = evidence.currentBranch || '—';
|
|
@@ -212,8 +220,8 @@ export function formatRedmineNote({ issue = {}, analysis = {}, evidence = {}, pu
|
|
|
212
220
|
);
|
|
213
221
|
}
|
|
214
222
|
|
|
215
|
-
if (
|
|
216
|
-
sections.push(`分析依據:${
|
|
223
|
+
if (localizedAnalysis.evidence?.length) {
|
|
224
|
+
sections.push(`分析依據:${localizedAnalysis.evidence.join('、')}`, '');
|
|
217
225
|
}
|
|
218
226
|
|
|
219
227
|
return sections.filter((section, index) => section || index === 1).join('\n').trim();
|
|
@@ -290,8 +298,8 @@ export function formatRedminePreview(drafts = []) {
|
|
|
290
298
|
for (const draft of drafts) {
|
|
291
299
|
lines.push(
|
|
292
300
|
chalk.bold.cyan(`Issue #${draft.issueId}`),
|
|
293
|
-
`${chalk.dim('目前狀態:')} ${draft.currentStatusName || draft.originalStatusName || '—'}`,
|
|
294
|
-
`${chalk.green('更新狀態:')} ${draft.statusName || '未選擇'}`,
|
|
301
|
+
`${chalk.dim('目前狀態:')} ${toTraditionalChinese(draft.currentStatusName || draft.originalStatusName || '—')}`,
|
|
302
|
+
`${chalk.green('更新狀態:')} ${toTraditionalChinese(draft.statusName || '未選擇')}`,
|
|
295
303
|
draft.doneRatio !== undefined ? `${chalk.green('完成百分比:')} ${draft.doneRatio}%` : '',
|
|
296
304
|
draft.dueDate ? `${chalk.green('完成日期:')} ${draft.dueDate}` : '',
|
|
297
305
|
draft.customFields?.length
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import OpenCC from 'opencc-js';
|
|
2
|
+
|
|
3
|
+
const convertToTraditional = OpenCC.Converter({ from: 'cn', to: 'tw' });
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 將文字轉換為台灣繁體中文
|
|
7
|
+
* @param {string} value
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function toTraditionalChinese(value) {
|
|
11
|
+
return typeof value === 'string' ? convertToTraditional(value) : value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 遞迴將資料中的所有字串轉換為台灣繁體中文
|
|
16
|
+
* @param {unknown} value
|
|
17
|
+
* @returns {unknown}
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeAnalysisLanguage(value) {
|
|
20
|
+
if (typeof value === 'string') return toTraditionalChinese(value);
|
|
21
|
+
if (Array.isArray(value)) return value.map(normalizeAnalysisLanguage);
|
|
22
|
+
if (value && typeof value === 'object') {
|
|
23
|
+
return Object.fromEntries(
|
|
24
|
+
Object.entries(value).map(([key, item]) => [key, normalizeAnalysisLanguage(item)])
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|