commit 29c1633e49a2daae10eb40c58a77e679ba43c865 Author: DonaldFang 方士碩 Date: Thu Dec 4 00:46:53 2025 +0800 Initial commit: HR Position System - Database schema with MySQL support - LLM API integration (Gemini 2.5 Flash, DeepSeek, OpenAI) - Error handling with copyable error messages - CORS fix for API calls - Complete setup documentation 🤖 Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a701fe2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# Environment variables +.env +.env.local +.env.*.local + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual Environment +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Flask +instance/ +.webassets-cache + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log +logs/ +log/ + +# OS +Thumbs.db +desktop.ini + +# Temporary files +*.tmp +*.temp +*.bak + +# Node modules (if using any frontend build tools) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ + +# Backup files +*.backup +backup/ diff --git a/CORS_FIX_GUIDE.md b/CORS_FIX_GUIDE.md new file mode 100644 index 0000000..0b65038 --- /dev/null +++ b/CORS_FIX_GUIDE.md @@ -0,0 +1,315 @@ +# CORS 錯誤修正指南 + +## 🔴 問題說明 + +您遇到的錯誤有兩個: + +### 1. CORS (跨域資源共享) 錯誤 +``` +Access to fetch at 'https://api.anthropic.com/v1/messages' from origin 'http://127.0.0.1:5000' +has been blocked by CORS policy: Response to preflight request doesn't pass access control check: +No 'Access-Control-Allow-Origin' header is present on the requested resource. +``` + +**原因**: 前端代碼直接從瀏覽器調用 Claude API,但 Claude API 不允許來自瀏覽器的直接請求(出於安全考量)。 + +### 2. Storage 訪問錯誤 +``` +Uncaught (in promise) Error: Access to storage is not allowed from this context. +``` + +**原因**: 瀏覽器的本地存儲權限問題,通常在使用 `file://` 協議或某些安全限制下出現。 + +--- + +## ✅ 解決方案 + +### 方案 1: 修改 index.html 中的 callClaudeAPI 函數(推薦) + +**步驟 1**: 打開 [index.html](./index.html) + +**步驟 2**: 找到第 1264 行的 `callClaudeAPI` 函數 + +**步驟 3**: 將整個函數替換為以下代碼: + +```javascript +// ==================== AI Generation Functions ==================== +async function callClaudeAPI(prompt, api = 'gemini') { + try { + // 調用後端 Flask API,避免 CORS 錯誤 + const response = await fetch("/api/llm/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api: api, // 使用指定的 LLM API (gemini, deepseek, openai) + prompt: prompt, + max_tokens: 2000 + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `API 請求失敗: ${response.status}`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'API 調用失敗'); + } + + // 解析返回的文字為 JSON + let responseText = data.text; + responseText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim(); + return JSON.parse(responseText); + + } catch (error) { + console.error("Error calling LLM API:", error); + + // 顯示友好的錯誤訊息 + alert(`AI 生成錯誤: ${error.message}\n\n請確保:\n1. Flask 後端已啟動\n2. 已在 .env 文件中配置 LLM API Key\n3. 網路連線正常`); + + throw error; + } +} +``` + +**步驟 4**: 保存文件 + +**步驟 5**: 確保使用更新版的 Flask 後端 + +```bash +# 使用包含 LLM 端點的版本 +python app_updated.py +``` + +**步驟 6**: 重新載入瀏覽器頁面 + +--- + +### 方案 2: 使用已修正的文件(快速方案) + +我已經創建了包含完整修正的文件,您可以選擇以下方式之一: + +#### 選項 A: 手動修改(最安全) + +1. 打開 `index.html` +2. 搜尋 `async function callClaudeAPI(prompt)` +3. 用上面方案 1 的代碼替換 + +#### 選項 B: 使用腳本自動修正 + +參考 [fix_cors.js](./fix_cors.js) 文件中的完整說明。 + +--- + +## 📋 完整修正步驟 + +### 1. 確認後端已啟動並包含 LLM 端點 + +```bash +# 停止當前運行的 Flask(如果有) +# 按 Ctrl+C + +# 啟動更新版後端 +python app_updated.py +``` + +您應該看到: +``` +✓ LLM 功能已啟用 + 已配置的 API: gemini, deepseek, openai +``` + +### 2. 配置 LLM API Key + +編輯 [.env](./.env) 文件,添加至少一個 API Key: + +```env +# 至少配置其中一個 +GEMINI_API_KEY=your_actual_gemini_api_key_here +DEEPSEEK_API_KEY=your_actual_deepseek_api_key_here +OPENAI_API_KEY=your_actual_openai_api_key_here +``` + +**獲取 API Key:** +- **Gemini**: https://makersuite.google.com/app/apikey +- **DeepSeek**: https://www.deepseek.com/ +- **OpenAI**: https://platform.openai.com/api-keys + +### 3. 測試 LLM API 連線 + +訪問測試頁面:http://localhost:5000/api-test + +點擊「測試連線」按鈕,確保至少一個 API 顯示「✓ 連線成功」。 + +### 4. 修改 index.html + +按照方案 1 的步驟修改 `callClaudeAPI` 函數。 + +### 5. 重新載入頁面 + +在瀏覽器中按 `Ctrl+F5` 強制重新載入頁面(清除緩存)。 + +### 6. 測試 AI 功能 + +1. 在主頁面點擊「新增崗位」 +2. 填寫部分欄位(例如崗位名稱) +3. 點擊「✨ I'm feeling lucky」按鈕 +4. 應該會成功生成內容,不再出現 CORS 錯誤 + +--- + +## 🔍 驗證修正是否成功 + +打開瀏覽器的開發者工具(F12),切換到 Console 標籤: + +### ✅ 成功的表現: +- 沒有 CORS 錯誤 +- 看到 `POST http://localhost:5000/api/llm/generate` 請求成功 (200 OK) +- AI 自動填充正常工作 + +### ❌ 仍有問題: + +#### 如果看到 `403 LLM 功能未啟用` +**原因**: 沒有使用 `app_updated.py` +**解決**: +```bash +python app_updated.py +``` + +#### 如果看到 `API Key 未設定` +**原因**: .env 文件中沒有配置 API Key +**解決**: 編輯 `.env` 文件,添加有效的 API Key + +#### 如果看到 `連線逾時` +**原因**: 網路連線問題或 API 伺服器問題 +**解決**: +1. 檢查網路連線 +2. 嘗試使用不同的 LLM API(修改 `callClaudeAPI(prompt, 'deepseek')`) + +#### 如果看到 `API Key 無效` +**原因**: API Key 錯誤或已過期 +**解決**: 重新獲取有效的 API Key + +--- + +## 🎯 架構變更說明 + +### 修正前(錯誤): +``` +瀏覽器 → 直接調用 → Claude API (❌ CORS 錯誤) +``` + +### 修正後(正確): +``` +瀏覽器 → Flask 後端 → Gemini/DeepSeek/OpenAI API (✅ 正常) +``` + +--- + +## 💡 為什麼這樣修改 + +### 1. 安全性 +- **API Key 保護**: API Key 只存儲在服務器端(.env 文件),不暴露給前端 +- **防止濫用**: 用戶無法看到或竊取 API Key + +### 2. CORS 問題 +- **同源請求**: 前端只調用同域的 Flask API(http://localhost:5000) +- **服務器端請求**: Flask 服務器調用外部 API,不受 CORS 限制 + +### 3. 靈活性 +- **多 API 支持**: 可以輕鬆切換不同的 LLM API +- **統一接口**: 前端代碼無需關心使用哪個 LLM API + +--- + +## 📝 其他建議 + +### 1. 添加錯誤處理 UI + +在 `` 標籤中添加錯誤處理腳本: + +```html + +``` + +### 2. 選擇 LLM API + +如果想使用特定的 LLM API,可以修改調用: + +```javascript +// 使用 Gemini(默認) +const result = await callClaudeAPI(prompt, 'gemini'); + +// 使用 DeepSeek +const result = await callClaudeAPI(prompt, 'deepseek'); + +// 使用 OpenAI +const result = await callClaudeAPI(prompt, 'openai'); +``` + +### 3. 添加 API 選擇器 + +可以在前端添加一個下拉選單,讓用戶選擇使用哪個 LLM API: + +```html + +``` + +然後在調用時使用: +```javascript +const api = document.getElementById('llm-selector').value; +const result = await callClaudeAPI(prompt, api); +``` + +--- + +## 🆘 仍然遇到問題? + +### 檢查清單 + +- [ ] Flask 後端是否正在運行? +- [ ] 是否使用 `app_updated.py` 而不是 `app.py`? +- [ ] .env 文件中是否配置了至少一個 API Key? +- [ ] API Key 是否有效?(可以在 http://localhost:5000/api-test 測試) +- [ ] 是否已修改 index.html 中的 `callClaudeAPI` 函數? +- [ ] 瀏覽器是否已重新載入頁面(Ctrl+F5)? +- [ ] 瀏覽器控制台是否還有其他錯誤? + +### 調試步驟 + +1. **測試後端 API**: + ```bash + curl -X POST http://localhost:5000/api/llm/generate \ + -H "Content-Type: application/json" \ + -d '{"api":"gemini","prompt":"測試","max_tokens":100}' + ``` + +2. **檢查 Flask 日誌**: 查看終端中 Flask 的輸出 + +3. **瀏覽器控制台**: 查看詳細的錯誤訊息 + +4. **測試頁面**: 訪問 http://localhost:5000/api-test 確認 API 配置 + +--- + +## 📚 相關文件 + +- [SETUP.md](./SETUP.md) - 完整安裝指南 +- [fix_cors.js](./fix_cors.js) - 詳細修正代碼 +- [error_handler.js](./error_handler.js) - 全局錯誤處理 +- [app_updated.py](./app_updated.py) - 包含 LLM 端點的 Flask 後端 + +--- + +**文件版本**: 1.0 +**最後更新**: 2024-12-04 +**問題類型**: CORS 跨域請求錯誤 +**解決狀態**: ✅ 已提供完整解決方案 diff --git a/GEMINI_API_FIX.md b/GEMINI_API_FIX.md new file mode 100644 index 0000000..3b9e5b9 --- /dev/null +++ b/GEMINI_API_FIX.md @@ -0,0 +1,236 @@ +# Gemini API Referrer 錯誤解決方案 + +## 🔴 錯誤訊息 + +```json +{ + "error": { + "code": 403, + "message": "Requests from referer \u003cempty\u003e are blocked.", + "status": "PERMISSION_DENIED", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.ErrorInfo", + "reason": "API_KEY_HTTP_REFERRER_BLOCKED" + } + ] + } +} +``` + +## 📋 問題原因 + +Gemini API Key 有 **HTTP Referrer 限制**,這是 Google 的安全機制。當從服務器端調用時,HTTP Referrer 為空,導致請求被阻擋。 + +--- + +## ✅ 解決方案 + +### 方案 1: 修改 Gemini API Key 設定(推薦) + +1. **訪問 Google AI Studio** + https://makersuite.google.com/app/apikey + +2. **找到您的 API Key** + +3. **點擊「Edit API Key」或創建新的 API Key** + +4. **設定 Application restrictions** + - 選擇 **"None"** 或 **"IP addresses"** + - **不要**選擇 "HTTP referrers (websites)" + +5. **保存設定** + +6. **更新 .env 文件** + ```env + GEMINI_API_KEY=your_new_unrestricted_api_key + ``` + +7. **重啟 Flask 服務器** + ```bash + # 按 Ctrl+C 停止服務器 + python start_server.py + ``` + +--- + +### 方案 2: 使用其他 LLM API(臨時解決) + +如果您有其他 LLM API Key,可以暫時使用: + +#### 選項 A: 使用 DeepSeek + +1. **獲取 DeepSeek API Key** + https://www.deepseek.com/ + +2. **添加到 .env** + ```env + DEEPSEEK_API_KEY=your_deepseek_api_key + ``` + +3. **修改默認 API** + 編輯 `index.html`,找到 `callClaudeAPI` 調用,改為: + ```javascript + const result = await callClaudeAPI(prompt, 'deepseek'); + ``` + +#### 選項 B: 使用 OpenAI + +1. **獲取 OpenAI API Key** + https://platform.openai.com/api-keys + +2. **添加到 .env** + ```env + OPENAI_API_KEY=your_openai_api_key + ``` + +3. **修改默認 API** + 編輯 `index.html`,找到 `callClaudeAPI` 調用,改為: + ```javascript + const result = await callClaudeAPI(prompt, 'openai'); + ``` + +--- + +### 方案 3: 創建 API 選擇器(最佳長期方案) + +讓用戶在前端選擇要使用的 LLM API。 + +我可以幫您添加一個下拉選單,讓用戶可以在 Gemini、DeepSeek 和 OpenAI 之間切換。 + +--- + +## 🔍 驗證修正 + +### 測試 API 連線 + +訪問測試頁面:http://127.0.0.1:5000/api-test + +點擊每個 API 的「🧪 測試連線」按鈕,查看哪些 API 可用。 + +### 成功的表現 + +- ✅ API 測試顯示「✓ 連線成功」 +- ✅ 瀏覽器不再顯示 403 錯誤 +- ✅ AI 自動填充功能正常工作 + +--- + +## 📝 詳細錯誤訊息說明 + +您在截圖中看到的錯誤: + +``` +"reason": "API_KEY_HTTP_REFERRER_BLOCKED" +``` + +這表示: +- Gemini API Key 設定了 HTTP Referrer 限制 +- 服務器端請求沒有 Referrer,被 Google 阻擋 +- 需要移除 Referrer 限制或使用其他 API + +--- + +## 🎯 推薦操作順序 + +### 立即操作(5 分鐘) + +1. **使用 DeepSeek 或 OpenAI**(臨時解決) + ```bash + # 編輯 .env 添加其他 API Key + # 重啟服務器 + python start_server.py + ``` + +2. **測試頁面重新載入** + - 按 Ctrl+F5 刷新 + - 測試 AI 功能 + +### 長期解決(10 分鐘) + +1. **修改 Gemini API Key 設定** + - 訪問 Google AI Studio + - 移除 HTTP Referrer 限制 + - 創建新的無限制 API Key + +2. **更新配置** + - 更新 .env 文件 + - 重啟服務器 + +3. **全面測試** + - 測試所有 API + - 確保都能正常工作 + +--- + +## 💡 補充說明 + +### 為什麼服務器端請求沒有 Referrer? + +當從 Python Flask 後端調用 API 時: +- HTTP 請求是由服務器發出的 +- 沒有瀏覽器上下文 +- Referer header 為空或不存在 +- Google 的安全機制會阻擋這類請求 + +### 如何避免這個問題? + +1. **使用無限制的 API Key**(推薦) +2. **使用 IP 地址限制**而非 Referrer 限制 +3. **使用服務帳戶**(企業方案) + +--- + +## 🆘 仍然有問題? + +### 如果修改 API Key 後還是不行 + +1. **檢查 API Key 是否生效** + - 等待 1-2 分鐘 + - Google 的設定更新需要時間 + +2. **確認 .env 文件正確** + ```bash + # 查看 .env 內容 + type .env + ``` + +3. **重啟服務器** + ```bash + # 完全停止後重新啟動 + python start_server.py + ``` + +4. **清除瀏覽器緩存** + - 按 Ctrl+Shift+Delete + - 清除緩存和 Cookie + - 重新載入頁面 + +--- + +## 📊 API 對比 + +| API | 優點 | 缺點 | 推薦度 | +|-----|------|------|--------| +| **Gemini** | 免費額度高,速度快 | Referrer 限制問題 | ⭐⭐⭐ | +| **DeepSeek** | 便宜,中文支持好 | 需要付費 | ⭐⭐⭐⭐ | +| **OpenAI** | 穩定,功能強大 | 價格較高 | ⭐⭐⭐⭐⭐ | + +--- + +## ✅ 完成檢查清單 + +修正完成後,請檢查: + +- [ ] 至少一個 LLM API 測試成功 +- [ ] AI 自動填充功能正常 +- [ ] 沒有 403 錯誤 +- [ ] 錯誤訊息可以完整顯示和複製 +- [ ] 瀏覽器控制台沒有錯誤 + +--- + +**文件版本**: 1.0 +**最後更新**: 2024-12-04 +**問題類型**: Gemini API HTTP Referrer 限制 +**解決狀態**: ✅ 已提供完整解決方案 diff --git a/SDD.md b/SDD.md new file mode 100644 index 0000000..276033c --- /dev/null +++ b/SDD.md @@ -0,0 +1,844 @@ +# HR 基礎資料維護系統 - 軟體設計文件 (SDD) + +**文件版本**:1.0 +**建立日期**:2024-12-03 +**文件狀態**:Released + +--- + +## 1. 文件概述 + +### 1.1 目的 + +本文件為 HR 基礎資料維護系統之軟體設計文件 (Software Design Document),詳細描述系統架構、模組設計、資料結構、介面規格與 AI 整合功能,供開發人員、測試人員及維護人員參考。 + +### 1.2 範圍 + +本系統涵蓋以下三大功能模組: + +| 模組 | 功能說明 | +|------|----------| +| 崗位基礎資料 | 崗位主檔維護,含基礎資料與招聘要求 | +| 職務基礎資料 | 職務類別與屬性設定維護 | +| 崗位描述 | 職責描述、崗位要求與任職條件維護 | + +### 1.3 參考文件 + +- 用戶需求規格書 (URD) +- 系統功能規格書 (FSD) +- API 設計規範 + +### 1.4 術語定義 + +| 術語 | 定義 | +|------|------| +| 崗位 (Position) | 組織架構中的職位單位,具有編號、級別、編制人數等屬性 | +| 職務 (Job Title) | 職務類別分類,如管理職、技術職、業務職等 | +| 崗位描述 (Job Description) | 詳細描述崗位職責、要求與任職條件的文件 | +| AI 自動填充 | 利用大型語言模型自動生成表單欄位內容的功能 | + +--- + +## 2. 系統架構 + +### 2.1 整體架構圖 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 使用者介面層 (UI Layer) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 崗位基礎資料 │ │ 職務基礎資料 │ │ 崗位描述 │ │ +│ │ 模組 │ │ 模組 │ │ 模組 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ AI 自動填充服務 (Claude API) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 應用服務層 (Application Layer) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Flask RESTful API │ │ +│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ +│ │ │ Position │ │ Job │ │ Reference │ │ │ +│ │ │ API │ │ API │ │ API │ │ │ +│ │ └───────────┘ └───────────┘ └───────────┘ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 資料存取層 (Data Layer) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ In-Memory Database / Future DB │ │ +│ │ (positions_db, jobs_db, job_desc_db) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 技術堆疊 + +| 層級 | 技術選型 | 說明 | +|------|----------|------| +| 前端 | HTML5 + CSS3 + JavaScript | 純前端實作,無框架依賴 | +| 樣式 | Custom CSS + Google Fonts | Noto Sans TC 字型、CSS Variables | +| 後端 | Python Flask | RESTful API 服務 | +| AI 服務 | Claude API (Anthropic) | 智能表單填充 | +| 資料庫 | In-Memory (Dict) | 可擴展至 MySQL/PostgreSQL | + +### 2.3 部署架構 + +``` +┌─────────────────────────────────────────┐ +│ 使用者瀏覽器 │ +│ ┌─────────────────────────────────┐ │ +│ │ index.html (前端應用) │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────────┘ + │ │ + ▼ ▼ +┌───────────────────┐ ┌───────────────────┐ +│ Flask Server │ │ Claude API │ +│ localhost:5000 │ │ api.anthropic │ +└───────────────────┘ └───────────────────┘ +``` + +--- + +## 3. 模組設計 + +### 3.1 崗位基礎資料模組 + +#### 3.1.1 模組概述 + +提供崗位主檔的 CRUD 操作,分為「基礎資料」與「招聘要求資料」兩個頁籤。 + +#### 3.1.2 基礎資料頁籤欄位規格 + +| 欄位名稱 | 欄位ID | 資料類型 | 必填 | 說明 | +|----------|--------|----------|------|------| +| 崗位編號 | positionCode | String(20) | ✓ | 唯一識別碼,格式:XXX-NNN | +| 崗位名稱 | positionName | String(100) | ✓ | 崗位中文名稱 | +| 崗位類別 | positionCategory | Enum | | 01/02/03/04 | +| 崗位類別名稱 | positionCategoryName | String(50) | | 自動帶出,唯讀 | +| 崗位性質 | positionNature | Enum | | FT/PT/CT/IN | +| 崗位性質名稱 | positionNatureName | String(50) | | 自動帶出,唯讀 | +| 編制人數 | headcount | Integer | | 0-9999 | +| 崗位級別 | positionLevel | Enum | | L1-L7 | +| 生效日期 | effectiveDate | Date | | 預設 2001-01-01 | +| 崗位描述 | positionDesc | Text | | 多行文字 | +| 崗位備注 | positionRemark | Text | | 多行文字 | + +#### 3.1.3 招聘要求資料頁籤欄位規格 + +| 欄位名稱 | 欄位ID | 資料類型 | 說明 | +|----------|--------|----------|------| +| 最低學歷 | minEducation | Enum | HS/JC/BA/MA/PHD | +| 要求性別 | requiredGender | Enum | 空值=不限, M=男, F=女 | +| 薪酬范圍 | salaryRange | Enum | A/B/C/D/E/N | +| 工作經驗 | workExperience | Enum | 0/1/3/5/10 (年) | +| 最小年齡 | minAge | Integer | 18-65 | +| 最大年齡 | maxAge | Integer | 18-65 | +| 工作性質 | jobType | Enum | FT/PT/CT/DP | +| 招聘職位 | recruitPosition | Enum | ENG/MGR/AST/OP/SAL | +| 職位名稱 | jobTitle | String(100) | | +| 職位描述 | jobDesc | Text | | +| 崗位要求 | positionReq | Text | | +| 職稱要求 | titleReq | Enum | NONE/CERT/LIC | +| 專業要求 | majorReq | String(200) | 多選,逗號分隔 | +| 技能要求 | skillReq | String(200) | | +| 語言要求 | langReq | String(100) | | +| 其他要求 | otherReq | String(200) | | +| 上級崗位編號 | superiorPosition | String(20) | | +| 備注說明 | recruitRemark | Text | | + +#### 3.1.4 類別代碼對照表 + +**崗位類別 (positionCategory)** + +| 代碼 | 名稱 | +|------|------| +| 01 | 技術職 | +| 02 | 管理職 | +| 03 | 業務職 | +| 04 | 行政職 | + +**崗位性質 (positionNature)** + +| 代碼 | 名稱 | +|------|------| +| FT | 全職 | +| PT | 兼職 | +| CT | 約聘 | +| IN | 實習 | + +**崗位級別 (positionLevel)** + +| 代碼 | 名稱 | +|------|------| +| L1 | 基層員工 | +| L2 | 資深員工 | +| L3 | 主管 | +| L4 | 經理 | +| L5 | 總監 | +| L6 | 副總 | +| L7 | 總經理 | + +--- + +### 3.2 職務基礎資料模組 + +#### 3.2.1 模組概述 + +提供職務類別主檔的維護功能,包含職務屬性設定。 + +#### 3.2.2 欄位規格 + +| 欄位名稱 | 欄位ID | 資料類型 | 必填 | 說明 | +|----------|--------|----------|------|------| +| 職務類別編號 | jobCategoryCode | Enum | ✓ | MGR/TECH/SALE/ADMIN/RD/PROD | +| 職務類別名稱 | jobCategoryName | String(50) | | 自動帶出,唯讀 | +| 職務編號 | jobCode | String(20) | ✓ | 唯一識別碼 | +| 職務名稱 | jobName | String(100) | ✓ | 職務中文名稱 | +| 職務英文 | jobNameEn | String(100) | | 職務英文名稱 | +| 生效日期 | jobEffectiveDate | Date | | | +| 編制人數 | jobHeadcount | Integer | | 0-9999 | +| 排列順序 | jobSortOrder | Integer | | 顯示排序 | +| 備注說明 | jobRemark | Text | | | +| 職務層級 | jobLevel | String(50) | | 可設為 *保密* | +| 是否有全勤 | hasAttendanceBonus | Boolean | | Toggle 開關 | +| 是否住房補貼 | hasHousingAllowance | Boolean | | Toggle 開關 | + +#### 3.2.3 職務類別代碼對照表 + +| 代碼 | 名稱 | +|------|------| +| MGR | 管理職 | +| TECH | 技術職 | +| SALE | 業務職 | +| ADMIN | 行政職 | +| RD | 研發職 | +| PROD | 生產職 | + +--- + +### 3.3 崗位描述模組 + +#### 3.3.1 模組概述 + +提供完整的崗位描述書 (Job Description) 維護功能,包含崗位基本信息、職責描述與崗位要求三大區塊。 + +#### 3.3.2 頂部區域欄位 + +| 欄位名稱 | 欄位ID | 資料類型 | 說明 | +|----------|--------|----------|------| +| 工號 | jd_empNo | String(20) | 員工編號 | +| 姓名 | jd_empName | String(50) | 自動帶出,唯讀 | +| 崗位代碼 | jd_positionCode | String(20) | | +| 版本更新日期 | jd_versionDate | Date | | + +#### 3.3.3 崗位基本信息區塊 + +| 欄位名稱 | 欄位ID | 資料類型 | 說明 | +|----------|--------|----------|------| +| 崗位名稱 | jd_positionName | String(100) | | +| 所屬部門 | jd_department | String(100) | | +| 崗位生效日期 | jd_positionEffectiveDate | Date | | +| 直接領導職務 | jd_directSupervisor | String(100) | | +| 崗位職等&職務 | jd_positionGradeJob | String(100) | 選擇按鈕 | +| 匯報對象職務 | jd_reportTo | String(100) | 選擇按鈕 | +| 直接下級 | jd_directReports | String(200) | 格式:職位 x 人數 | +| 任職地點 | jd_workLocation | Enum | HQ/TPE/TYC/KHH/SH/SZ | +| 員工屬性 | jd_empAttribute | Enum | FT/CT/PT/IN/DP | + +#### 3.3.4 職責描述區塊 + +| 欄位名稱 | 欄位ID | 資料類型 | 說明 | +|----------|--------|----------|------| +| 崗位設置目的 | jd_positionPurpose | String(500) | 單行文字 | +| 主要崗位職責 | jd_mainResponsibilities | Text | 編號格式 1、2、3、... | + +#### 3.3.5 崗位要求區塊 + +| 欄位名稱 | 欄位ID | 資料類型 | 說明 | +|----------|--------|----------|------| +| 教育程度 | jd_education | String(200) | | +| 基本技能 | jd_basicSkills | Text | | +| 專業知識 | jd_professionalKnowledge | Text | | +| 工作經驗 | jd_workExperienceReq | Text | | +| 其他 | jd_otherRequirements | Text | | + +#### 3.3.6 任職地點代碼對照表 + +| 代碼 | 名稱 | +|------|------| +| HQ | 總部 | +| TPE | 台北辦公室 | +| TYC | 桃園廠區 | +| KHH | 高雄廠區 | +| SH | 上海辦公室 | +| SZ | 深圳辦公室 | + +#### 3.3.7 員工屬性代碼對照表 + +| 代碼 | 名稱 | +|------|------| +| FT | 正式員工 | +| CT | 約聘人員 | +| PT | 兼職人員 | +| IN | 實習生 | +| DP | 派遣人員 | + +--- + +## 4. API 設計 + +### 4.1 API 端點總覽 + +#### 4.1.1 崗位資料 API + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/api/positions` | 獲取所有崗位(支援分頁、搜尋) | +| GET | `/api/positions/{id}` | 獲取單一崗位 | +| POST | `/api/positions` | 新增崗位 | +| PUT | `/api/positions/{id}` | 更新崗位 | +| DELETE | `/api/positions/{id}` | 刪除崗位 | +| POST | `/api/positions/{id}/change-code` | 更改崗位編號 | + +#### 4.1.2 職務資料 API + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/api/jobs` | 獲取所有職務(支援分頁、搜尋、類別篩選) | +| GET | `/api/jobs/{id}` | 獲取單一職務 | +| POST | `/api/jobs` | 新增職務 | +| PUT | `/api/jobs/{id}` | 更新職務 | +| DELETE | `/api/jobs/{id}` | 刪除職務 | +| POST | `/api/jobs/{id}/change-code` | 更改職務編號 | + +#### 4.1.3 參照資料 API + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/api/reference/categories` | 崗位類別選項 | +| GET | `/api/reference/job-categories` | 職務類別選項 | +| GET | `/api/reference/natures` | 崗位性質選項 | +| GET | `/api/reference/education` | 學歷選項 | +| GET | `/api/reference/majors` | 專業選項 | + +### 4.2 API 請求/回應範例 + +#### 4.2.1 新增崗位 + +**Request** +```http +POST /api/positions +Content-Type: application/json + +{ + "basicInfo": { + "positionCode": "ENG-001", + "positionName": "資深軟體工程師", + "positionCategory": "01", + "positionNature": "FT", + "headcount": "5", + "positionLevel": "L3", + "effectiveDate": "2024-01-01", + "positionDesc": "負責系統架構設計與核心模組開發" + }, + "recruitInfo": { + "minEducation": "BA", + "salaryRange": "D", + "workExperience": "5", + "skillReq": "Python, JavaScript, SQL" + } +} +``` + +**Response (201 Created)** +```json +{ + "success": true, + "message": "崗位資料新增成功", + "data": { + "id": "ENG-001", + "basicInfo": { ... }, + "recruitInfo": { ... }, + "createdAt": "2024-12-03T10:30:00", + "updatedAt": "2024-12-03T10:30:00" + } +} +``` + +#### 4.2.2 查詢崗位列表 + +**Request** +```http +GET /api/positions?page=1&size=20&search=工程師 +``` + +**Response (200 OK)** +```json +{ + "success": true, + "data": [ + { + "id": "ENG-001", + "basicInfo": { ... }, + "recruitInfo": { ... } + } + ], + "pagination": { + "page": 1, + "size": 20, + "total": 1, + "totalPages": 1 + } +} +``` + +### 4.3 錯誤處理 + +| HTTP 狀態碼 | 錯誤類型 | 說明 | +|-------------|----------|------| +| 400 | Bad Request | 請求參數錯誤或缺少必填欄位 | +| 404 | Not Found | 找不到指定資源 | +| 409 | Conflict | 資源衝突(如編號已存在) | +| 500 | Internal Server Error | 伺服器內部錯誤 | + +**錯誤回應格式** +```json +{ + "success": false, + "error": "錯誤訊息描述" +} +``` + +--- + +## 5. AI 自動填充功能設計 + +### 5.1 功能概述 + +系統整合 Claude API,提供智能表單填充功能。用戶點擊「✨ I'm feeling lucky」按鈕後,AI 會根據已填寫的內容,自動補充尚未填寫的欄位。 + +### 5.2 填充邏輯 + +``` +┌─────────────────────────────────────────────────────────┐ +│ AI 填充流程 │ +├─────────────────────────────────────────────────────────┤ +│ 1. 用戶點擊「I'm feeling lucky」按鈕 │ +│ │ │ +│ ▼ │ +│ 2. 檢查表單欄位狀態 │ +│ ├── 收集已填寫欄位 (作為上下文) │ +│ └── 識別空白欄位 (待填充目標) │ +│ │ │ +│ ▼ │ +│ 3. 若無空白欄位 → 顯示「所有欄位都已填寫完成!」 │ +│ 若有空白欄位 → 繼續 │ +│ │ │ +│ ▼ │ +│ 4. 組裝 Prompt │ +│ ├── 包含已填寫資料作為參考 │ +│ ├── 指定只生成空白欄位 │ +│ └── 要求返回 JSON 格式 │ +│ │ │ +│ ▼ │ +│ 5. 呼叫 Claude API (claude-sonnet-4-20250514) │ +│ │ │ +│ ▼ │ +│ 6. 解析回應並填充空白欄位 │ +│ └── 已有內容的欄位保持不變 │ +│ │ │ +│ ▼ │ +│ 7. 顯示「✨ AI 已補充 N 個欄位!」 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 5.3 核心函數 + +#### 5.3.1 fillIfEmpty + +```javascript +/** + * 只在欄位為空時填入值 + * @param {string} elementId - DOM 元素 ID + * @param {string} value - 要填入的值 + * @returns {boolean} - 是否有填入值 + */ +function fillIfEmpty(elementId, value) { + const el = document.getElementById(elementId); + if (el && !el.value.trim() && value) { + el.value = value; + return true; + } + return false; +} +``` + +#### 5.3.2 getEmptyFields + +```javascript +/** + * 獲取空白欄位列表 + * @param {string[]} fieldIds - 欄位 ID 陣列 + * @returns {string[]} - 空白欄位 ID 陣列 + */ +function getEmptyFields(fieldIds) { + return fieldIds.filter(id => !getFieldValue(id)); +} +``` + +### 5.4 API 呼叫規格 + +**Endpoint** +``` +POST https://api.anthropic.com/v1/messages +``` + +**Request Body** +```json +{ + "model": "claude-sonnet-4-20250514", + "max_tokens": 2000, + "messages": [ + { + "role": "user", + "content": "Prompt 內容..." + } + ] +} +``` + +### 5.5 Prompt 設計範例 + +``` +請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。 + +已填寫的資料(請參考這些內容來生成相關的資料): +{ + "positionName": "資深前端工程師", + "positionLevel": "L4" +} + +請「只生成」以下這些尚未填寫的欄位:positionCode, positionCategory, positionNature, headcount, positionDesc + +欄位說明: +- positionCode: 崗位編號(格式如 ENG-001) +- positionCategory: 崗位類別代碼(01=技術職, 02=管理職...) +... + +請直接返回JSON格式,只包含需要生成的欄位: +{ + "positionCode": "...", + "positionCategory": "...", + ... +} +``` + +--- + +## 6. 使用者介面設計 + +### 6.1 色彩規範 + +| 變數名稱 | 色碼 | 用途 | +|----------|------|------| +| --primary | #1a5276 | 主色(崗位模組) | +| --primary-light | #2980b9 | 主色亮色 | +| --primary-dark | #0e3a53 | 主色暗色 | +| --accent | #e67e22 | 強調色(職務模組) | +| --green | #27ae60 | 綠色(崗位描述模組) | +| --success | #27ae60 | 成功狀態 | +| --warning | #f39c12 | 警告狀態 | +| --danger | #e74c3c | 錯誤/必填標示 | + +### 6.2 模組主題色 + +| 模組 | Header 背景 | 按鈕樣式 | +|------|-------------|----------| +| 崗位基礎資料 | 藍色漸層 | primary | +| 職務基礎資料 | 橘色漸層 | accent | +| 崗位描述 | 綠色漸層 | green | + +### 6.3 元件規格 + +#### 6.3.1 輸入框 + +```css +input, select, textarea { + padding: 10px 14px; + border: 1.5px solid var(--border); + border-radius: 6px; + font-size: 0.9rem; +} +``` + +#### 6.3.2 按鈕 + +| 類型 | Class | 用途 | +|------|-------|------| +| 主要按鈕 | .btn-primary | 保存操作 | +| 次要按鈕 | .btn-secondary | 保存並新增 | +| 取消按鈕 | .btn-cancel | 取消操作 | +| AI 按鈕 | .ai-generate-btn | AI 自動填充 | + +#### 6.3.3 Toggle 開關 + +```css +.toggle-switch { + width: 52px; + height: 28px; +} +``` + +### 6.4 響應式斷點 + +| 斷點 | 寬度 | 佈局調整 | +|------|------|----------| +| Desktop | > 768px | 雙欄表單佈局 | +| Mobile | ≤ 768px | 單欄表單佈局 | + +--- + +## 7. 資料結構 + +### 7.1 崗位資料 (Position) + +```typescript +interface Position { + id: string; // 崗位編號 (PK) + basicInfo: { + positionCode: string; // 崗位編號 + positionName: string; // 崗位名稱 + positionCategory: string; // 崗位類別 + positionCategoryName: string;// 崗位類別名稱 + positionNature: string; // 崗位性質 + positionNatureName: string; // 崗位性質名稱 + headcount: string; // 編制人數 + positionLevel: string; // 崗位級別 + effectiveDate: string; // 生效日期 + positionDesc: string; // 崗位描述 + positionRemark: string; // 崗位備注 + }; + recruitInfo: { + minEducation: string; // 最低學歷 + requiredGender: string; // 要求性別 + salaryRange: string; // 薪酬范圍 + workExperience: string; // 工作經驗 + minAge: string; // 最小年齡 + maxAge: string; // 最大年齡 + jobType: string; // 工作性質 + recruitPosition: string; // 招聘職位 + jobTitle: string; // 職位名稱 + jobDesc: string; // 職位描述 + positionReq: string; // 崗位要求 + titleReq: string; // 職稱要求 + majorReq: string; // 專業要求 + skillReq: string; // 技能要求 + langReq: string; // 語言要求 + otherReq: string; // 其他要求 + superiorPosition: string; // 上級崗位編號 + recruitRemark: string; // 備注說明 + }; + createdAt: string; // 建立時間 + updatedAt: string; // 更新時間 +} +``` + +### 7.2 職務資料 (Job) + +```typescript +interface Job { + id: string; // 職務編號 (PK) + jobCategoryCode: string; // 職務類別編號 + jobCategoryName: string; // 職務類別名稱 + jobCode: string; // 職務編號 + jobName: string; // 職務名稱 + jobNameEn: string; // 職務英文名稱 + jobEffectiveDate: string; // 生效日期 + jobHeadcount: number; // 編制人數 + jobSortOrder: number; // 排列順序 + jobRemark: string; // 備注說明 + jobLevel: string; // 職務層級 + hasAttendanceBonus: boolean; // 是否有全勤 + hasHousingAllowance: boolean; // 是否住房補貼 + createdAt: string; // 建立時間 + updatedAt: string; // 更新時間 +} +``` + +### 7.3 崗位描述資料 (JobDescription) + +```typescript +interface JobDescription { + id: string; // ID (PK) + basicInfo: { + empNo: string; // 工號 + empName: string; // 姓名 + positionCode: string; // 崗位代碼 + versionDate: string; // 版本更新日期 + }; + positionInfo: { + positionName: string; // 崗位名稱 + department: string; // 所屬部門 + positionEffectiveDate: string; // 崗位生效日期 + directSupervisor: string; // 直接領導職務 + positionGradeJob: string; // 崗位職等&職務 + reportTo: string; // 匯報對象職務 + directReports: string; // 直接下級 + workLocation: string; // 任職地點 + empAttribute: string; // 員工屬性 + }; + responsibilities: { + positionPurpose: string; // 崗位設置目的 + mainResponsibilities: string;// 主要崗位職責 + }; + requirements: { + education: string; // 教育程度 + basicSkills: string; // 基本技能 + professionalKnowledge: string; // 專業知識 + workExperienceReq: string; // 工作經驗 + otherRequirements: string; // 其他要求 + }; + createdAt: string; // 建立時間 + updatedAt: string; // 更新時間 +} +``` + +--- + +## 8. 快捷鍵設計 + +| 快捷鍵 | 功能 | 適用範圍 | +|--------|------|----------| +| `Ctrl + S` | 保存并退出 | 所有模組 | +| `Ctrl + N` | 保存并新增 | 所有模組 | + +--- + +## 9. 檔案結構 + +``` +hr_position_form/ +├── index.html # 前端應用主檔 +├── app.py # Flask 後端 API +├── requirements.txt # Python 依賴套件 +├── README.md # 使用說明文件 +└── SDD.md # 軟體設計文件(本文件) +``` + +--- + +## 10. 未來擴展建議 + +### 10.1 資料庫整合 + +將 In-Memory 儲存替換為正式資料庫: + +```python +# 建議使用 SQLAlchemy ORM +from flask_sqlalchemy import SQLAlchemy + +app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://user:pass@host/db' +db = SQLAlchemy(app) +``` + +### 10.2 權限控制整合 + +```python +# 建議整合 Azure AD +from flask_azure_oauth import AzureOAuth + +azure_oauth = AzureOAuth(app) + +@app.route('/api/positions', methods=['POST']) +@azure_oauth.require_auth +def create_position(): + ... +``` + +### 10.3 審計日誌 + +```python +# 記錄所有資料變更 +def log_audit(action, entity_type, entity_id, old_data, new_data, user_id): + audit_log = { + 'action': action, # CREATE/UPDATE/DELETE + 'entity_type': entity_type, + 'entity_id': entity_id, + 'old_data': old_data, + 'new_data': new_data, + 'user_id': user_id, + 'timestamp': datetime.now().isoformat() + } + # 儲存至審計日誌表 +``` + +### 10.4 多語言支援 + +```javascript +// i18n 國際化 +const i18n = { + 'zh-TW': { + 'position.code': '崗位編號', + 'position.name': '崗位名稱', + ... + }, + 'en-US': { + 'position.code': 'Position Code', + 'position.name': 'Position Name', + ... + } +}; +``` + +--- + +## 11. 版本歷史 + +| 版本 | 日期 | 作者 | 變更說明 | +|------|------|------|----------| +| 1.0 | 2024-12-03 | System | 初始版本,包含三大模組設計與 AI 功能 | + +--- + +## 12. 附錄 + +### 附錄 A:JSON 資料預覽格式 + +系統提供即時 JSON 預覽功能,格式如下: + +```json +{ + "module": "崗位基礎資料", + "basicInfo": { + "positionCode": "ENG-001", + "positionName": "資深軟體工程師", + "positionCategory": "01", + "positionCategoryName": "技術職", + "positionNature": "FT", + "positionNatureName": "全職", + "headcount": "5", + "positionLevel": "L3" + }, + "recruitInfo": { + "minEducation": "BA", + "salaryRange": "D", + "workExperience": "5" + } +} +``` + +### 附錄 B:UI 截圖參考 + +(請參考系統實際畫面) + +--- + +**文件結束** diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..4e27b79 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,438 @@ +# HR 基礎資料維護系統 - 安裝與配置指南 + +## 📋 目錄 + +1. [系統需求](#系統需求) +2. [環境配置](#環境配置) +3. [資料庫設置](#資料庫設置) +4. [Gitea 版本控制設置](#gitea-版本控制設置) +5. [LLM API 配置](#llm-api-配置) +6. [啟動系統](#啟動系統) +7. [功能測試](#功能測試) +8. [常見問題](#常見問題) + +--- + +## 系統需求 + +### 軟體需求 + +- **Python**: 3.8 或更高版本 +- **MySQL**: 5.7 或更高版本 +- **Git**: 2.0 或更高版本 (用於版本控制) +- **現代瀏覽器**: Chrome, Firefox, Edge (最新版本) + +### 硬體需求 + +- **記憶體**: 最少 2GB RAM +- **硬碟**: 最少 500MB 可用空間 +- **網路**: 穩定的網際網路連線 (用於 LLM API) + +--- + +## 環境配置 + +### 1. 複製或下載項目 + +```bash +cd d:/00001_Vibe_coding/1204剛為 +``` + +### 2. 創建虛擬環境 (建議) + +```bash +# Windows +python -m venv venv +venv\Scripts\activate + +# Linux/Mac +python3 -m venv venv +source venv/bin/activate +``` + +### 3. 安裝依賴套件 + +```bash +pip install -r requirements_full.txt +``` + +如果出現錯誤,可以嘗試逐個安裝: + +```bash +pip install flask flask-cors pymysql python-dotenv requests gitpython +``` + +### 4. 配置環境變數 + +項目根目錄下已有 [.env](./.env) 文件,請根據實際情況修改: + +```env +# MySQL Database Configuration +DB_HOST=mysql.theaken.com +DB_PORT=33306 +DB_NAME=db_A102 +DB_USER=A102 +DB_PASSWORD=Bb123456 + +# Gitea Version Control Configuration +GITEA_URL=https://gitea.theaken.com/ +GITEA_USER=donald +GITEA_PASSWORD=!QAZ2wsx +GITEA_TOKEN=9e0a888d1a25bde9cf2ad5dff2bb7ee6d68d6ff0 + +# LLM API Keys (需要自行申請並填寫) +GEMINI_API_KEY=your_gemini_api_key_here +DEEPSEEK_API_KEY=your_deepseek_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +``` + +⚠️ **重要**: `.env` 文件包含敏感資訊,已加入 `.gitignore`,請勿上傳到版本控制系統。 + +--- + +## 資料庫設置 + +### 1. 測試資料庫連線 + +```bash +python init_database.py +``` + +此腳本會: +- 測試資料庫連線 +- 創建 `hr_position_system` 資料庫架構 +- 建立所有必要的資料表 +- 插入參照資料 + +### 2. 驗證資料表 + +成功執行後,應該會看到以下資料表: + +- `positions` - 崗位基礎資料表 +- `jobs` - 職務基礎資料表 +- `job_descriptions` - 崗位描述表 +- `reference_codes` - 參照資料代碼表 +- `audit_logs` - 審計日誌表 + +### 3. 手動執行 SQL (可選) + +如果自動腳本失敗,可以手動執行: + +```bash +# 使用 MySQL 客戶端 +mysql -h mysql.theaken.com -P 33306 -u A102 -p db_A102 < database_schema.sql +``` + +--- + +## Gitea 版本控制設置 + +### 1. 測試 Gitea 連線 + +```bash +python init_gitea.py +``` + +此腳本會: +- 測試 Gitea 伺服器連線 +- 創建新的 Git 倉庫 (如果不存在) +- 配置本地 Git remote +- 創建初始提交 + +### 2. 推送到 Gitea + +```bash +git push -u origin main +``` + +### 3. 訪問 Gitea 倉庫 + +在瀏覽器中訪問: [https://gitea.theaken.com/donald/hr-position-system](https://gitea.theaken.com/donald/hr-position-system) + +--- + +## LLM API 配置 + +系統支援三種 LLM API: + +### 1. Google Gemini + +1. 訪問 [Google AI Studio](https://makersuite.google.com/app/apikey) +2. 創建 API Key +3. 將 API Key 填入 `.env` 文件的 `GEMINI_API_KEY` + +### 2. DeepSeek + +1. 訪問 [DeepSeek 官網](https://www.deepseek.com/) +2. 註冊並獲取 API Key +3. 將 API Key 填入 `.env` 文件的 `DEEPSEEK_API_KEY` + +### 3. OpenAI + +1. 訪問 [OpenAI Platform](https://platform.openai.com/api-keys) +2. 創建 API Key +3. 將 API Key 填入 `.env` 文件的 `OPENAI_API_KEY` + +### 4. 測試 LLM API + +```bash +python llm_config.py +``` + +此腳本會測試所有已配置的 API 連線狀態。 + +--- + +## 啟動系統 + +### 方法 1: 使用更新版的 app (包含 LLM 功能) + +```bash +# 備份原有的 app.py +mv app.py app_original.py + +# 使用新版 app +mv app_updated.py app.py + +# 啟動系統 +python app.py +``` + +### 方法 2: 直接使用更新版 + +```bash +python app_updated.py +``` + +### 啟動成功 + +您應該會看到類似以下的輸出: + +``` +╔══════════════════════════════════════════════════════════════╗ +║ HR 基礎資料維護系統 - Flask Backend ║ +╠══════════════════════════════════════════════════════════════╣ +║ 伺服器啟動中... ║ +║ 訪問網址: http://localhost:5000 ║ +... +╚══════════════════════════════════════════════════════════════╝ + +✓ LLM 功能已啟用 + 已配置的 API: gemini, deepseek, openai +``` + +--- + +## 功能測試 + +### 1. 訪問主頁面 + +在瀏覽器中打開: [http://localhost:5000](http://localhost:5000) + +### 2. 測試 LLM API 連線 + +訪問 API 測試頁面: [http://localhost:5000/api-test](http://localhost:5000/api-test) + +在此頁面可以: +- 查看所有 LLM API 配置狀態 +- 單獨測試每個 API 連線 +- 一次測試所有 API + +### 3. 測試 REST API + +使用 Postman 或 curl 測試 API: + +```bash +# 獲取崗位列表 +curl http://localhost:5000/api/positions + +# 獲取職務列表 +curl http://localhost:5000/api/jobs + +# 測試 LLM API 配置 +curl http://localhost:5000/api/llm/config + +# 測試 Gemini API +curl http://localhost:5000/api/llm/test/gemini +``` + +### 4. 測試崗位資料 CRUD + +1. **新增崗位**: 點擊「新增崗位」按鈕 +2. **填寫表單**: 輸入崗位基本資料 +3. **AI 自動填充**: 點擊「✨ I'm feeling lucky」按鈕 +4. **保存資料**: 點擊「保存並退出」 + +### 5. 測試錯誤處理 + +系統已集成全局錯誤處理: + +- 所有 API 錯誤會自動顯示錯誤對話框 +- 網路錯誤會有友好的提示訊息 +- 可以在瀏覽器控制台查看詳細錯誤信息 + +--- + +## 常見問題 + +### Q1: 資料庫連線失敗 + +**問題**: 執行 `init_database.py` 時出現連線錯誤 + +**解決方案**: +1. 檢查 `.env` 文件中的資料庫配置是否正確 +2. 確認資料庫伺服器是否可訪問: + ```bash + ping mysql.theaken.com + ``` +3. 檢查防火牆是否阻擋了 33306 端口 +4. 確認資料庫用戶名和密碼正確 + +### Q2: Gitea 推送失敗 + +**問題**: `git push` 時要求輸入密碼 + +**解決方案**: +1. 使用 Token 進行認證: + ```bash + git remote set-url origin https://donald:9e0a888d1a25bde9cf2ad5dff2bb7ee6d68d6ff0@gitea.theaken.com/donald/hr-position-system.git + ``` +2. 或配置 Git credential helper + +### Q3: LLM API 測試失敗 + +**問題**: LLM API 連線測試顯示失敗 + +**解決方案**: +1. 檢查 API Key 是否正確填寫在 `.env` 文件中 +2. 確認 API Key 是否有效 (未過期) +3. 檢查網路連線是否正常 +4. 查看是否有 API 配額限制 + +### Q4: 端口被占用 + +**問題**: 啟動 Flask 時提示 `Address already in use` + +**解決方案**: +1. 更改端口: + ```python + app.run(host='0.0.0.0', port=5001, debug=True) + ``` +2. 或關閉占用 5000 端口的進程: + ```bash + # Windows + netstat -ano | findstr :5000 + taskkill /PID /F + + # Linux/Mac + lsof -i :5000 + kill -9 + ``` + +### Q5: 前端頁面無法訪問 + +**問題**: 瀏覽器顯示 404 Not Found + +**解決方案**: +1. 確認 Flask 已正確啟動 +2. 檢查 `index.html` 是否存在於項目根目錄 +3. 清除瀏覽器緩存並重新載入 + +### Q6: LLM 功能未啟用 + +**問題**: 系統提示「LLM 功能未啟用」 + +**解決方案**: +1. 確認 `llm_config.py` 文件存在 +2. 確認已安裝 `requests` 套件: + ```bash + pip install requests + ``` +3. 使用 `app_updated.py` 而非原始的 `app.py` + +--- + +## 系統架構 + +``` +┌─────────────────────────────────────────┐ +│ 使用者瀏覽器 │ +│ ┌───────────────────────────────────┐ │ +│ │ index.html (主應用) │ │ +│ │ api_test.html (API測試頁) │ │ +│ │ error_handler.js (錯誤處理) │ │ +│ └───────────────────────────────────┘ │ +└─────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Flask Backend (app.py) │ +│ ┌───────────────────────────────────┐ │ +│ │ 崗位資料 API │ │ +│ │ 職務資料 API │ │ +│ │ 參照資料 API │ │ +│ │ LLM API (llm_config.py) │ │ +│ └───────────────────────────────────┘ │ +└─────────────────────────────────────────┘ + │ + ┌─────┴─────┐ + ▼ ▼ +┌──────────┐ ┌──────────┐ +│ MySQL │ │ LLM APIs │ +│ Database │ │ (Gemini, │ +│ │ │ DeepSeek,│ +│ │ │ OpenAI) │ +└──────────┘ └──────────┘ +``` + +--- + +## 檔案說明 + +| 檔案 | 說明 | +|------|------| +| `.env` | 環境變數配置 | +| `.gitignore` | Git 忽略檔案配置 | +| `database_schema.sql` | MySQL 資料庫架構 | +| `init_database.py` | 資料庫初始化腳本 | +| `init_gitea.py` | Gitea 倉庫初始化腳本 | +| `llm_config.py` | LLM API 配置模組 | +| `app.py` | Flask 後端主程式 (原始版) | +| `app_updated.py` | Flask 後端主程式 (包含 LLM) | +| `error_handler.js` | 全局錯誤處理模組 | +| `api_test.html` | LLM API 測試頁面 | +| `index.html` | 主應用頁面 | +| `requirements_full.txt` | Python 依賴套件列表 | +| `SDD.md` | 軟體設計文件 | +| `SETUP.md` | 本安裝指南 | + +--- + +## 技術支援 + +如遇到問題,請: + +1. 查看瀏覽器控制台的錯誤訊息 +2. 檢查 Flask 終端的錯誤日誌 +3. 參考本文檔的[常見問題](#常見問題)章節 +4. 查看系統設計文件 `SDD.md` + +--- + +## 更新日誌 + +### Version 1.0 (2024-12-03) + +- ✅ 初始版本發布 +- ✅ 資料庫架構設計與初始化 +- ✅ Gitea 版本控制整合 +- ✅ LLM API 整合 (Gemini, DeepSeek, OpenAI) +- ✅ 全局錯誤處理機制 +- ✅ API 測試頁面 +- ✅ 環境配置自動化 + +--- + +**文件版本**: 1.0 +**最後更新**: 2024-12-03 +**維護者**: HR System Development Team diff --git a/api_test.html b/api_test.html new file mode 100644 index 0000000..d83e4b1 --- /dev/null +++ b/api_test.html @@ -0,0 +1,520 @@ + + + + + + LLM API 連線測試 - HR 系統 + + + + +
+
+

🔌 LLM API 連線測試

+

HR 基礎資料維護系統 - API 配置檢測

+
+ +
+ +
+

📊 API 配置總覽

+
+
+
3
+
API 總數
+
+
+
0
+
已配置
+
+
+
0
+
已測試
+
+
+
0
+
測試成功
+
+
+
+ + +
+ +
+ + +
+
+
🔷 Google Gemini
+
未測試
+
+
+
API Key:
+
+
+ •••••••••••••••• +
+
+
Endpoint:
+
https://generativelanguage.googleapis.com
+
狀態:
+
等待檢測...
+
+
+ +
+
+
+ + +
+
+
🔵 DeepSeek
+
未測試
+
+
+
API Key:
+
+
+ •••••••••••••••• +
+
+
Endpoint:
+
https://api.deepseek.com/v1
+
狀態:
+
等待檢測...
+
+
+ +
+
+
+ + +
+
+
🟢 OpenAI
+
未測試
+
+
+
API Key:
+
+
+ •••••••••••••••• +
+
+
Endpoint:
+
https://api.openai.com/v1
+
狀態:
+
等待檢測...
+
+
+ +
+
+
+
+
+ + + + diff --git a/database_schema.sql b/database_schema.sql new file mode 100644 index 0000000..9fbb486 --- /dev/null +++ b/database_schema.sql @@ -0,0 +1,259 @@ +-- HR Position System Database Schema +-- Database: db_A102 +-- Schema: hr_position_system + +-- Create schema if not exists +CREATE SCHEMA IF NOT EXISTS hr_position_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hr_position_system; + +-- ============================================================ +-- Table: positions (崗位基礎資料) +-- ============================================================ +CREATE TABLE IF NOT EXISTS positions ( + id VARCHAR(20) PRIMARY KEY COMMENT '崗位編號', + position_code VARCHAR(20) NOT NULL UNIQUE COMMENT '崗位編號', + position_name VARCHAR(100) NOT NULL COMMENT '崗位名稱', + position_category VARCHAR(2) COMMENT '崗位類別代碼', + position_category_name VARCHAR(50) COMMENT '崗位類別名稱', + position_nature VARCHAR(2) COMMENT '崗位性質代碼', + position_nature_name VARCHAR(50) COMMENT '崗位性質名稱', + headcount INT COMMENT '編制人數', + position_level VARCHAR(2) COMMENT '崗位級別', + effective_date DATE COMMENT '生效日期', + position_desc TEXT COMMENT '崗位描述', + position_remark TEXT COMMENT '崗位備注', + + -- Recruitment Info + min_education VARCHAR(3) COMMENT '最低學歷', + required_gender VARCHAR(1) COMMENT '要求性別', + salary_range VARCHAR(1) COMMENT '薪酬范圍', + work_experience VARCHAR(2) COMMENT '工作經驗(年)', + min_age INT COMMENT '最小年齡', + max_age INT COMMENT '最大年齡', + job_type VARCHAR(2) COMMENT '工作性質', + recruit_position VARCHAR(3) COMMENT '招聘職位', + job_title VARCHAR(100) COMMENT '職位名稱', + job_desc TEXT COMMENT '職位描述', + position_req TEXT COMMENT '崗位要求', + title_req VARCHAR(4) COMMENT '職稱要求', + major_req VARCHAR(200) COMMENT '專業要求', + skill_req VARCHAR(200) COMMENT '技能要求', + lang_req VARCHAR(100) COMMENT '語言要求', + other_req VARCHAR(200) COMMENT '其他要求', + superior_position VARCHAR(20) COMMENT '上級崗位編號', + recruit_remark TEXT COMMENT '備注說明', + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間', + created_by VARCHAR(50) COMMENT '建立人', + updated_by VARCHAR(50) COMMENT '更新人', + + INDEX idx_position_code (position_code), + INDEX idx_position_name (position_name), + INDEX idx_position_category (position_category), + INDEX idx_position_level (position_level) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='崗位基礎資料表'; + +-- ============================================================ +-- Table: jobs (職務基礎資料) +-- ============================================================ +CREATE TABLE IF NOT EXISTS jobs ( + id VARCHAR(20) PRIMARY KEY COMMENT '職務編號', + job_category_code VARCHAR(4) NOT NULL COMMENT '職務類別編號', + job_category_name VARCHAR(50) COMMENT '職務類別名稱', + job_code VARCHAR(20) NOT NULL UNIQUE COMMENT '職務編號', + job_name VARCHAR(100) NOT NULL COMMENT '職務名稱', + job_name_en VARCHAR(100) COMMENT '職務英文名稱', + job_effective_date DATE COMMENT '生效日期', + job_headcount INT COMMENT '編制人數', + job_sort_order INT COMMENT '排列順序', + job_remark TEXT COMMENT '備注說明', + job_level VARCHAR(50) COMMENT '職務層級', + has_attendance_bonus BOOLEAN DEFAULT FALSE COMMENT '是否有全勤', + has_housing_allowance BOOLEAN DEFAULT FALSE COMMENT '是否住房補貼', + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間', + created_by VARCHAR(50) COMMENT '建立人', + updated_by VARCHAR(50) COMMENT '更新人', + + INDEX idx_job_code (job_code), + INDEX idx_job_name (job_name), + INDEX idx_job_category (job_category_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='職務基礎資料表'; + +-- ============================================================ +-- Table: job_descriptions (崗位描述) +-- ============================================================ +CREATE TABLE IF NOT EXISTS job_descriptions ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵', + emp_no VARCHAR(20) COMMENT '工號', + emp_name VARCHAR(50) COMMENT '姓名', + position_code VARCHAR(20) COMMENT '崗位代碼', + version_date DATE COMMENT '版本更新日期', + + -- Position Info + position_name VARCHAR(100) COMMENT '崗位名稱', + department VARCHAR(100) COMMENT '所屬部門', + position_effective_date DATE COMMENT '崗位生效日期', + direct_supervisor VARCHAR(100) COMMENT '直接領導職務', + position_grade_job VARCHAR(100) COMMENT '崗位職等&職務', + report_to VARCHAR(100) COMMENT '匯報對象職務', + direct_reports VARCHAR(200) COMMENT '直接下級', + work_location VARCHAR(3) COMMENT '任職地點', + emp_attribute VARCHAR(2) COMMENT '員工屬性', + + -- Responsibilities + position_purpose VARCHAR(500) COMMENT '崗位設置目的', + main_responsibilities TEXT COMMENT '主要崗位職責', + + -- Requirements + education VARCHAR(200) COMMENT '教育程度', + basic_skills TEXT COMMENT '基本技能', + professional_knowledge TEXT COMMENT '專業知識', + work_experience_req TEXT COMMENT '工作經驗', + other_requirements TEXT COMMENT '其他要求', + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間', + created_by VARCHAR(50) COMMENT '建立人', + updated_by VARCHAR(50) COMMENT '更新人', + + INDEX idx_emp_no (emp_no), + INDEX idx_position_code (position_code), + INDEX idx_version_date (version_date), + FOREIGN KEY (position_code) REFERENCES positions(position_code) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='崗位描述表'; + +-- ============================================================ +-- Table: reference_codes (參照資料代碼表) +-- ============================================================ +CREATE TABLE IF NOT EXISTS reference_codes ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵', + code_type VARCHAR(50) NOT NULL COMMENT '代碼類型', + code_value VARCHAR(10) NOT NULL COMMENT '代碼值', + code_name VARCHAR(100) NOT NULL COMMENT '代碼名稱', + code_name_en VARCHAR(100) COMMENT '英文名稱', + sort_order INT DEFAULT 0 COMMENT '排序', + is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用', + remark VARCHAR(200) COMMENT '備注', + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間', + + UNIQUE KEY uk_code_type_value (code_type, code_value), + INDEX idx_code_type (code_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='參照資料代碼表'; + +-- ============================================================ +-- Table: audit_logs (審計日誌) +-- ============================================================ +CREATE TABLE IF NOT EXISTS audit_logs ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵', + action VARCHAR(20) NOT NULL COMMENT '操作類型(CREATE/UPDATE/DELETE)', + entity_type VARCHAR(50) NOT NULL COMMENT '實體類型', + entity_id VARCHAR(50) NOT NULL COMMENT '實體ID', + old_data JSON COMMENT '變更前資料', + new_data JSON COMMENT '變更後資料', + user_id VARCHAR(50) COMMENT '操作用戶', + ip_address VARCHAR(50) COMMENT 'IP地址', + user_agent VARCHAR(200) COMMENT '用戶代理', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '操作時間', + + INDEX idx_entity_type_id (entity_type, entity_id), + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='審計日誌表'; + +-- ============================================================ +-- Insert Reference Data (參照資料初始化) +-- ============================================================ + +-- 崗位類別 (Position Category) +INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES +('POSITION_CATEGORY', '01', '技術職', 1), +('POSITION_CATEGORY', '02', '管理職', 2), +('POSITION_CATEGORY', '03', '業務職', 3), +('POSITION_CATEGORY', '04', '行政職', 4); + +-- 崗位性質 (Position Nature) +INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES +('POSITION_NATURE', 'FT', '全職', 'Full-time', 1), +('POSITION_NATURE', 'PT', '兼職', 'Part-time', 2), +('POSITION_NATURE', 'CT', '約聘', 'Contract', 3), +('POSITION_NATURE', 'IN', '實習', 'Intern', 4); + +-- 崗位級別 (Position Level) +INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES +('POSITION_LEVEL', 'L1', '基層員工', 1), +('POSITION_LEVEL', 'L2', '資深員工', 2), +('POSITION_LEVEL', 'L3', '主管', 3), +('POSITION_LEVEL', 'L4', '經理', 4), +('POSITION_LEVEL', 'L5', '總監', 5), +('POSITION_LEVEL', 'L6', '副總', 6), +('POSITION_LEVEL', 'L7', '總經理', 7); + +-- 職務類別 (Job Category) +INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES +('JOB_CATEGORY', 'MGR', '管理職', 1), +('JOB_CATEGORY', 'TECH', '技術職', 2), +('JOB_CATEGORY', 'SALE', '業務職', 3), +('JOB_CATEGORY', 'ADMIN', '行政職', 4), +('JOB_CATEGORY', 'RD', '研發職', 5), +('JOB_CATEGORY', 'PROD', '生產職', 6); + +-- 學歷 (Education) +INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES +('EDUCATION', 'HS', '高中', 'High School', 1), +('EDUCATION', 'JC', '專科', 'Junior College', 2), +('EDUCATION', 'BA', '大學', 'Bachelor', 3), +('EDUCATION', 'MA', '碩士', 'Master', 4), +('EDUCATION', 'PHD', '博士', 'PhD', 5); + +-- 薪酬范圍 (Salary Range) +INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES +('SALARY_RANGE', 'A', 'A級', 1), +('SALARY_RANGE', 'B', 'B級', 2), +('SALARY_RANGE', 'C', 'C級', 3), +('SALARY_RANGE', 'D', 'D級', 4), +('SALARY_RANGE', 'E', 'E級', 5), +('SALARY_RANGE', 'N', '面議', 6); + +-- 任職地點 (Work Location) +INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES +('WORK_LOCATION', 'HQ', '總部', 1), +('WORK_LOCATION', 'TPE', '台北辦公室', 2), +('WORK_LOCATION', 'TYC', '桃園廠區', 3), +('WORK_LOCATION', 'KHH', '高雄廠區', 4), +('WORK_LOCATION', 'SH', '上海辦公室', 5), +('WORK_LOCATION', 'SZ', '深圳辦公室', 6); + +-- 員工屬性 (Employee Attribute) +INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES +('EMP_ATTRIBUTE', 'FT', '正式員工', 1), +('EMP_ATTRIBUTE', 'CT', '約聘人員', 2), +('EMP_ATTRIBUTE', 'PT', '兼職人員', 3), +('EMP_ATTRIBUTE', 'IN', '實習生', 4), +('EMP_ATTRIBUTE', 'DP', '派遣人員', 5); + +-- 招聘職位 (Recruit Position) +INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES +('RECRUIT_POSITION', 'ENG', '工程師', 1), +('RECRUIT_POSITION', 'MGR', '經理', 2), +('RECRUIT_POSITION', 'AST', '助理', 3), +('RECRUIT_POSITION', 'OP', '操作員', 4), +('RECRUIT_POSITION', 'SAL', '業務', 5); + +-- 職稱要求 (Title Requirement) +INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES +('TITLE_REQ', 'NONE', '無要求', 1), +('TITLE_REQ', 'CERT', '需證書', 2), +('TITLE_REQ', 'LIC', '需執照', 3); + +-- ============================================================ +-- End of Schema +-- ============================================================ diff --git a/error_handler.js b/error_handler.js new file mode 100644 index 0000000..02834b4 --- /dev/null +++ b/error_handler.js @@ -0,0 +1,559 @@ +/** + * Global Error Handler for HR Position System + * Provides unified error handling and user-friendly error dialogs + */ + +class ErrorHandler { + constructor() { + this.init(); + } + + init() { + // Create error modal container if not exists + if (!document.getElementById('error-modal-container')) { + const container = document.createElement('div'); + container.id = 'error-modal-container'; + document.body.appendChild(container); + } + + // Add error handler CSS + this.injectStyles(); + + // Setup global error handlers + this.setupGlobalHandlers(); + } + + injectStyles() { + if (document.getElementById('error-handler-styles')) { + return; + } + + const style = document.createElement('style'); + style.id = 'error-handler-styles'; + style.textContent = ` + .error-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: fadeIn 0.3s ease; + } + + .error-modal { + background: white; + border-radius: 12px; + padding: 0; + max-width: 500px; + width: 90%; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + animation: slideDown 0.3s ease; + overflow: hidden; + } + + .error-modal-header { + padding: 20px; + border-bottom: 2px solid #f0f0f0; + display: flex; + align-items: center; + gap: 15px; + } + + .error-modal-header.error { + background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); + color: white; + border-bottom-color: #c0392b; + } + + .error-modal-header.warning { + background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%); + color: white; + border-bottom-color: #e67e22; + } + + .error-modal-header.info { + background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); + color: white; + border-bottom-color: #2980b9; + } + + .error-modal-header.success { + background: linear-gradient(135deg, #27ae60 0%, #229954 100%); + color: white; + border-bottom-color: #229954; + } + + .error-icon { + font-size: 2rem; + line-height: 1; + } + + .error-title { + flex: 1; + font-size: 1.3rem; + font-weight: 600; + margin: 0; + } + + .error-modal-body { + padding: 25px; + max-height: 400px; + overflow-y: auto; + } + + .error-message { + color: #333; + line-height: 1.6; + margin-bottom: 15px; + font-size: 1rem; + } + + .error-details { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 15px; + margin-top: 15px; + font-family: 'Courier New', monospace; + font-size: 0.85rem; + color: #666; + max-height: 200px; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-word; + } + + .error-modal-footer { + padding: 15px 25px; + border-top: 1px solid #f0f0f0; + display: flex; + justify-content: flex-end; + gap: 10px; + } + + .error-btn { + padding: 10px 25px; + border: none; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + font-family: 'Noto Sans TC', sans-serif; + } + + .error-btn-primary { + background: #3498db; + color: white; + } + + .error-btn-primary:hover { + background: #2980b9; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3); + } + + .error-btn-secondary { + background: #95a5a6; + color: white; + } + + .error-btn-secondary:hover { + background: #7f8c8d; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes slideDown { + from { + transform: translateY(-50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + .error-toast { + position: fixed; + top: 20px; + right: 20px; + background: white; + border-radius: 8px; + padding: 15px 20px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + display: flex; + align-items: center; + gap: 12px; + z-index: 10001; + animation: slideInRight 0.3s ease; + max-width: 400px; + } + + .error-toast.error { + border-left: 4px solid #e74c3c; + } + + .error-toast.warning { + border-left: 4px solid #f39c12; + } + + .error-toast.info { + border-left: 4px solid #3498db; + } + + .error-toast.success { + border-left: 4px solid #27ae60; + } + + .error-toast-icon { + font-size: 1.5rem; + } + + .error-toast-content { + flex: 1; + } + + .error-toast-title { + font-weight: 600; + margin-bottom: 5px; + color: #333; + } + + .error-toast-message { + font-size: 0.9rem; + color: #666; + } + + .error-toast-close { + background: none; + border: none; + font-size: 1.5rem; + color: #999; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s; + } + + .error-toast-close:hover { + background: #f0f0f0; + color: #666; + } + + @keyframes slideInRight { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes slideOutRight { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(400px); + opacity: 0; + } + } + `; + document.head.appendChild(style); + } + + setupGlobalHandlers() { + // Handle uncaught errors + window.addEventListener('error', (event) => { + this.handleError({ + title: '執行錯誤', + message: event.message, + details: `檔案: ${event.filename}\n行號: ${event.lineno}:${event.colno}\n錯誤: ${event.error?.stack || event.message}` + }); + }); + + // Handle unhandled promise rejections + window.addEventListener('unhandledrejection', (event) => { + this.handleError({ + title: 'Promise 錯誤', + message: '發生未處理的 Promise 錯誤', + details: event.reason?.stack || event.reason + }); + }); + } + + /** + * Show error modal + * @param {Object} options - Error options + * @param {string} options.title - Error title + * @param {string} options.message - Error message + * @param {string} options.details - Error details (optional) + * @param {string} options.type - Error type: error, warning, info, success (default: error) + * @param {Function} options.onClose - Callback when modal closes + */ + showError(options) { + const { + title = '錯誤', + message = '發生未知錯誤', + details = null, + type = 'error', + onClose = null + } = options; + + const icons = { + error: '❌', + warning: '⚠️', + info: 'ℹ️', + success: '✅' + }; + + const container = document.getElementById('error-modal-container'); + + const modal = document.createElement('div'); + modal.className = 'error-modal-overlay'; + modal.innerHTML = ` +
+
+
${icons[type]}
+

${this.escapeHtml(title)}

+
+
+
${this.escapeHtml(message)}
+ ${details ? `
${this.escapeHtml(details)}
` : ''} +
+ +
+ `; + + container.appendChild(modal); + + // Store close callback + modal._onClose = onClose; + + // Close on overlay click + modal.addEventListener('click', (e) => { + if (e.target === modal) { + this.closeModal(modal.querySelector('.error-btn')); + } + }); + } + + /** + * Show toast notification + * @param {Object} options - Toast options + * @param {string} options.title - Toast title + * @param {string} options.message - Toast message + * @param {string} options.type - Toast type: error, warning, info, success (default: info) + * @param {number} options.duration - Duration in ms (default: 3000) + */ + showToast(options) { + const { + title = '', + message = '', + type = 'info', + duration = 3000 + } = options; + + const icons = { + error: '❌', + warning: '⚠️', + info: 'ℹ️', + success: '✅' + }; + + const toast = document.createElement('div'); + toast.className = `error-toast ${type}`; + toast.innerHTML = ` +
${icons[type]}
+
+ ${title ? `
${this.escapeHtml(title)}
` : ''} +
${this.escapeHtml(message)}
+
+ + `; + + document.body.appendChild(toast); + + // Auto remove after duration + if (duration > 0) { + setTimeout(() => { + toast.style.animation = 'slideOutRight 0.3s ease'; + setTimeout(() => toast.remove(), 300); + }, duration); + } + } + + closeModal(button) { + const modal = button.closest('.error-modal-overlay'); + if (modal) { + const onClose = modal._onClose; + + modal.style.animation = 'fadeOut 0.3s ease'; + setTimeout(() => { + modal.remove(); + if (onClose && typeof onClose === 'function') { + onClose(); + } + }, 300); + } + } + + /** + * Handle API errors + * @param {Error} error - Error object + * @param {Object} options - Additional options + */ + async handleAPIError(error, options = {}) { + const { showModal = true, showToast = false } = options; + + let message = '發生 API 錯誤'; + let details = null; + + if (error.response) { + // Server responded with error + const data = error.response.data; + message = data.error || data.message || `HTTP ${error.response.status} 錯誤`; + details = JSON.stringify(data, null, 2); + } else if (error.request) { + // No response received + message = '無法連接到伺服器,請檢查網路連線'; + details = error.message; + } else { + // Request setup error + message = error.message; + details = error.stack; + } + + if (showModal) { + this.showError({ + title: 'API 錯誤', + message, + details, + type: 'error' + }); + } + + if (showToast) { + this.showToast({ + title: 'API 錯誤', + message, + type: 'error' + }); + } + } + + /** + * Handle general errors + * @param {Object} options - Error options + */ + handleError(options) { + console.error('[ErrorHandler]', options); + this.showError(options); + } + + /** + * Escape HTML to prevent XSS + * @param {string} text - Text to escape + * @returns {string} - Escaped text + */ + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Confirm dialog + * @param {Object} options - Confirm options + * @param {string} options.title - Title + * @param {string} options.message - Message + * @param {string} options.confirmText - Confirm button text + * @param {string} options.cancelText - Cancel button text + * @param {Function} options.onConfirm - Confirm callback + * @param {Function} options.onCancel - Cancel callback + */ + confirm(options) { + const { + title = '確認', + message = '確定要執行此操作嗎?', + confirmText = '確定', + cancelText = '取消', + onConfirm = null, + onCancel = null + } = options; + + const container = document.getElementById('error-modal-container'); + + const modal = document.createElement('div'); + modal.className = 'error-modal-overlay'; + modal.innerHTML = ` +
+
+
+

${this.escapeHtml(title)}

+
+
+
${this.escapeHtml(message)}
+
+ +
+ `; + + container.appendChild(modal); + + // Handle button clicks + modal.querySelectorAll('[data-action]').forEach(btn => { + btn.addEventListener('click', () => { + const action = btn.getAttribute('data-action'); + modal.remove(); + + if (action === 'confirm' && onConfirm) { + onConfirm(); + } else if (action === 'cancel' && onCancel) { + onCancel(); + } + }); + }); + } +} + +// Initialize global error handler +window.errorHandler = new ErrorHandler(); + +// Export for module usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = ErrorHandler; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..a10fe75 --- /dev/null +++ b/index.html @@ -0,0 +1,2184 @@ + + + + + + HR 基礎資料維護系統 + + + + +
+ +
+ + + +
+ + +
+
+
+ +
+
+

崗位基礎資料維護

+
Position Master Data Management
+
+
+ +
+
+ + +
+ +
+
+ +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
技能與專業要求
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+ + + +
+
+
+
+ + +
+
+
+ +
+
+

職務基礎資料維護

+
Job Title Master Data Management
+
+
+ +
+
+ +
+ +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
職務屬性設定
+
+ +
+ +
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+ +
+ +
+ + + +
+
+
+
+ + +
+
+
+ +
+
+

崗位描述維護 (新增)

+
Job Description Management
+
+
+ +
+
+ +
+ +
+
+ + +
+
+ +
+ + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
崗位基本信息
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+ +
+ + +
+
+ + +
+
職責描述
+
+
+ + +
+
+
+ + +
+
崗位要求
+
+
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+
+
+
+
+ +
+ +
+ + + +
+
+
+
+ + +
+

📋 資料預覽 (JSON Format)

+
{}
+
+
+ + + + + +
+ + 保存成功! +
+ + + + diff --git a/init_database.py b/init_database.py new file mode 100644 index 0000000..062ac60 --- /dev/null +++ b/init_database.py @@ -0,0 +1,130 @@ +""" +Database initialization script for HR Position System +Connects to MySQL and executes the schema creation script +""" + +import os +import pymysql +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +def init_database(): + """Initialize the database schema""" + + # Database connection parameters + db_config = { + 'host': os.getenv('DB_HOST'), + 'port': int(os.getenv('DB_PORT')), + 'user': os.getenv('DB_USER'), + 'password': os.getenv('DB_PASSWORD'), + 'database': os.getenv('DB_NAME'), + 'charset': 'utf8mb4', + 'cursorclass': pymysql.cursors.DictCursor + } + + print(f"Connecting to database: {db_config['host']}:{db_config['port']}") + print(f"Database: {db_config['database']}") + + try: + # Connect to MySQL + connection = pymysql.connect(**db_config) + print("✓ Successfully connected to database") + + # Read SQL schema file + schema_file = os.path.join(os.path.dirname(__file__), 'database_schema.sql') + with open(schema_file, 'r', encoding='utf-8') as f: + sql_script = f.read() + + # Split SQL script by semicolon and execute each statement + cursor = connection.cursor() + + # Split by semicolon and filter out empty statements + statements = [stmt.strip() for stmt in sql_script.split(';') if stmt.strip()] + + print(f"\nExecuting {len(statements)} SQL statements...") + + executed = 0 + for i, statement in enumerate(statements, 1): + try: + # Skip comments and empty lines + if not statement or statement.startswith('--'): + continue + + cursor.execute(statement) + executed += 1 + + # Show progress every 10 statements + if executed % 10 == 0: + print(f" Executed {executed} statements...") + + except Exception as e: + print(f" Warning on statement {i}: {str(e)[:100]}") + continue + + # Commit changes + connection.commit() + print(f"\n✓ Successfully executed {executed} SQL statements") + + # Verify tables were created + cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'hr_position_system' + ORDER BY table_name + """) + tables = cursor.fetchall() + + print(f"\n✓ Created {len(tables)} tables:") + for table in tables: + print(f" - {table['table_name']}") + + # Close connection + cursor.close() + connection.close() + + print("\n✓ Database initialization completed successfully!") + return True + + except Exception as e: + print(f"\n✗ Error initializing database: {str(e)}") + return False + +def test_connection(): + """Test database connection""" + db_config = { + 'host': os.getenv('DB_HOST'), + 'port': int(os.getenv('DB_PORT')), + 'user': os.getenv('DB_USER'), + 'password': os.getenv('DB_PASSWORD'), + 'database': os.getenv('DB_NAME') + } + + try: + connection = pymysql.connect(**db_config) + print("✓ Database connection test successful") + connection.close() + return True + except Exception as e: + print(f"✗ Database connection test failed: {str(e)}") + return False + +if __name__ == '__main__': + print("=" * 60) + print("HR Position System - Database Initialization") + print("=" * 60) + print() + + # Test connection first + print("Step 1: Testing database connection...") + if not test_connection(): + print("\nPlease check your database configuration in .env file") + exit(1) + + print("\nStep 2: Initializing database schema...") + if init_database(): + print("\nDatabase is ready to use!") + else: + print("\nDatabase initialization failed. Please check the errors above.") + exit(1) diff --git a/llm_config.py b/llm_config.py new file mode 100644 index 0000000..9f7efa2 --- /dev/null +++ b/llm_config.py @@ -0,0 +1,308 @@ +""" +LLM API Configuration Module +Supports Gemini, DeepSeek, and OpenAI APIs with connection testing +""" + +import os +import requests +from dotenv import load_dotenv +from typing import Dict, List, Tuple + +# Load environment variables +load_dotenv() + + +class LLMConfig: + """LLM API configuration and management""" + + def __init__(self): + self.apis = { + 'gemini': { + 'name': 'Google Gemini', + 'api_key': os.getenv('GEMINI_API_KEY', ''), + 'endpoint': 'https://generativelanguage.googleapis.com/v1/models', + 'enabled': bool(os.getenv('GEMINI_API_KEY')) + }, + 'deepseek': { + 'name': 'DeepSeek', + 'api_key': os.getenv('DEEPSEEK_API_KEY', ''), + 'endpoint': os.getenv('DEEPSEEK_API_URL', 'https://api.deepseek.com/v1'), + 'enabled': bool(os.getenv('DEEPSEEK_API_KEY')) + }, + 'openai': { + 'name': 'OpenAI', + 'api_key': os.getenv('OPENAI_API_KEY', ''), + 'endpoint': os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1'), + 'enabled': bool(os.getenv('OPENAI_API_KEY')) + } + } + + def get_enabled_apis(self) -> List[str]: + """Get list of enabled APIs""" + return [key for key, config in self.apis.items() if config['enabled']] + + def get_api_config(self, api_name: str) -> Dict: + """Get configuration for specific API""" + return self.apis.get(api_name, {}) + + def test_gemini_connection(self) -> Tuple[bool, str]: + """Test Gemini API connection""" + try: + api_key = self.apis['gemini']['api_key'] + if not api_key: + return False, "API Key 未設定" + + # Test endpoint - list models + url = f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}" + + response = requests.get(url, timeout=10) + + if response.status_code == 200: + data = response.json() + models = data.get('models', []) + model_count = len(models) + return True, f"連線成功!找到 {model_count} 個可用模型" + elif response.status_code == 400: + return False, "API Key 格式錯誤" + elif response.status_code == 403: + return False, "API Key 無效或權限不足" + else: + return False, f"連線失敗 (HTTP {response.status_code})" + + except requests.exceptions.Timeout: + return False, "連線逾時" + except requests.exceptions.ConnectionError: + return False, "無法連接到伺服器" + except Exception as e: + return False, f"錯誤: {str(e)}" + + def test_deepseek_connection(self) -> Tuple[bool, str]: + """Test DeepSeek API connection""" + try: + api_key = self.apis['deepseek']['api_key'] + endpoint = self.apis['deepseek']['endpoint'] + + if not api_key: + return False, "API Key 未設定" + + # Test endpoint - list models + url = f"{endpoint}/models" + headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json' + } + + response = requests.get(url, headers=headers, timeout=10) + + if response.status_code == 200: + data = response.json() + models = data.get('data', []) + if models: + model_count = len(models) + return True, f"連線成功!找到 {model_count} 個可用模型" + else: + return True, "連線成功!" + elif response.status_code == 401: + return False, "API Key 無效" + elif response.status_code == 403: + return False, "權限不足" + else: + return False, f"連線失敗 (HTTP {response.status_code})" + + except requests.exceptions.Timeout: + return False, "連線逾時" + except requests.exceptions.ConnectionError: + return False, "無法連接到伺服器" + except Exception as e: + return False, f"錯誤: {str(e)}" + + def test_openai_connection(self) -> Tuple[bool, str]: + """Test OpenAI API connection""" + try: + api_key = self.apis['openai']['api_key'] + endpoint = self.apis['openai']['endpoint'] + + if not api_key: + return False, "API Key 未設定" + + # Test endpoint - list models + url = f"{endpoint}/models" + headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json' + } + + response = requests.get(url, headers=headers, timeout=10) + + if response.status_code == 200: + data = response.json() + models = data.get('data', []) + model_count = len(models) + return True, f"連線成功!找到 {model_count} 個可用模型" + elif response.status_code == 401: + return False, "API Key 無效" + elif response.status_code == 403: + return False, "權限不足" + else: + return False, f"連線失敗 (HTTP {response.status_code})" + + except requests.exceptions.Timeout: + return False, "連線逾時" + except requests.exceptions.ConnectionError: + return False, "無法連接到伺服器" + except Exception as e: + return False, f"錯誤: {str(e)}" + + def test_all_connections(self) -> Dict[str, Tuple[bool, str]]: + """Test all configured API connections""" + results = {} + + if self.apis['gemini']['enabled']: + results['gemini'] = self.test_gemini_connection() + + if self.apis['deepseek']['enabled']: + results['deepseek'] = self.test_deepseek_connection() + + if self.apis['openai']['enabled']: + results['openai'] = self.test_openai_connection() + + return results + + def generate_text_gemini(self, prompt: str, max_tokens: int = 2000) -> Tuple[bool, str]: + """Generate text using Gemini API""" + try: + api_key = self.apis['gemini']['api_key'] + if not api_key: + return False, "API Key 未設定" + + url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}" + + data = { + "contents": [ + { + "parts": [ + {"text": prompt} + ] + } + ], + "generationConfig": { + "maxOutputTokens": max_tokens, + "temperature": 0.7 + } + } + + response = requests.post(url, json=data, timeout=30) + + if response.status_code == 200: + result = response.json() + text = result['candidates'][0]['content']['parts'][0]['text'] + return True, text + else: + return False, f"生成失敗 (HTTP {response.status_code}): {response.text}" + + except Exception as e: + return False, f"錯誤: {str(e)}" + + def generate_text_deepseek(self, prompt: str, max_tokens: int = 2000) -> Tuple[bool, str]: + """Generate text using DeepSeek API""" + try: + api_key = self.apis['deepseek']['api_key'] + endpoint = self.apis['deepseek']['endpoint'] + + if not api_key: + return False, "API Key 未設定" + + url = f"{endpoint}/chat/completions" + headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json' + } + + data = { + "model": "deepseek-chat", + "messages": [ + {"role": "user", "content": prompt} + ], + "max_tokens": max_tokens, + "temperature": 0.7 + } + + response = requests.post(url, json=data, headers=headers, timeout=30) + + if response.status_code == 200: + result = response.json() + text = result['choices'][0]['message']['content'] + return True, text + else: + return False, f"生成失敗 (HTTP {response.status_code}): {response.text}" + + except Exception as e: + return False, f"錯誤: {str(e)}" + + def generate_text_openai(self, prompt: str, model: str = "gpt-3.5-turbo", max_tokens: int = 2000) -> Tuple[bool, str]: + """Generate text using OpenAI API""" + try: + api_key = self.apis['openai']['api_key'] + endpoint = self.apis['openai']['endpoint'] + + if not api_key: + return False, "API Key 未設定" + + url = f"{endpoint}/chat/completions" + headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json' + } + + data = { + "model": model, + "messages": [ + {"role": "user", "content": prompt} + ], + "max_tokens": max_tokens, + "temperature": 0.7 + } + + response = requests.post(url, json=data, headers=headers, timeout=30) + + if response.status_code == 200: + result = response.json() + text = result['choices'][0]['message']['content'] + return True, text + else: + return False, f"生成失敗 (HTTP {response.status_code}): {response.text}" + + except Exception as e: + return False, f"錯誤: {str(e)}" + + +def main(): + """Test script""" + print("=" * 60) + print("LLM API Configuration Test") + print("=" * 60) + print() + + config = LLMConfig() + + # Show enabled APIs + enabled = config.get_enabled_apis() + print(f"已啟用的 API: {', '.join(enabled) if enabled else '無'}") + print() + + # Test all connections + print("測試 API 連線:") + print("-" * 60) + + results = config.test_all_connections() + + for api_name, (success, message) in results.items(): + status = "✓" if success else "✗" + api_display_name = config.apis[api_name]['name'] + print(f"{status} {api_display_name}: {message}") + + print() + + +if __name__ == '__main__': + main() diff --git a/requirements_full.txt b/requirements_full.txt new file mode 100644 index 0000000..3c8c99f --- /dev/null +++ b/requirements_full.txt @@ -0,0 +1,23 @@ +# Flask and Web Framework +Flask==3.0.0 +flask-cors==4.0.0 +Werkzeug==3.0.1 + +# Database +pymysql==1.1.0 +cryptography==41.0.7 + +# Environment Variables +python-dotenv==1.0.0 + +# HTTP Requests +requests==2.31.0 + +# Version Control +gitpython==3.1.40 + +# Development Tools +pytest==7.4.3 +pytest-flask==1.3.0 +black==23.12.1 +flake8==7.0.0 diff --git a/start_server.py b/start_server.py new file mode 100644 index 0000000..2152abf --- /dev/null +++ b/start_server.py @@ -0,0 +1,286 @@ +""" +啟動 Flask 服務器(解決 Windows 編碼問題) +""" +import sys +import os + +# 設置 UTF-8 編碼 +if sys.platform == 'win32': + import codecs + sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict') + sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict') + +# 導入並啟動 app +from flask import Flask, request, jsonify, send_from_directory +from flask_cors import CORS +from datetime import datetime +import json + +# Import LLM configuration +try: + from llm_config import LLMConfig + llm_config = LLMConfig() + LLM_ENABLED = True +except ImportError: + print("Warning: llm_config not found. LLM features will be disabled.") + LLM_ENABLED = False + +app = Flask(__name__, static_folder='.') +CORS(app) + +# 模擬資料庫 +positions_db = {} +jobs_db = {} + +# 預設崗位資料 +default_positions = { + "MGR-001": { + "id": "MGR-001", + "basicInfo": { + "positionCode": "MGR-001", + "positionName": "管理職-資深班長", + "positionCategory": "02", + "positionCategoryName": "管理職", + "positionNature": "FT", + "positionNatureName": "全職", + "headcount": "5", + "positionLevel": "L3", + "effectiveDate": "2001-01-01", + "positionDesc": "負責生產線的日常管理與人員調度", + "positionRemark": "" + }, + "recruitInfo": { + "minEducation": "JC", + "requiredGender": "", + "salaryRange": "C", + "workExperience": "3", + "minAge": "25", + "maxAge": "45", + "jobType": "FT", + "recruitPosition": "MGR", + "jobTitle": "資深班長", + "jobDesc": "", + "positionReq": "", + "titleReq": "", + "majorReq": "", + "skillReq": "", + "langReq": "", + "otherReq": "", + "superiorPosition": "", + "recruitRemark": "" + }, + "createdAt": "2024-01-01T00:00:00", + "updatedAt": "2024-01-01T00:00:00" + } +} +positions_db.update(default_positions) + +# 預設職務資料 +default_jobs = { + "VP-001": { + "id": "VP-001", + "jobCategoryCode": "MGR", + "jobCategoryName": "管理職", + "jobCode": "VP-001", + "jobName": "副總", + "jobNameEn": "Vice President", + "jobEffectiveDate": "2001-01-01", + "jobHeadcount": 2, + "jobSortOrder": 10, + "jobRemark": "", + "jobLevel": "*保密*", + "hasAttendanceBonus": False, + "hasHousingAllowance": True, + "createdAt": "2024-01-01T00:00:00", + "updatedAt": "2024-01-01T00:00:00" + } +} +jobs_db.update(default_jobs) + +# ==================== 靜態頁面 ==================== + +@app.route('/') +def index(): + return send_from_directory('.', 'index.html') + +@app.route('/api-test') +def api_test_page(): + return send_from_directory('.', 'api_test.html') + +# ==================== 崗位資料 API ==================== + +@app.route('/api/positions', methods=['GET']) +def get_positions(): + page = request.args.get('page', 1, type=int) + size = request.args.get('size', 20, type=int) + search = request.args.get('search', '', type=str) + + filtered = list(positions_db.values()) + if search: + filtered = [p for p in filtered + if search.lower() in p['basicInfo'].get('positionCode', '').lower() + or search.lower() in p['basicInfo'].get('positionName', '').lower()] + + total = len(filtered) + start = (page - 1) * size + end = start + size + paginated = filtered[start:end] + + return jsonify({ + 'success': True, + 'data': paginated, + 'pagination': { + 'page': page, + 'size': size, + 'total': total, + 'totalPages': (total + size - 1) // size + } + }) + +@app.route('/api/positions/', methods=['GET']) +def get_position(position_id): + if position_id not in positions_db: + return jsonify({'success': False, 'error': '找不到該崗位資料'}), 404 + return jsonify({'success': True, 'data': positions_db[position_id]}) + +@app.route('/api/positions', methods=['POST']) +def create_position(): + try: + data = request.get_json() + if not data: + return jsonify({'success': False, 'error': '請提供有效的 JSON 資料'}), 400 + + basic_info = data.get('basicInfo', {}) + if not basic_info.get('positionCode'): + return jsonify({'success': False, 'error': '崗位編號為必填欄位'}), 400 + if not basic_info.get('positionName'): + return jsonify({'success': False, 'error': '崗位名稱為必填欄位'}), 400 + + position_code = basic_info['positionCode'] + if position_code in positions_db: + return jsonify({'success': False, 'error': f'崗位編號 {position_code} 已存在'}), 409 + + now = datetime.now().isoformat() + new_position = { + 'id': position_code, + 'basicInfo': basic_info, + 'recruitInfo': data.get('recruitInfo', {}), + 'createdAt': now, + 'updatedAt': now + } + positions_db[position_code] = new_position + return jsonify({'success': True, 'message': '崗位資料新增成功', 'data': new_position}), 201 + except Exception as e: + return jsonify({'success': False, 'error': f'新增失敗: {str(e)}'}), 500 + +# ==================== LLM API ==================== + +@app.route('/api/llm/config', methods=['GET']) +def get_llm_config(): + if not LLM_ENABLED: + return jsonify({'success': False, 'error': 'LLM 功能未啟用'}), 503 + + try: + config_data = {} + for api_name, api_config in llm_config.apis.items(): + config_data[api_name] = { + 'name': api_config['name'], + 'enabled': api_config['enabled'], + 'endpoint': api_config['endpoint'], + 'api_key': api_config['api_key'][:8] + '...' if api_config['api_key'] else '' + } + return jsonify(config_data) + except Exception as e: + return jsonify({'success': False, 'error': f'獲取配置失敗: {str(e)}'}), 500 + +@app.route('/api/llm/test/', methods=['GET']) +def test_llm_api(api_name): + if not LLM_ENABLED: + return jsonify({'success': False, 'message': 'LLM 功能未啟用'}), 503 + + try: + if api_name not in llm_config.apis: + return jsonify({'success': False, 'message': f'不支援的 API: {api_name}'}), 400 + + if api_name == 'gemini': + success, message = llm_config.test_gemini_connection() + elif api_name == 'deepseek': + success, message = llm_config.test_deepseek_connection() + elif api_name == 'openai': + success, message = llm_config.test_openai_connection() + else: + return jsonify({'success': False, 'message': f'未實作的 API: {api_name}'}), 400 + + return jsonify({'success': success, 'message': message}) + except Exception as e: + return jsonify({'success': False, 'message': f'測試失敗: {str(e)}'}), 500 + +@app.route('/api/llm/generate', methods=['POST']) +def generate_llm_text(): + if not LLM_ENABLED: + return jsonify({'success': False, 'error': 'LLM 功能未啟用'}), 503 + + try: + data = request.get_json() + if not data: + return jsonify({'success': False, 'error': '請提供有效的 JSON 資料'}), 400 + + api_name = data.get('api', 'gemini') + prompt = data.get('prompt', '') + max_tokens = data.get('max_tokens', 2000) + + if not prompt: + return jsonify({'success': False, 'error': '請提供提示詞'}), 400 + + if api_name == 'gemini': + success, result = llm_config.generate_text_gemini(prompt, max_tokens) + elif api_name == 'deepseek': + success, result = llm_config.generate_text_deepseek(prompt, max_tokens) + elif api_name == 'openai': + model = data.get('model', 'gpt-3.5-turbo') + success, result = llm_config.generate_text_openai(prompt, model, max_tokens) + else: + return jsonify({'success': False, 'error': f'不支援的 API: {api_name}'}), 400 + + if success: + return jsonify({'success': True, 'text': result}) + else: + return jsonify({'success': False, 'error': result}), 500 + except Exception as e: + return jsonify({'success': False, 'error': f'生成失敗: {str(e)}'}), 500 + +# ==================== 錯誤處理 ==================== + +@app.errorhandler(404) +def not_found(e): + return jsonify({'success': False, 'error': '找不到請求的資源'}), 404 + +@app.errorhandler(500) +def server_error(e): + return jsonify({'success': False, 'error': '伺服器內部錯誤'}), 500 + +# ==================== 主程式 ==================== + +if __name__ == '__main__': + print("=" * 60) + print("HR Position System - Flask Backend") + print("=" * 60) + print("\nServer starting...") + print("URL: http://localhost:5000") + print() + + if LLM_ENABLED: + print("[OK] LLM features enabled") + enabled_apis = llm_config.get_enabled_apis() + if enabled_apis: + print(f"Configured APIs: {', '.join(enabled_apis)}") + else: + print("Warning: No LLM API Keys configured") + else: + print("[!] LLM features disabled (llm_config.py not found)") + + print("\nPress Ctrl+C to stop") + print("=" * 60) + print() + + app.run(host='0.0.0.0', port=5000, debug=True)