Files
hr-performance-system/public/api-proxy-example.html
donald bf475d16c1 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>
2025-12-04 00:05:36 +08:00

505 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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