@@ -4,147 +4,205 @@ import path from 'path';
44import { fileURLToPath } from 'url' ;
55import fetch from 'node-fetch' ;
66import 'dotenv/config' ;
7+ import { execSync } from 'child_process' ;
78
89// --- 檔案路徑設定 ---
910const __filename = fileURLToPath ( import . meta. url ) ;
1011const __dirname = path . dirname ( __filename ) ;
11- const ROOT_DIR = path . resolve ( __dirname , '..' ) ; // 專案根目錄
12+ const ROOT_DIR = path . resolve ( __dirname , '..' ) ;
1213const PROJECTS_DIR = path . join ( ROOT_DIR , 'public' , 'show' ) ;
1314const METADATA_PATH = path . join ( ROOT_DIR , 'metadata.json' ) ;
1415
1516// --- Gemini API 設定 ---
1617const API_KEY = process . env . GEMINI_API_KEY ;
1718const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${ API_KEY } ` ;
1819
19- /**
20- * 讀取 JSON 檔案並回傳內容,若檔案不存在則回傳空物件。
21- * @param {string } filePath - 檔案路徑
22- * @returns {Promise<object> }
23- */
20+ // --- 輔助函式 ---
21+
2422async function readJsonFile ( filePath ) {
2523 try {
2624 const data = await fs . readFile ( filePath , 'utf8' ) ;
2725 return JSON . parse ( data ) ;
2826 } catch ( error ) {
29- if ( error . code === 'ENOENT' ) {
30- console . warn ( `警告: 找不到檔案 ${ filePath } ,將建立一個新的。` ) ;
31- return { } ;
32- }
27+ if ( error . code === 'ENOENT' ) return { } ;
3328 throw error ;
3429 }
3530}
3631
37- /**
38- * 使用 Gemini API 為 HTML 內容生成元數據。
39- * @param {string } htmlContent - HTML 檔案的內容
40- * @returns {Promise<object|null> } - 包含 name, desc, tags 的物件
41- */
42- async function generateMetadata ( htmlContent ) {
32+ async function callGeminiAPI ( prompt ) {
4333 if ( ! API_KEY ) {
4434 console . error ( '錯誤: 找不到 GEMINI_API_KEY 環境變數。' ) ;
4535 return null ;
4636 }
47-
48- // 提取 <body> 內容以減少 token 使用並聚焦重點
49- const bodyMatch = htmlContent . match ( / < b o d y [ ^ > ] * > ( [ \s \S ] * ) < \/ b o d y > / i) ;
50- const contentToAnalyze = bodyMatch ? bodyMatch [ 1 ] : htmlContent ;
51-
52- const prompt = `
53- 請分析以下 HTML 內容,並以一個專業的產品經理的身份,為其生成一個簡潔且吸引人的專案介紹。
54- 你的目標是幫助訪客快速理解這個專案的核心價值。
55-
56- HTML 內容:
57- \`\`\`html
58- ${ contentToAnalyze . substring ( 0 , 3000 ) }
59- \`\`\`
60-
61- 請根據以上內容,提供一個 JSON 物件,包含以下三個鍵:
62- 1. "name": (string) 一個簡潔、響亮的中文專案名稱 (最多15個字)。
63- 2. "desc": (string) 一段引人入勝的中文描述 (最多60個字),說明這個專案是什麼,解決了什麼問題。
64- 3. "tags": (string[]) 一個包含3個相關中文關鍵字的陣列,便於分類。
65-
66- 請直接回傳格式正確的 JSON 物件,不要包含任何額外的解釋或 markdown 格式。
67- ` ;
68-
6937 try {
7038 const response = await fetch ( API_URL , {
7139 method : 'POST' ,
7240 headers : { 'Content-Type' : 'application/json' } ,
7341 body : JSON . stringify ( {
7442 contents : [ { parts : [ { text : prompt } ] } ] ,
75- generationConfig : {
76- responseMimeType : "application/json" ,
77- }
43+ generationConfig : { responseMimeType : "application/json" } ,
7844 } ) ,
7945 } ) ;
8046
8147 if ( ! response . ok ) {
8248 const errorBody = await response . text ( ) ;
83- console . error ( `Google API 請求失敗: ${ response . status } ${ response . statusText } ` , errorBody ) ;
49+ console . error ( `Google API 請求失敗: ${ response . status } ` , errorBody ) ;
8450 return null ;
8551 }
8652
8753 const data = await response . json ( ) ;
8854 const jsonString = data . candidates [ 0 ] . content . parts [ 0 ] . text ;
8955 return JSON . parse ( jsonString ) ;
90-
9156 } catch ( error ) {
9257 console . error ( '呼叫 Gemini API 時發生錯誤:' , error ) ;
9358 return null ;
9459 }
9560}
9661
97- /**
98- * 主執行函式
99- */
100- async function main ( ) {
101- console . log ( '🚀 開始執行專案自動更新腳本...' ) ;
62+ // --- 核心 Metadata 生成函式 ---
10263
103- // 1. 讀取現有元數據和專案檔案
104- const metadata = await readJsonFile ( METADATA_PATH ) ;
105- const projectFiles = await fs . readdir ( PROJECTS_DIR ) ;
106- const htmlFiles = projectFiles . filter ( file => file . endsWith ( '.html' ) ) ;
64+ // 「暴力破解」模式: 從頭分析整個檔案
65+ async function generateMetadata ( htmlContent ) {
66+ const bodyMatch = htmlContent . match ( / < b o d y [ ^ > ] * > ( [ \s \S ] * ) < \/ b o d y > / i) ;
67+ const contentToAnalyze = bodyMatch ? bodyMatch [ 1 ] : htmlContent ;
68+ console . log ( '💥 「暴力破解」模式:' ) ;
69+ console . log ( contentToAnalyze . substring ( 0 , 500 ) ) ;
70+ const prompt = `
71+ 請分析以下 HTML 內容,為其生成一個簡潔且吸引人的專案介紹。
10772
108- // 2. 找出新的專案檔案
109- const newFiles = htmlFiles . filter ( file => ! metadata [ file ] ) ;
73+ HTML 內容:
74+ \`\`\`html
75+ ${ contentToAnalyze . substring ( 0 , 4000 ) }
76+ \`\`\`
11077
111- if ( newFiles . length === 0 ) {
112- console . log ( '✅ 沒有偵測到新的專案檔案,無需更新。' ) ;
78+ 請提供一個 JSON 物件,包含 "name" (string, 最多15字), "desc" (string, 最多80字), "tags" (string[], 3個關鍵字)。
79+ 請直接回傳格式正確的 JSON 物件,不要包含任何額外的解釋或 markdown 格式。
80+ ` ;
81+ return callGeminiAPI ( prompt ) ;
82+ }
83+
84+ // 「手術刀」模式: 根據差異潤飾現有元數據
85+ async function generateRefinedMetadata ( oldMetadata , htmlDiff ) {
86+ console . log ( '「手術刀」模式:' ) ;
87+ console . log ( oldMetadata ) ;
88+ console . log ( htmlDiff ) ;
89+ console . log ( '=======================================' ) ;
90+
91+ const prompt = `
92+ 作為一個產品文案專家,請根據一個 HTML 檔案的內容變動,來優化它的元數據。
93+
94+ 這是【舊的元數據】:
95+ \`\`\`json
96+ ${ JSON . stringify ( oldMetadata , null , 2 ) }
97+ \`\`\`
98+
99+ 這是【HTML 檔案的變動內容】(以 diff 格式呈現):
100+ \`\`\`diff
101+ ${ htmlDiff . substring ( 0 , 4000 ) }
102+ \`\`\`
103+
104+ 任務:
105+ 請在舊元數據的基礎上,進行細微但精準的潤飾,以反映 HTML 的變動。
106+ - 如果變動很小 (例如修正錯字),則稍微調整文字使其更通順即可。
107+ - 如果變動增加了新功能,則在描述中簡要體現出來。
108+ - 保持風格和語氣的一致性。
109+
110+ JSON 物件,包含 "name" (string, 最多15字), "desc" (string, 最多80字), "tags" (string[], 3個關鍵字)。
111+
112+ 請回傳一個【優化後】的 JSON 物件,包含 "name", "desc", "tags"。格式需與舊元數據一致。
113+ 請直接回傳格式正確的 JSON 物件,不要包含任何額外的解釋或 markdown 格式。
114+ ` ;
115+ return callGeminiAPI ( prompt ) ;
116+ }
117+
118+ // --- Git 變動分析函式 ---
119+
120+ function getChangedFiles ( ) {
121+ // 獲取 HEAD 與其前一個 commit 之間的差異
122+ const output = execSync ( 'git diff --name-status HEAD~1 HEAD' ) . toString ( ) ;
123+ const files = output . split ( '\n' ) . filter ( Boolean ) . map ( line => {
124+ const [ status , filePath ] = line . split ( '\t' ) ;
125+ return { status, filePath } ;
126+ } ) ;
127+ return files . filter ( f => f . filePath && f . filePath . startsWith ( 'public/show/' ) ) ;
128+ }
129+
130+ function getDiffLineCount ( filePath ) {
131+ const output = execSync ( `git diff --numstat HEAD~1 HEAD -- ${ filePath } ` ) . toString ( ) ;
132+ const match = output . match ( / ^ ( \d + ) \s + ( \d + ) / ) ;
133+ return match ? parseInt ( match [ 1 ] ) + parseInt ( match [ 2 ] ) : 0 ;
134+ }
135+
136+ // --- 主執行函式 ---
137+
138+ async function main ( ) {
139+ console . log ( '🚀 開始執行差異化專案更新腳本...' ) ;
140+ const metadata = await readJsonFile ( METADATA_PATH ) ;
141+ const changedFiles = getChangedFiles ( ) ;
142+
143+ if ( changedFiles . length === 0 ) {
144+ console . log ( '✅ 沒有偵測到 public/show/ 中的檔案變動。' ) ;
113145 return ;
114146 }
115147
116- console . log ( `🔍 偵測到 ${ newFiles . length } 個新專案,開始處理...` ) ;
117- let updatedCount = 0 ;
148+ let hasChanges = false ;
118149
119- // 3. 為每個新檔案生成元數據並更新
120- for ( const file of newFiles ) {
121- console . log ( `📄 正在處理檔案: ${ file } ` ) ;
122- const filePath = path . join ( PROJECTS_DIR , file ) ;
123- const htmlContent = await fs . readFile ( filePath , 'utf8' ) ;
150+ for ( const { status, filePath } of changedFiles ) {
151+ const fileName = path . basename ( filePath ) ;
152+ console . log ( `\n📄 偵測到 [${ status } ] 狀態的檔案: ${ fileName } ` ) ;
124153
125- console . log ( '🧠 正在呼叫 Gemini API 生成元數據...' ) ;
126- const newMetadata = await generateMetadata ( htmlContent ) ;
154+ if ( status === 'D' ) { // 檔案被刪除
155+ if ( metadata [ fileName ] ) {
156+ delete metadata [ fileName ] ;
157+ hasChanges = true ;
158+ console . log ( `🗑️ 已從 metadata.json 中移除 ${ fileName } 。` ) ;
159+ }
160+ continue ;
161+ }
127162
128- if ( newMetadata && newMetadata . name && newMetadata . desc && newMetadata . tags ) {
129- metadata [ file ] = newMetadata ;
130- updatedCount ++ ;
131- console . log ( `✨ 成功生成元數據:` ) ;
132- console . log ( ` - 名稱: ${ newMetadata . name } ` ) ;
133- console . log ( ` - 描述: ${ newMetadata . desc } ` ) ;
134- console . log ( ` - 標籤: [${ newMetadata . tags . join ( ', ' ) } ]` ) ;
163+ const htmlContent = await fs . readFile ( filePath , 'utf8' ) ;
164+ let newMetadata = null ;
165+
166+ if ( status === 'A' ) { // 新增的檔案
167+ console . log ( '✨ 此為新檔案,使用「暴力破解」模式生成全新元數據...' ) ;
168+ newMetadata = await generateMetadata ( htmlContent ) ;
169+ } else if ( status === 'M' ) { // 修改的檔案
170+ const lineCount = getDiffLineCount ( filePath ) ;
171+ console . log ( `📊 變動行數: ${ lineCount } ` ) ;
172+
173+ if ( lineCount >= 10 ) {
174+ console . log ( '💥 變動較大,使用「暴力破解」模式重新生成元數據...' ) ;
175+ newMetadata = await generateMetadata ( htmlContent ) ;
176+ } else {
177+ console . log ( '🔪 變動較小,使用「手術刀」模式潤飾元數據...' ) ;
178+ const oldMetadata = metadata [ fileName ] ;
179+ if ( oldMetadata ) {
180+ const htmlDiff = execSync ( `git diff HEAD~1 HEAD -- ${ filePath } ` ) . toString ( ) ;
181+ newMetadata = await generateRefinedMetadata ( oldMetadata , htmlDiff ) ;
182+ } else {
183+ console . warn ( `⚠️ 找不到 ${ fileName } 的舊元數據,將改用「暴力破解」模式。` ) ;
184+ newMetadata = await generateMetadata ( htmlContent ) ;
185+ }
186+ }
187+ }
188+
189+ if ( newMetadata ) {
190+ metadata [ fileName ] = newMetadata ;
191+ hasChanges = true ;
192+ console . log ( `💡 成功為 ${ fileName } 生成/更新元數據。` ) ;
135193 } else {
136- console . warn ( `⚠️ 無法為 ${ file } 生成元數據,將跳過此檔案 。` ) ;
194+ console . warn ( `⚠️ 未能為 ${ fileName } 處理元數據 。` ) ;
137195 }
138196 }
139197
140- // 4. 如果有更新,則寫回 metadata.json
141- if ( updatedCount > 0 ) {
198+ if ( hasChanges ) {
142199 await fs . writeFile ( METADATA_PATH , JSON . stringify ( metadata , null , 2 ) , 'utf8' ) ;
143- console . log ( `💾 成功將 ${ updatedCount } 個新專案的元數據寫入 metadata.json!` ) ;
144- console . log ( '🎉 自動化更新完成!' ) ;
200+ console . log ( '\n💾 成功將更新寫入 metadata.json!' ) ;
145201 } else {
146- console . log ( ` 🤷♂️ 本次執行沒有成功更新任何專案的元數據。` ) ;
202+ console . log ( '\n 🤷♂️ 本次執行沒有對 metadata.json 產生任何變更。' ) ;
147203 }
204+
205+ console . log ( '🎉 腳本執行完畢!' ) ;
148206}
149207
150208main ( ) . catch ( error => {
0 commit comments