- 增加智能 JSON 提取:自動查找首尾大括號 - 更詳細的錯誤訊息:顯示原始響應前 200 字符 - 更新錯誤提示建議 - 防止亂碼導致的解析失敗 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
292 lines
8.1 KiB
JavaScript
292 lines
8.1 KiB
JavaScript
/**
|
||
* 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<Object>} - 解析後的 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<Object>} - 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<Array>} - 崗位清單
|
||
*/
|
||
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<Object>} - 崗位描述資料
|
||
*/
|
||
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<Object>} - 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<Object>} - 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<Object>} - 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<boolean>} - 連線是否成功
|
||
*/
|
||
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;
|
||
}
|
||
}
|