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:
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