Skip to content

Commit ff057c4

Browse files
committed
Refactor auto-update workflow and metadata logic
Improved the GitHub Actions workflow for auto-updating metadata by adding concurrency control and preventing infinite CI loops. Enhanced the update-projects.js script to support diff-based metadata refinement and robust file change detection. Updated metadata.json for test-project.html to reflect new project scope and tags. Removed entry overlay and streamlined music/effect initialization in index.html.
1 parent d068c4c commit ff057c4

File tree

3 files changed

+154
-105
lines changed

3 files changed

+154
-105
lines changed

.github/workflows/auto-update-metadata.yml

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ on:
55
push:
66
branches:
77
- main
8-
paths:
9-
- 'public/show/**.html' # 只在 show 資料夾有新 html 檔案時觸發
8+
9+
# 確保同一個分支上,一次只有一個工作流程在運行
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
1013

1114
jobs:
1215
update-metadata:
@@ -18,38 +21,36 @@ jobs:
1821
- name: Checkout repository
1922
uses: actions/checkout@v4
2023
with:
24+
# 獲取所有歷史紀錄,以便腳本可以比較 commits
2125
fetch-depth: 0
2226

2327
- name: Set up Node.js
2428
uses: actions/setup-node@v4
2529
with:
26-
node-version: '20' # 使用一個穩定的 LTS 版本
30+
node-version: '20'
2731
cache: 'npm'
2832

2933
- name: Install dependencies
3034
run: npm install
3135

3236
- name: Run update script
33-
run: npm run update
37+
id: run_script
38+
run: node scripts/update-projects.js
3439
env:
3540
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
3641

3742
- name: Commit and Push Results
38-
env:
39-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4043
run: |
4144
git config --global user.name "github-actions[bot]"
4245
git config --global user.email "github-actions[bot]@users.noreply.github.com"
43-
44-
# 拉取最新的程式碼以避免衝突
45-
git pull origin main
46-
47-
# 檢查是否有變更需要提交
46+
47+
# 只需要檢查 metadata.json 是否有變動
4848
if git diff --quiet metadata.json; then
4949
echo "✅ metadata.json is up to date. No changes to commit."
5050
else
5151
git add metadata.json
52-
git commit -m "chore(bot): auto-update project metadata [CI]"
53-
git push https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git HEAD:main
52+
# 在 commit message 中加入 [skip ci] 來防止無限循環
53+
git commit -m "chore(bot): auto-update project metadata [skip ci]"
54+
git push
5455
echo "🚀 Committed and pushed updated metadata.json"
5556
fi

public/index.html

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,7 @@
195195
<!-- 天氣效果畫布 -->
196196
<canvas id="weather-canvas"></canvas>
197197

198-
<!-- 初始載入畫面 -->
199-
<div id="entry-overlay">
200-
<h1 class="text-4xl md:text-6xl font-black uppercase tracking-widest themed-title mb-8">晝夜協奏曲</h1>
201-
<button id="entry-button" class="px-8 py-3 rounded-full font-bold themed-button text-lg">啟動感官</button>
202-
</div>
198+
203199

204200

205201

@@ -246,8 +242,6 @@ <h1 id="main-title" class="text-5xl md:text-7xl font-black uppercase tracking-wi
246242
document.addEventListener('DOMContentLoaded', () => {
247243
// --- 1. 獲取所有需要的 DOM 元素與全域變數 ---
248244
const body = document.body;
249-
const entryOverlay = document.getElementById('entry-overlay');
250-
const entryButton = document.getElementById('entry-button');
251245
const themeToggleButton = document.getElementById('theme-toggle-button');
252246
const sunIcon = document.getElementById('sun-icon');
253247
const moonIcon = document.getElementById('moon-icon');
@@ -403,18 +397,14 @@ <h2 class="text-2xl font-bold themed-title mb-2 font-serif group-hover:text-[var
403397
renderProjects(allProjects.filter(p => searchInput.value ? p.name.toLowerCase().includes(searchInput.value.toLowerCase()) : true));
404398
}
405399

406-
// --- 5. 事件監聽器 ---
407-
entryButton.addEventListener('click', () => {
408-
entryOverlay.classList.add('hidden');
409-
nightMusic.play().catch(e => console.log("Music auto-play prevented."));
410-
startRain();
411-
}, { once: true });
412-
413400
themeToggleButton.addEventListener('click', toggleTheme);
414401

415402
// --- 6. 初始化頁面 ---
416403
fetchProjects();
417404
setupSearch();
405+
// 嘗試自動播放音樂與啟動效果
406+
nightMusic.play().catch(e => console.log("音樂自動播放被瀏覽器阻止,需要使用者互動。"));
407+
startRain();
418408
});
419409
</script>
420410
</body>

scripts/update-projects.js

Lines changed: 136 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,147 +4,205 @@ import path from 'path';
44
import { fileURLToPath } from 'url';
55
import fetch from 'node-fetch';
66
import 'dotenv/config';
7+
import { execSync } from 'child_process';
78

89
// --- 檔案路徑設定 ---
910
const __filename = fileURLToPath(import.meta.url);
1011
const __dirname = path.dirname(__filename);
11-
const ROOT_DIR = path.resolve(__dirname, '..'); // 專案根目錄
12+
const ROOT_DIR = path.resolve(__dirname, '..');
1213
const PROJECTS_DIR = path.join(ROOT_DIR, 'public', 'show');
1314
const METADATA_PATH = path.join(ROOT_DIR, 'metadata.json');
1415

1516
// --- Gemini API 設定 ---
1617
const API_KEY = process.env.GEMINI_API_KEY;
1718
const 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+
2422
async 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(/<body[^>]*>([\s\S]*)<\/body>/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(/<body[^>]*>([\s\S]*)<\/body>/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

150208
main().catch(error => {

0 commit comments

Comments
 (0)