/** * API - API 呼叫函式 * 包含所有與後端 API 的通訊邏輯 */ import { API_BASE_URL } from './config.js'; import { showCopyableError } from './utils.js'; // ==================== LLM API ==================== /** * 獲取當前選擇的 Ollama 模型 * @returns {string} - 模型名稱 */ function getOllamaModel() { return localStorage.getItem('selectedOllamaModel') || 'deepseek-r1:8b'; } /** * 調用 Claude/Ollama LLM API * @param {string} prompt - Prompt 內容 * @param {string} api - API 類型('ollama' 或其他) * @returns {Promise} - 解析後的 JSON 回應 */ export async function callClaudeAPI(prompt, api = 'ollama') { try { // 準備請求資料 const requestData = { api: api, prompt: prompt, max_tokens: 2000 }; // 如果使用 Ollama API,加入選擇的模型 if (api === 'ollama') { requestData.model = getOllamaModel(); } // 調用後端 Flask API,避免 CORS 錯誤 const response = await fetch(`${API_BASE_URL}/llm/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestData) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `API 請求失敗: ${response.status}`); } const data = await response.json(); if (!data.success) { throw new Error(data.error || 'API 調用失敗'); } // 清理 JSON 代碼塊標記和其他格式 let responseText = data.text; // 移除 Markdown 代碼塊標記 responseText = responseText.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); // 嘗試提取 JSON 內容(如果包含其他文字) // 查找第一個 { 和最後一個 } const firstBrace = responseText.indexOf('{'); const lastBrace = responseText.lastIndexOf('}'); if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { responseText = responseText.substring(firstBrace, lastBrace + 1); } // 嘗試解析 JSON try { return JSON.parse(responseText); } catch (parseError) { // JSON 解析失敗,拋出更詳細的錯誤 console.error('JSON 解析失敗,原始響應:', responseText); throw new Error(`LLM 返回的內容不是有效的 JSON 格式。\n\n原始響應前 200 字符:\n${responseText.substring(0, 200)}...`); } } catch (error) { console.error('Error calling LLM API:', error); // 嘗試解析更詳細的錯誤訊息 let errorDetails = error.message; try { const errorJson = JSON.parse(error.message); errorDetails = JSON.stringify(errorJson, null, 2); } catch (e) { // 不是 JSON,使用原始訊息 } // 創建可複製的錯誤對話框 showCopyableError({ title: 'AI 生成錯誤', message: error.message, details: errorDetails, suggestions: [ 'Flask 後端已啟動 (python app.py)', '已在 .env 文件中配置有效的 LLM API Key', '網路連線正常', '確認 Prompt 要求返回純 JSON 格式', '嘗試使用不同的 LLM API (切換到其他模型)', '檢查 LLM 模型是否支援繁體中文' ] }); throw error; } } // ==================== Position API ==================== /** * 保存崗位至崗位清單 * @param {Object} positionData - 崗位資料 * @returns {Promise} - API 回應 */ export async function savePositionToList(positionData) { const response = await fetch(`${API_BASE_URL}/positions`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(positionData) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '保存失敗'); } return await response.json(); } /** * 載入崗位清單 * @returns {Promise} - 崗位清單 */ export async function loadPositionList() { const response = await fetch(`${API_BASE_URL}/position-list`); if (!response.ok) { throw new Error('載入崗位清單失敗'); } const data = await response.json(); return data.data || []; } /** * 獲取單一崗位描述 * @param {string} positionCode - 崗位編號 * @returns {Promise} - 崗位描述資料 */ export async function getPositionDescription(positionCode) { const response = await fetch(`${API_BASE_URL}/position-descriptions/${positionCode}`); if (!response.ok) { if (response.status === 404) { return null; // 未找到描述 } throw new Error('載入崗位描述失敗'); } const data = await response.json(); return data.data; } /** * 保存崗位描述 * @param {Object} descData - 崗位描述資料 * @returns {Promise} - API 回應 */ export async function savePositionDescription(descData) { const response = await fetch(`${API_BASE_URL}/position-descriptions`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(descData) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '保存崗位描述失敗'); } return await response.json(); } // ==================== CSV API ==================== /** * 下載崗位 CSV 範本 */ export async function downloadPositionCSVTemplate() { window.location.href = `${API_BASE_URL}/positions/csv-template`; } /** * 下載職務 CSV 範本 */ export async function downloadJobCSVTemplate() { window.location.href = `${API_BASE_URL}/jobs/csv-template`; } /** * 匯入崗位 CSV * @param {File} file - CSV 檔案 * @returns {Promise} - API 回應 */ export async function importPositionsCSV(file) { const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_BASE_URL}/positions/import-csv`, { method: 'POST', body: formData }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'CSV 匯入失敗'); } return await response.json(); } /** * 匯入職務 CSV * @param {File} file - CSV 檔案 * @returns {Promise} - API 回應 */ export async function importJobsCSV(file) { const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_BASE_URL}/jobs/import-csv`, { method: 'POST', body: formData }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'CSV 匯入失敗'); } return await response.json(); } /** * 匯出完整崗位資料 */ export async function exportCompletePositionData() { window.location.href = `${API_BASE_URL}/position-list/export`; } // ==================== Ollama Connection Test ==================== /** * 測試 Ollama 連線 * @returns {Promise} - 連線是否成功 */ export async function testOllamaConnection() { try { const response = await fetch(`${API_BASE_URL}/llm/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ api: 'ollama', model: getOllamaModel(), prompt: '請回答:「連線測試成功」', max_tokens: 50 }) }); if (!response.ok) { return false; } const data = await response.json(); return data.success === true; } catch (error) { console.error('Ollama 連線測試失敗:', error); return false; } }