Add Claude API support and fix CORS issues
- Add Claude API integration to LLM service - Create Express backend server with CORS support - Add API proxy example page - Fix CORS errors by routing through backend - Update LLM configuration to support Claude - Add package.json with dependencies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,8 @@
|
||||
"Bash(git config:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)"
|
||||
"Bash(git push:*)",
|
||||
"Bash(curl:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -33,8 +33,18 @@ const llmConfig = {
|
||||
timeout: 30000,
|
||||
},
|
||||
|
||||
// Claude Configuration
|
||||
claude: {
|
||||
apiKey: process.env.CLAUDE_API_KEY,
|
||||
apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1',
|
||||
model: process.env.CLAUDE_MODEL || 'claude-3-5-sonnet-20241022',
|
||||
enabled: !!process.env.CLAUDE_API_KEY,
|
||||
timeout: 30000,
|
||||
version: '2023-06-01', // Anthropic API version
|
||||
},
|
||||
|
||||
// Default LLM Provider
|
||||
defaultProvider: 'gemini',
|
||||
defaultProvider: 'claude',
|
||||
|
||||
// Common Settings
|
||||
maxTokens: 2000,
|
||||
@@ -49,6 +59,7 @@ function getEnabledProviders() {
|
||||
if (llmConfig.gemini.enabled) enabled.push('gemini');
|
||||
if (llmConfig.deepseek.enabled) enabled.push('deepseek');
|
||||
if (llmConfig.openai.enabled) enabled.push('openai');
|
||||
if (llmConfig.claude.enabled) enabled.push('claude');
|
||||
return enabled;
|
||||
}
|
||||
|
||||
|
||||
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "hr-performance-system",
|
||||
"version": "1.0.0",
|
||||
"description": "HR 績效評核系統 - 四卡循環管理平台",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"test": "jest",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write \"**/*.{js,jsx,json,md}\""
|
||||
},
|
||||
"keywords": [
|
||||
"hr",
|
||||
"performance",
|
||||
"management",
|
||||
"four-card-system"
|
||||
],
|
||||
"author": "Donald",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql2": "^3.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"eslint": "^8.55.0",
|
||||
"prettier": "^3.1.1",
|
||||
"jest": "^29.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
504
public/api-proxy-example.html
Normal file
504
public/api-proxy-example.html
Normal file
@@ -0,0 +1,504 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API 代理使用範例 - HR 績效系統</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft JhengHei', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 20px;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border-left: 4px solid #dc2626;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1f2937;
|
||||
color: #f9fafb;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.code-block .comment {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.code-block .keyword {
|
||||
color: #c084fc;
|
||||
}
|
||||
|
||||
.code-block .string {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.code-block .function {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin: 8px 8px 8px 0;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 12px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.result-box pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.comparison-item.wrong {
|
||||
border-color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.comparison-item.correct {
|
||||
border-color: #10b981;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.comparison-item h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔧 API 代理使用範例</h1>
|
||||
<p>正確的方式:透過後端伺服器呼叫 LLM API</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- 問題說明 -->
|
||||
<div class="section">
|
||||
<h2>❌ 您遇到的錯誤</h2>
|
||||
<div class="alert alert-error">
|
||||
<strong>CORS 錯誤:</strong>
|
||||
<p>Access to fetch at 'https://api.anthropic.com/v1/messages' from origin 'http://127.0.0.1:5000' has been blocked by CORS policy</p>
|
||||
</div>
|
||||
<div class="alert alert-error">
|
||||
<strong>Storage 錯誤:</strong>
|
||||
<p>Access to storage is not allowed from this context</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 錯誤原因 -->
|
||||
<div class="section">
|
||||
<h2>🤔 為什麼會發生這個錯誤?</h2>
|
||||
<div class="alert alert-info">
|
||||
<strong>原因:</strong>
|
||||
<ul style="margin-left: 20px; margin-top: 8px;">
|
||||
<li><strong>安全限制:</strong>瀏覽器的 CORS 政策不允許直接從前端呼叫第三方 API</li>
|
||||
<li><strong>API 金鑰暴露:</strong>在前端直接使用 API 金鑰會洩露給所有使用者</li>
|
||||
<li><strong>Storage 限制:</strong>本地檔案 (file://) 無法使用 localStorage</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 解決方案對比 -->
|
||||
<div class="section">
|
||||
<h2>🔄 錯誤 vs 正確的做法</h2>
|
||||
<div class="comparison">
|
||||
<div class="comparison-item wrong">
|
||||
<h3>❌ 錯誤:前端直接呼叫</h3>
|
||||
<div class="code-block" style="background: #7f1d1d; color: #fecaca;">
|
||||
<span class="comment">// 不安全!API 金鑰會暴露</span>
|
||||
<span class="keyword">fetch</span>(<span class="string">'https://api.anthropic.com/v1/messages'</span>, {
|
||||
headers: {
|
||||
<span class="string">'x-api-key'</span>: <span class="string">'sk-ant-...'</span>
|
||||
}
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-item correct">
|
||||
<h3>✅ 正確:透過後端代理</h3>
|
||||
<div class="code-block" style="background: #064e3b; color: #a7f3d0;">
|
||||
<span class="comment">// 安全!API 金鑰在後端</span>
|
||||
<span class="keyword">fetch</span>(<span class="string">'http://localhost:3000/api/llm/generate'</span>, {
|
||||
method: <span class="string">'POST'</span>,
|
||||
body: <span class="function">JSON.stringify</span>({...})
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 正確的使用方式 -->
|
||||
<div class="section">
|
||||
<h2>✅ 正確的使用方式</h2>
|
||||
|
||||
<h3 style="font-size: 16px; margin: 20px 0 12px 0;">1. 測試 Claude API 連線</h3>
|
||||
<button class="button" onclick="testClaudeConnection()">
|
||||
<span id="test-btn-text">測試 Claude 連線</span>
|
||||
</button>
|
||||
<div class="result-box" id="test-result">等待測試...</div>
|
||||
|
||||
<h3 style="font-size: 16px; margin: 20px 0 12px 0;">2. 使用 Claude 生成內容</h3>
|
||||
<button class="button" onclick="generateWithClaude()">
|
||||
<span id="generate-btn-text">生成內容範例</span>
|
||||
</button>
|
||||
<div class="result-box" id="generate-result">等待生成...</div>
|
||||
|
||||
<h3 style="font-size: 16px; margin: 20px 0 12px 0;">3. 測試所有 LLM</h3>
|
||||
<button class="button" onclick="testAllLLMs()">
|
||||
<span id="test-all-btn-text">測試所有 LLM</span>
|
||||
</button>
|
||||
<div class="result-box" id="test-all-result">等待測試...</div>
|
||||
</div>
|
||||
|
||||
<!-- 程式碼範例 -->
|
||||
<div class="section">
|
||||
<h2>📝 程式碼範例</h2>
|
||||
|
||||
<h3 style="font-size: 16px; margin: 20px 0 12px 0;">測試連線</h3>
|
||||
<div class="code-block">
|
||||
<span class="comment">// 測試 Claude API 連線</span>
|
||||
<span class="keyword">async function</span> <span class="function">testClaudeConnection</span>() {
|
||||
<span class="keyword">try</span> {
|
||||
<span class="keyword">const</span> response = <span class="keyword">await</span> <span class="function">fetch</span>(<span class="string">'http://localhost:3000/api/llm/test/claude'</span>, {
|
||||
method: <span class="string">'POST'</span>,
|
||||
headers: { <span class="string">'Content-Type'</span>: <span class="string">'application/json'</span> }
|
||||
});
|
||||
|
||||
<span class="keyword">const</span> result = <span class="keyword">await</span> response.<span class="function">json</span>();
|
||||
console.<span class="function">log</span>(result);
|
||||
} <span class="keyword">catch</span> (error) {
|
||||
console.<span class="function">error</span>(error);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 style="font-size: 16px; margin: 20px 0 12px 0;">生成內容</h3>
|
||||
<div class="code-block">
|
||||
<span class="comment">// 使用 Claude 生成內容</span>
|
||||
<span class="keyword">async function</span> <span class="function">generateContent</span>(prompt) {
|
||||
<span class="keyword">try</span> {
|
||||
<span class="keyword">const</span> response = <span class="keyword">await</span> <span class="function">fetch</span>(<span class="string">'http://localhost:3000/api/llm/generate'</span>, {
|
||||
method: <span class="string">'POST'</span>,
|
||||
headers: { <span class="string">'Content-Type'</span>: <span class="string">'application/json'</span> },
|
||||
body: <span class="function">JSON.stringify</span>({
|
||||
prompt: prompt,
|
||||
provider: <span class="string">'claude'</span>,
|
||||
options: {
|
||||
temperature: <span class="string">0.7</span>,
|
||||
maxTokens: <span class="string">2000</span>
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
<span class="keyword">const</span> result = <span class="keyword">await</span> response.<span class="function">json</span>();
|
||||
<span class="keyword">return</span> result.content;
|
||||
} <span class="keyword">catch</span> (error) {
|
||||
console.<span class="function">error</span>(error);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 設定步驟 -->
|
||||
<div class="section">
|
||||
<h2>⚙️ 設定步驟</h2>
|
||||
<div class="alert alert-info">
|
||||
<strong>1. 設定環境變數 (.env)</strong>
|
||||
<div class="code-block" style="margin-top: 12px;">
|
||||
CLAUDE_API_KEY=<span class="string">your_claude_api_key_here</span>
|
||||
CLAUDE_API_URL=<span class="string">https://api.anthropic.com/v1</span>
|
||||
CLAUDE_MODEL=<span class="string">claude-3-5-sonnet-20241022</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>2. 啟動後端伺服器</strong>
|
||||
<div class="code-block" style="margin-top: 12px;">
|
||||
<span class="comment"># 安裝依賴</span>
|
||||
npm install
|
||||
|
||||
<span class="comment"># 啟動開發伺服器</span>
|
||||
npm run dev
|
||||
|
||||
<span class="comment"># 或生產環境</span>
|
||||
npm start
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<strong>3. 完成!</strong>
|
||||
<p style="margin-top: 8px;">現在可以透過 <code>http://localhost:3000/api/llm/*</code> 安全地呼叫 LLM API 了</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:3000/api/llm';
|
||||
|
||||
// 測試 Claude 連線
|
||||
async function testClaudeConnection() {
|
||||
const btn = document.getElementById('test-btn-text');
|
||||
const result = document.getElementById('test-result');
|
||||
|
||||
btn.innerHTML = '<span class="loading"></span> 測試中...';
|
||||
result.innerHTML = '正在連線到 Claude API...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/test/claude`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
result.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
||||
|
||||
if (data.success) {
|
||||
result.style.background = '#f0fdf4';
|
||||
result.style.borderLeft = '4px solid #10b981';
|
||||
} else {
|
||||
result.style.background = '#fef2f2';
|
||||
result.style.borderLeft = '4px solid #dc2626';
|
||||
}
|
||||
} catch (error) {
|
||||
result.innerHTML = `<pre style="color: #dc2626;">錯誤: ${error.message}\n\n請確認:\n1. 後端伺服器已啟動 (npm run dev)\n2. API 地址正確\n3. 網路連線正常</pre>`;
|
||||
result.style.background = '#fef2f2';
|
||||
result.style.borderLeft = '4px solid #dc2626';
|
||||
} finally {
|
||||
btn.textContent = '測試 Claude 連線';
|
||||
}
|
||||
}
|
||||
|
||||
// 生成內容
|
||||
async function generateWithClaude() {
|
||||
const btn = document.getElementById('generate-btn-text');
|
||||
const result = document.getElementById('generate-result');
|
||||
|
||||
btn.innerHTML = '<span class="loading"></span> 生成中...';
|
||||
result.innerHTML = '正在使用 Claude 生成內容...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: '請用一句話介紹什麼是 HR 績效評核系統',
|
||||
provider: 'claude',
|
||||
options: {
|
||||
temperature: 0.7,
|
||||
maxTokens: 200
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
result.innerHTML = `<strong>生成結果:</strong><p style="margin-top: 12px; line-height: 1.6;">${data.content}</p>`;
|
||||
result.style.background = '#f0fdf4';
|
||||
result.style.borderLeft = '4px solid #10b981';
|
||||
} else {
|
||||
result.innerHTML = `<pre style="color: #dc2626;">${JSON.stringify(data, null, 2)}</pre>`;
|
||||
result.style.background = '#fef2f2';
|
||||
result.style.borderLeft = '4px solid #dc2626';
|
||||
}
|
||||
} catch (error) {
|
||||
result.innerHTML = `<pre style="color: #dc2626;">錯誤: ${error.message}</pre>`;
|
||||
result.style.background = '#fef2f2';
|
||||
result.style.borderLeft = '4px solid #dc2626';
|
||||
} finally {
|
||||
btn.textContent = '生成內容範例';
|
||||
}
|
||||
}
|
||||
|
||||
// 測試所有 LLM
|
||||
async function testAllLLMs() {
|
||||
const btn = document.getElementById('test-all-btn-text');
|
||||
const result = document.getElementById('test-all-result');
|
||||
|
||||
btn.innerHTML = '<span class="loading"></span> 測試中...';
|
||||
result.innerHTML = '正在測試所有 LLM API...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/test/all`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
let html = '<div style="display: grid; gap: 12px;">';
|
||||
for (const [provider, result] of Object.entries(data)) {
|
||||
const status = result.success ? '✅' : '❌';
|
||||
const color = result.success ? '#10b981' : '#dc2626';
|
||||
html += `
|
||||
<div style="padding: 12px; border-left: 4px solid ${color}; background: ${result.success ? '#f0fdf4' : '#fef2f2'}; border-radius: 4px;">
|
||||
<strong>${status} ${provider}</strong>: ${result.message}
|
||||
${result.model ? `<br><small>模型: ${result.model}</small>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += '</div>';
|
||||
result.innerHTML = html;
|
||||
} catch (error) {
|
||||
result.innerHTML = `<pre style="color: #dc2626;">錯誤: ${error.message}</pre>`;
|
||||
result.style.background = '#fef2f2';
|
||||
result.style.borderLeft = '4px solid #dc2626';
|
||||
} finally {
|
||||
btn.textContent = '測試所有 LLM';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,6 +35,15 @@ router.post('/test/openai', asyncHandler(async (req, res) => {
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
/**
|
||||
* POST /api/llm/test/claude
|
||||
* 測試 Claude API 連線
|
||||
*/
|
||||
router.post('/test/claude', asyncHandler(async (req, res) => {
|
||||
const result = await llmService.testClaudeConnection();
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
/**
|
||||
* POST /api/llm/test/all
|
||||
* 測試所有 LLM API 連線
|
||||
|
||||
151
server.js
Normal file
151
server.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Express Server
|
||||
* HR 績效評核系統後端伺服器
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
|
||||
// Import routes
|
||||
const llmRoutes = require('./routes/llm.routes');
|
||||
|
||||
// Import error handler
|
||||
const { handleError } = require('./utils/errorHandler');
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// ============================================
|
||||
// Middleware
|
||||
// ============================================
|
||||
|
||||
// Security headers
|
||||
app.use(helmet());
|
||||
|
||||
// CORS configuration
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || '*',
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Logging
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(morgan('dev'));
|
||||
} else {
|
||||
app.use(morgan('combined'));
|
||||
}
|
||||
|
||||
// Static files
|
||||
app.use(express.static('public'));
|
||||
|
||||
// ============================================
|
||||
// Routes
|
||||
// ============================================
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'HR Performance System API is running',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV,
|
||||
});
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.use('/api/llm', llmRoutes);
|
||||
|
||||
// Root endpoint
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
name: 'HR Performance System API',
|
||||
version: '1.0.0',
|
||||
description: '四卡循環績效管理系統',
|
||||
endpoints: {
|
||||
health: '/health',
|
||||
llm: '/api/llm',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Error Handling
|
||||
// ============================================
|
||||
|
||||
// 404 handler
|
||||
app.use((req, res, next) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
statusCode: 404,
|
||||
message: `Cannot ${req.method} ${req.path}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: req.path,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Global error handler
|
||||
app.use(handleError);
|
||||
|
||||
// ============================================
|
||||
// Server Start
|
||||
// ============================================
|
||||
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log('='.repeat(50));
|
||||
console.log('🚀 HR Performance System API Server');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`📡 Server running on: http://localhost:${PORT}`);
|
||||
console.log(`🌍 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`📅 Started at: ${new Date().toLocaleString('zh-TW')}`);
|
||||
console.log('='.repeat(50));
|
||||
console.log('\n📚 Available endpoints:');
|
||||
console.log(` GET / - API information`);
|
||||
console.log(` GET /health - Health check`);
|
||||
console.log(` POST /api/llm/test/* - Test LLM connections`);
|
||||
console.log(` POST /api/llm/generate - Generate content with LLM`);
|
||||
console.log('\n✨ Server is ready to accept connections!\n');
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\n⚠️ SIGTERM received. Shutting down gracefully...');
|
||||
server.close(() => {
|
||||
console.log('✅ Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n⚠️ SIGINT received. Shutting down gracefully...');
|
||||
server.close(() => {
|
||||
console.log('✅ Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Uncaught exception handler
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('❌ Uncaught Exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Unhandled rejection handler
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -193,6 +193,68 @@ class LLMService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 測試 Claude API 連線
|
||||
*/
|
||||
async testClaudeConnection() {
|
||||
try {
|
||||
if (!isProviderEnabled('claude')) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Claude API key not configured',
|
||||
provider: 'claude',
|
||||
};
|
||||
}
|
||||
|
||||
const config = getProviderConfig('claude');
|
||||
const url = `${config.apiUrl}/messages`;
|
||||
|
||||
const response = await axios.post(
|
||||
url,
|
||||
{
|
||||
model: config.model,
|
||||
max_tokens: 50,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello, this is a connection test.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': config.apiKey,
|
||||
'anthropic-version': config.version,
|
||||
},
|
||||
timeout: config.timeout,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200 && response.data.content) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Claude API connection successful',
|
||||
provider: 'claude',
|
||||
model: config.model,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unexpected response from Claude API',
|
||||
provider: 'claude',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.error?.message || error.message,
|
||||
provider: 'claude',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 測試所有 LLM 連線
|
||||
*/
|
||||
@@ -201,6 +263,7 @@ class LLMService {
|
||||
gemini: await this.testGeminiConnection(),
|
||||
deepseek: await this.testDeepSeekConnection(),
|
||||
openai: await this.testOpenAIConnection(),
|
||||
claude: await this.testClaudeConnection(),
|
||||
};
|
||||
|
||||
return results;
|
||||
@@ -342,6 +405,55 @@ class LLMService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Claude 生成內容
|
||||
*/
|
||||
async generateWithClaude(prompt, options = {}) {
|
||||
try {
|
||||
if (!isProviderEnabled('claude')) {
|
||||
throw new Error('Claude API not configured');
|
||||
}
|
||||
|
||||
const config = getProviderConfig('claude');
|
||||
const url = `${config.apiUrl}/messages`;
|
||||
|
||||
const response = await axios.post(
|
||||
url,
|
||||
{
|
||||
model: config.model,
|
||||
max_tokens: options.maxTokens || llmConfig.maxTokens,
|
||||
temperature: options.temperature || llmConfig.temperature,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': config.apiKey,
|
||||
'anthropic-version': config.version,
|
||||
},
|
||||
timeout: config.timeout,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data?.content?.[0]?.text) {
|
||||
return {
|
||||
success: true,
|
||||
content: response.data.content[0].text,
|
||||
provider: 'claude',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Invalid response format from Claude');
|
||||
} catch (error) {
|
||||
throw new Error(`Claude API error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用預設或指定的 LLM 生成內容
|
||||
*/
|
||||
@@ -355,6 +467,8 @@ class LLMService {
|
||||
return await this.generateWithDeepSeek(prompt, options);
|
||||
case 'openai':
|
||||
return await this.generateWithOpenAI(prompt, options);
|
||||
case 'claude':
|
||||
return await this.generateWithClaude(prompt, options);
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${selectedProvider}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user