ai-git-tools 2.1.12 → 2.1.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.1.12",
3
+ "version": "2.1.13",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -6,6 +6,49 @@
6
6
  import { CopilotClient, approveAll } from '@github/copilot-sdk';
7
7
  import { toTraditionalChinese } from '../utils/traditional-chinese.js';
8
8
 
9
+ function extractJsonCandidates(content) {
10
+ const candidates = [];
11
+
12
+ for (let start = 0; start < content.length; start += 1) {
13
+ if (content[start] !== '{' && content[start] !== '[') continue;
14
+
15
+ const expectedClosers = [];
16
+ let inString = false;
17
+ let escaped = false;
18
+
19
+ for (let index = start; index < content.length; index += 1) {
20
+ const character = content[index];
21
+
22
+ if (inString) {
23
+ if (escaped) {
24
+ escaped = false;
25
+ } else if (character === '\\') {
26
+ escaped = true;
27
+ } else if (character === '"') {
28
+ inString = false;
29
+ }
30
+ continue;
31
+ }
32
+
33
+ if (character === '"') {
34
+ inString = true;
35
+ } else if (character === '{') {
36
+ expectedClosers.push('}');
37
+ } else if (character === '[') {
38
+ expectedClosers.push(']');
39
+ } else if (character === '}' || character === ']') {
40
+ if (expectedClosers.pop() !== character) break;
41
+ if (expectedClosers.length === 0) {
42
+ candidates.push(content.slice(start, index + 1));
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ return candidates;
50
+ }
51
+
9
52
  export class AIClient {
10
53
  /**
11
54
  * 判斷 Node.js 是否符合 Copilot SDK 的版本需求
@@ -105,7 +148,7 @@ export class AIClient {
105
148
  */
106
149
  static parseJSON(content) {
107
150
  // 移除可能的 markdown code block 標記
108
- const jsonContent = content
151
+ const jsonContent = String(content || '')
109
152
  .replace(/```json\n?/g, '')
110
153
  .replace(/```\n?/g, '')
111
154
  .trim();
@@ -113,6 +156,14 @@ export class AIClient {
113
156
  try {
114
157
  return JSON.parse(jsonContent);
115
158
  } catch (error) {
159
+ for (const candidate of extractJsonCandidates(jsonContent)) {
160
+ try {
161
+ return JSON.parse(candidate);
162
+ } catch {
163
+ // 繼續嘗試下一個完整 JSON 區塊
164
+ }
165
+ }
166
+
116
167
  throw new Error(`無法解析 AI 回應為 JSON: ${error.message}`);
117
168
  }
118
169
  }
@@ -25,7 +25,7 @@ function normalizeFlowcharts(value) {
25
25
  : [];
26
26
 
27
27
  const usedIds = new Set();
28
- return flowcharts
28
+ return flowcharts.slice(0, 1)
29
29
  .filter(flowchart => flowchart && typeof flowchart === 'object')
30
30
  .map((flowchart, index) => {
31
31
  const title = typeof flowchart.title === 'string' ? flowchart.title.trim() : '';
@@ -95,7 +95,9 @@ export function buildIssueAnalysisPrompt({ issue, evidence }) {
95
95
  - behaviorRules 必須使用 scenario/behavior 物件描述不同情境的行為。
96
96
  - impactScope 必須描述受到影響的功能、API、頁面或模組。
97
97
  - 只要 Issue 或 Git evidence 能推導出頁面、API、驗證或狀態之間的實際步驟,flowcharts 必須至少產生一張 Mermaid 流程圖,不得因為已有文字摘要而省略。
98
- - 只有在證據不足以描述任何流程時,flowcharts 才能回傳空陣列;每張流程圖必須有穩定 id、title、source 與 evidence。
98
+ - 每個 Issue 只能產生一張流程圖;flowcharts 陣列最多只能有一個元素。若有多個候選流程,請合併成一張涵蓋完整流程的圖。
99
+ - API path 不得使用大括號參數,例如 /items/{id} 必須改成 /items/:id;每張流程圖必須有穩定 id、title、source 與 evidence。
100
+ - 只有在證據不足以描述任何流程時,flowcharts 才能回傳空陣列。
99
101
  - 沒有明確測試或驗證證據時,verification 必須是空陣列。
100
102
  - 不要產生 suggestedStatus、coverage 或 confidence,也不要決定 Redmine status。
101
103
 
@@ -41,7 +41,7 @@ function formatBehaviorRules(rules = []) {
41
41
  function formatFlowcharts(flowcharts = []) {
42
42
  if (!Array.isArray(flowcharts) || flowcharts.length === 0) return '';
43
43
 
44
- return flowcharts
44
+ return flowcharts.slice(0, 1)
45
45
  .map((flowchart, index) => {
46
46
  const mermaid = formatMermaid(flowchart?.source);
47
47
  if (!mermaid) return '';
@@ -77,6 +77,18 @@ function normalizeFlowchartInput(flowcharts) {
77
77
  return [];
78
78
  }
79
79
 
80
+ function normalizeApiPathPlaceholders(source) {
81
+ return source
82
+ .split('\n')
83
+ .map(line => {
84
+ const containsApiPath = /(?:\b(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b|https?:\/\/|\/[^\s/]+\/)/i.test(line);
85
+ return containsApiPath
86
+ ? line.replace(/\{([A-Za-z][A-Za-z0-9_-]*)\}/g, ':$1')
87
+ : line;
88
+ })
89
+ .join('\n');
90
+ }
91
+
80
92
  function hasSameMermaid(description, mermaid) {
81
93
  const existing = description.match(/\{\{\s*mermaid\b[\s\S]*?\}\}/gi) || [];
82
94
  return existing.some(block => formatMermaid(block) === mermaid);
@@ -122,6 +134,7 @@ export function formatMermaid(source) {
122
134
  }
123
135
 
124
136
  cleanSource = cleanSource.replace(/^flowchart\s+TD\b/i, 'graph TD');
137
+ cleanSource = normalizeApiPathPlaceholders(cleanSource);
125
138
 
126
139
  if (
127
140
  cleanSource.includes('{{mermaid') ||