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 <noreply@anthropic.com>
This commit is contained in:
82
.gitignore
vendored
Normal file
82
.gitignore
vendored
Normal file
@@ -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/
|
||||||
315
CORS_FIX_GUIDE.md
Normal file
315
CORS_FIX_GUIDE.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
在 `<head>` 標籤中添加錯誤處理腳本:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="error_handler.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
<select id="llm-selector">
|
||||||
|
<option value="gemini">Google Gemini</option>
|
||||||
|
<option value="deepseek">DeepSeek</option>
|
||||||
|
<option value="openai">OpenAI</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
然後在調用時使用:
|
||||||
|
```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 跨域請求錯誤
|
||||||
|
**解決狀態**: ✅ 已提供完整解決方案
|
||||||
236
GEMINI_API_FIX.md
Normal file
236
GEMINI_API_FIX.md
Normal file
@@ -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 限制
|
||||||
|
**解決狀態**: ✅ 已提供完整解決方案
|
||||||
844
SDD.md
Normal file
844
SDD.md
Normal file
@@ -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 截圖參考
|
||||||
|
|
||||||
|
(請參考系統實際畫面)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文件結束**
|
||||||
438
SETUP.md
Normal file
438
SETUP.md
Normal file
@@ -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 <PID> /F
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
lsof -i :5000
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
520
api_test.html
Normal file
520
api_test.html
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-TW">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>LLM API 連線測試 - HR 系統</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #1a5276;
|
||||||
|
--primary-light: #2980b9;
|
||||||
|
--success: #27ae60;
|
||||||
|
--danger: #e74c3c;
|
||||||
|
--warning: #f39c12;
|
||||||
|
--border: #ddd;
|
||||||
|
--bg-light: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans TC', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-card {
|
||||||
|
background: var(--bg-light);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-name {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: #e8e8e8;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-testing {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-info-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-info-value {
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-display {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: white;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-masked {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-family: 'Noto Sans TC', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: var(--primary-light);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(26, 82, 118, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover:not(:disabled) {
|
||||||
|
background: #229954;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: var(--success);
|
||||||
|
border: 1px solid var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: var(--danger);
|
||||||
|
border: 1px solid var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-all-section {
|
||||||
|
text-align: center;
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-all-btn {
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 3px solid rgba(255,255,255,.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: white;
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary h2 {
|
||||||
|
color: var(--primary);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔌 LLM API 連線測試</h1>
|
||||||
|
<p>HR 基礎資料維護系統 - API 配置檢測</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<!-- Summary Section -->
|
||||||
|
<div class="summary">
|
||||||
|
<h2>📊 API 配置總覽</h2>
|
||||||
|
<div class="summary-stats">
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-value" id="totalApis">3</div>
|
||||||
|
<div class="stat-label">API 總數</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-value" id="configuredApis" style="color: var(--success);">0</div>
|
||||||
|
<div class="stat-label">已配置</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-value" id="testedApis" style="color: var(--primary);">0</div>
|
||||||
|
<div class="stat-label">已測試</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-value" id="successApis" style="color: var(--success);">0</div>
|
||||||
|
<div class="stat-label">測試成功</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test All Button -->
|
||||||
|
<div class="test-all-section">
|
||||||
|
<button class="btn btn-success test-all-btn" onclick="testAllAPIs()">
|
||||||
|
🚀 測試所有已配置的 API
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gemini API Card -->
|
||||||
|
<div class="api-card" id="gemini-card">
|
||||||
|
<div class="api-header">
|
||||||
|
<div class="api-name">🔷 Google Gemini</div>
|
||||||
|
<div class="status-badge status-pending" id="gemini-status">未測試</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-info">
|
||||||
|
<div class="api-info-label">API Key:</div>
|
||||||
|
<div class="api-info-value">
|
||||||
|
<div class="api-key-display" id="gemini-key">
|
||||||
|
<span class="api-key-masked">••••••••••••••••</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-info-label">Endpoint:</div>
|
||||||
|
<div class="api-info-value">https://generativelanguage.googleapis.com</div>
|
||||||
|
<div class="api-info-label">狀態:</div>
|
||||||
|
<div class="api-info-value" id="gemini-config-status">等待檢測...</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-actions">
|
||||||
|
<button class="btn btn-primary" onclick="testAPI('gemini')" id="gemini-test-btn">
|
||||||
|
🧪 測試連線
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="result-message" id="gemini-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DeepSeek API Card -->
|
||||||
|
<div class="api-card" id="deepseek-card">
|
||||||
|
<div class="api-header">
|
||||||
|
<div class="api-name">🔵 DeepSeek</div>
|
||||||
|
<div class="status-badge status-pending" id="deepseek-status">未測試</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-info">
|
||||||
|
<div class="api-info-label">API Key:</div>
|
||||||
|
<div class="api-info-value">
|
||||||
|
<div class="api-key-display" id="deepseek-key">
|
||||||
|
<span class="api-key-masked">••••••••••••••••</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-info-label">Endpoint:</div>
|
||||||
|
<div class="api-info-value">https://api.deepseek.com/v1</div>
|
||||||
|
<div class="api-info-label">狀態:</div>
|
||||||
|
<div class="api-info-value" id="deepseek-config-status">等待檢測...</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-actions">
|
||||||
|
<button class="btn btn-primary" onclick="testAPI('deepseek')" id="deepseek-test-btn">
|
||||||
|
🧪 測試連線
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="result-message" id="deepseek-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OpenAI API Card -->
|
||||||
|
<div class="api-card" id="openai-card">
|
||||||
|
<div class="api-header">
|
||||||
|
<div class="api-name">🟢 OpenAI</div>
|
||||||
|
<div class="status-badge status-pending" id="openai-status">未測試</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-info">
|
||||||
|
<div class="api-info-label">API Key:</div>
|
||||||
|
<div class="api-info-value">
|
||||||
|
<div class="api-key-display" id="openai-key">
|
||||||
|
<span class="api-key-masked">••••••••••••••••</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-info-label">Endpoint:</div>
|
||||||
|
<div class="api-info-value">https://api.openai.com/v1</div>
|
||||||
|
<div class="api-info-label">狀態:</div>
|
||||||
|
<div class="api-info-value" id="openai-config-status">等待檢測...</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-actions">
|
||||||
|
<button class="btn btn-primary" onclick="testAPI('openai')" id="openai-test-btn">
|
||||||
|
🧪 測試連線
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="result-message" id="openai-result"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Check API configuration on page load
|
||||||
|
window.addEventListener('DOMContentLoaded', checkAPIConfiguration);
|
||||||
|
|
||||||
|
async function checkAPIConfiguration() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/llm/config');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
let configured = 0;
|
||||||
|
let tested = 0;
|
||||||
|
|
||||||
|
// Update UI for each API
|
||||||
|
['gemini', 'deepseek', 'openai'].forEach(api => {
|
||||||
|
const config = data[api];
|
||||||
|
const isConfigured = config && config.enabled;
|
||||||
|
|
||||||
|
if (isConfigured) {
|
||||||
|
configured++;
|
||||||
|
document.getElementById(`${api}-config-status`).textContent = '✓ 已配置';
|
||||||
|
document.getElementById(`${api}-config-status`).style.color = 'var(--success)';
|
||||||
|
|
||||||
|
// Show partial API key
|
||||||
|
if (config.api_key) {
|
||||||
|
const maskedKey = config.api_key.substring(0, 8) + '••••••••' + config.api_key.substring(config.api_key.length - 4);
|
||||||
|
document.getElementById(`${api}-key`).innerHTML = `<span>${maskedKey}</span>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById(`${api}-config-status`).textContent = '✗ 未配置';
|
||||||
|
document.getElementById(`${api}-config-status`).style.color = 'var(--danger)';
|
||||||
|
document.getElementById(`${api}-test-btn`).disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update summary
|
||||||
|
document.getElementById('configuredApis').textContent = configured;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showError('無法載入 API 配置');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAPI(apiName) {
|
||||||
|
const statusBadge = document.getElementById(`${apiName}-status`);
|
||||||
|
const resultDiv = document.getElementById(`${apiName}-result`);
|
||||||
|
const testBtn = document.getElementById(`${apiName}-test-btn`);
|
||||||
|
|
||||||
|
// Update UI to testing state
|
||||||
|
statusBadge.className = 'status-badge status-testing';
|
||||||
|
statusBadge.innerHTML = '測試中<span class="loading-spinner"></span>';
|
||||||
|
testBtn.disabled = true;
|
||||||
|
resultDiv.className = 'result-message';
|
||||||
|
resultDiv.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/llm/test/${apiName}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
statusBadge.className = 'status-badge status-success';
|
||||||
|
statusBadge.textContent = '✓ 連線成功';
|
||||||
|
resultDiv.className = 'result-message result-success show';
|
||||||
|
resultDiv.textContent = `✓ ${data.message}`;
|
||||||
|
updateSummary('tested', true);
|
||||||
|
} else {
|
||||||
|
statusBadge.className = 'status-badge status-error';
|
||||||
|
statusBadge.textContent = '✗ 連線失敗';
|
||||||
|
resultDiv.className = 'result-message result-error show';
|
||||||
|
resultDiv.textContent = `✗ ${data.message}`;
|
||||||
|
updateSummary('tested', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
statusBadge.className = 'status-badge status-error';
|
||||||
|
statusBadge.textContent = '✗ 錯誤';
|
||||||
|
resultDiv.className = 'result-message result-error show';
|
||||||
|
resultDiv.textContent = `✗ 發生錯誤: ${error.message}`;
|
||||||
|
updateSummary('tested', false);
|
||||||
|
} finally {
|
||||||
|
testBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAllAPIs() {
|
||||||
|
const apis = ['gemini', 'deepseek', 'openai'];
|
||||||
|
const testBtn = document.querySelector('.test-all-btn');
|
||||||
|
|
||||||
|
testBtn.disabled = true;
|
||||||
|
testBtn.innerHTML = '測試中<span class="loading-spinner"></span>';
|
||||||
|
|
||||||
|
for (const api of apis) {
|
||||||
|
const isEnabled = !document.getElementById(`${api}-test-btn`).disabled;
|
||||||
|
if (isEnabled) {
|
||||||
|
await testAPI(api);
|
||||||
|
// Small delay between tests
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testBtn.disabled = false;
|
||||||
|
testBtn.innerHTML = '🚀 測試所有已配置的 API';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSummary(type, success) {
|
||||||
|
const testedEl = document.getElementById('testedApis');
|
||||||
|
const successEl = document.getElementById('successApis');
|
||||||
|
|
||||||
|
if (type === 'tested') {
|
||||||
|
const currentTested = parseInt(testedEl.textContent) || 0;
|
||||||
|
testedEl.textContent = currentTested + 1;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const currentSuccess = parseInt(successEl.textContent) || 0;
|
||||||
|
successEl.textContent = currentSuccess + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
259
database_schema.sql
Normal file
259
database_schema.sql
Normal file
@@ -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
|
||||||
|
-- ============================================================
|
||||||
559
error_handler.js
Normal file
559
error_handler.js
Normal file
@@ -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 = `
|
||||||
|
<div class="error-modal">
|
||||||
|
<div class="error-modal-header ${type}">
|
||||||
|
<div class="error-icon">${icons[type]}</div>
|
||||||
|
<h3 class="error-title">${this.escapeHtml(title)}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="error-modal-body">
|
||||||
|
<div class="error-message">${this.escapeHtml(message)}</div>
|
||||||
|
${details ? `<div class="error-details">${this.escapeHtml(details)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="error-modal-footer">
|
||||||
|
<button class="error-btn error-btn-primary" onclick="window.errorHandler.closeModal(this)">
|
||||||
|
確定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="error-toast-icon">${icons[type]}</div>
|
||||||
|
<div class="error-toast-content">
|
||||||
|
${title ? `<div class="error-toast-title">${this.escapeHtml(title)}</div>` : ''}
|
||||||
|
<div class="error-toast-message">${this.escapeHtml(message)}</div>
|
||||||
|
</div>
|
||||||
|
<button class="error-toast-close" onclick="this.parentElement.remove()">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="error-modal">
|
||||||
|
<div class="error-modal-header info">
|
||||||
|
<div class="error-icon">❓</div>
|
||||||
|
<h3 class="error-title">${this.escapeHtml(title)}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="error-modal-body">
|
||||||
|
<div class="error-message">${this.escapeHtml(message)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="error-modal-footer">
|
||||||
|
<button class="error-btn error-btn-secondary" data-action="cancel">
|
||||||
|
${this.escapeHtml(cancelText)}
|
||||||
|
</button>
|
||||||
|
<button class="error-btn error-btn-primary" data-action="confirm">
|
||||||
|
${this.escapeHtml(confirmText)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
2184
index.html
Normal file
2184
index.html
Normal file
File diff suppressed because it is too large
Load Diff
130
init_database.py
Normal file
130
init_database.py
Normal file
@@ -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)
|
||||||
308
llm_config.py
Normal file
308
llm_config.py
Normal file
@@ -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()
|
||||||
23
requirements_full.txt
Normal file
23
requirements_full.txt
Normal file
@@ -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
|
||||||
286
start_server.py
Normal file
286
start_server.py
Normal file
@@ -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/<position_id>', 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/<api_name>', 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)
|
||||||
Reference in New Issue
Block a user