- 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>
505 lines
18 KiB
HTML
505 lines
18 KiB
HTML
<!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>
|