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:
2025-12-04 00:46:53 +08:00
commit 29c1633e49
13 changed files with 6184 additions and 0 deletions

286
start_server.py Normal file
View 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)