- 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>
287 lines
9.4 KiB
Python
287 lines
9.4 KiB
Python
"""
|
|
啟動 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)
|