ai-git-tools 2.1.10 → 2.1.12

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
@@ -399,6 +399,8 @@ AI 產出的 notes 會固定整理成以下架構:
399
399
 
400
400
  實作內容最多顯示 6 個項目;沒有待確認事項時不會建立空的 `待確認` 區塊。Git 與流程圖區塊會維持既有格式。
401
401
 
402
+ 所有 Copilot AI 回應會在程式內統一轉換為台灣繁體中文,包含 Redmine notes、需求說明、實作內容、行為規則、影響範圍與 Mermaid 文字;不依賴模型自行遵守語言設定。
403
+
402
404
  ## ⚙️ 配置
403
405
 
404
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.10",
3
+ "version": "2.1.12",
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 資訊');
@@ -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,4 +1,5 @@
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',
@@ -17,9 +18,11 @@ const ANALYSIS_FIELDS = [
17
18
  function normalizeFlowcharts(value) {
18
19
  const flowcharts = Array.isArray(value)
19
20
  ? value
20
- : value
21
+ : typeof value === 'string'
21
22
  ? [{ source: value }]
22
- : [];
23
+ : value
24
+ ? [value]
25
+ : [];
23
26
 
24
27
  const usedIds = new Set();
25
28
  return flowcharts
@@ -39,7 +42,9 @@ function normalizeFlowcharts(value) {
39
42
  return {
40
43
  id: uniqueId,
41
44
  title,
42
- source: typeof flowchart.source === 'string' ? flowchart.source.trim() : '',
45
+ source: typeof (flowchart.source || flowchart.mermaid) === 'string'
46
+ ? (flowchart.source || flowchart.mermaid).trim()
47
+ : '',
43
48
  evidence: asStringArray(flowchart.evidence),
44
49
  };
45
50
  })
@@ -82,13 +87,15 @@ export function buildIssueAnalysisPrompt({ issue, evidence }) {
82
87
 
83
88
  重要規則:
84
89
  - 這次只分析指定的 Issue #${issue.id},不得把結果指派給其他 Issue。
90
+ - 所有自然語言欄位必須使用台灣繁體中文,禁止輸出簡體中文。
85
91
  - 只能描述下方 Issue、Git commits、changed files、diff 或 PR evidence 中有明確證據的內容。
86
92
  - 不得捏造 API endpoint、參數、檔案、function、class、測試或部署結果。
87
93
  - requirementDescription 必須是一句話,說明這次為什麼修改。
88
94
  - implementationDetails 必須整理 3~6 個實際完成事項。
89
95
  - behaviorRules 必須使用 scenario/behavior 物件描述不同情境的行為。
90
96
  - impactScope 必須描述受到影響的功能、API、頁面或模組。
91
- - 若能從實際流程推導出可靠的流程,才產生 flowcharts;每張流程圖必須有穩定 id、title、source evidence。
97
+ - 只要 Issue 或 Git evidence 能推導出頁面、API、驗證或狀態之間的實際步驟,flowcharts 必須至少產生一張 Mermaid 流程圖,不得因為已有文字摘要而省略。
98
+ - 只有在證據不足以描述任何流程時,flowcharts 才能回傳空陣列;每張流程圖必須有穩定 id、title、source 與 evidence。
92
99
  - 沒有明確測試或驗證證據時,verification 必須是空陣列。
93
100
  - 不要產生 suggestedStatus、coverage 或 confidence,也不要決定 Redmine status。
94
101
 
@@ -125,7 +132,7 @@ requirementDescription 與 impactScope 必須是字串;implementationDetails
125
132
  * @returns {object}
126
133
  */
127
134
  export function parseIssueAnalysis(content, issueId) {
128
- const parsed = AIClient.parseJSON(content);
135
+ const parsed = normalizeAnalysisLanguage(AIClient.parseJSON(content));
129
136
  const result = { issueId };
130
137
 
131
138
  for (const field of ANALYSIS_FIELDS) {
@@ -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 '';
@@ -169,18 +170,20 @@ export function formatRedmineDescription(description = '', source = null) {
169
170
  * @returns {string}
170
171
  */
171
172
  export function formatRedmineNote({ issue = {}, analysis = {}, evidence = {}, pullRequest = null }) {
173
+ const localizedIssue = normalizeAnalysisLanguage(issue);
174
+ const localizedAnalysis = normalizeAnalysisLanguage(analysis);
172
175
  const sections = ['## 開發內容', ''];
173
176
  const requirementDescription = formatRequirement(
174
- analysis.requirementDescription || analysis.developmentSummary?.[0] || issue.description || ''
177
+ localizedAnalysis.requirementDescription || localizedAnalysis.developmentSummary?.[0] || localizedIssue.description || ''
175
178
  );
176
- const implementationDetails = getImplementationDetails(analysis);
177
- const behaviorRules = formatBehaviorRules(analysis.behaviorRules);
179
+ const implementationDetails = getImplementationDetails(localizedAnalysis);
180
+ const behaviorRules = formatBehaviorRules(localizedAnalysis.behaviorRules);
178
181
  const implementation = bulletSection('實作內容', implementationDetails);
179
- const impactScope = paragraphSection('影響範圍', analysis.impactScope);
180
- const unresolved = analysis.unresolvedItems?.length
181
- ? `### 待確認\n${analysis.unresolvedItems.map(item => `- [ ] ${item}`).join('\n')}\n`
182
+ const impactScope = paragraphSection('影響範圍', localizedAnalysis.impactScope);
183
+ const unresolved = localizedAnalysis.unresolvedItems?.length
184
+ ? `### 待確認\n${localizedAnalysis.unresolvedItems.map(item => `- [ ] ${item}`).join('\n')}\n`
182
185
  : '';
183
- const flowcharts = formatFlowcharts(analysis.flowcharts);
186
+ const flowcharts = formatFlowcharts(localizedAnalysis.flowcharts);
184
187
 
185
188
  sections.push(
186
189
  paragraphSection('需求說明', requirementDescription),
@@ -217,8 +220,8 @@ export function formatRedmineNote({ issue = {}, analysis = {}, evidence = {}, pu
217
220
  );
218
221
  }
219
222
 
220
- if (analysis.evidence?.length) {
221
- sections.push(`分析依據:${analysis.evidence.join('、')}`, '');
223
+ if (localizedAnalysis.evidence?.length) {
224
+ sections.push(`分析依據:${localizedAnalysis.evidence.join('、')}`, '');
222
225
  }
223
226
 
224
227
  return sections.filter((section, index) => section || index === 1).join('\n').trim();
@@ -295,8 +298,8 @@ export function formatRedminePreview(drafts = []) {
295
298
  for (const draft of drafts) {
296
299
  lines.push(
297
300
  chalk.bold.cyan(`Issue #${draft.issueId}`),
298
- `${chalk.dim('目前狀態:')} ${draft.currentStatusName || draft.originalStatusName || '—'}`,
299
- `${chalk.green('更新狀態:')} ${draft.statusName || '未選擇'}`,
301
+ `${chalk.dim('目前狀態:')} ${toTraditionalChinese(draft.currentStatusName || draft.originalStatusName || '—')}`,
302
+ `${chalk.green('更新狀態:')} ${toTraditionalChinese(draft.statusName || '未選擇')}`,
300
303
  draft.doneRatio !== undefined ? `${chalk.green('完成百分比:')} ${draft.doneRatio}%` : '',
301
304
  draft.dueDate ? `${chalk.green('完成日期:')} ${draft.dueDate}` : '',
302
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
+ }