Files
hr-position-system/js/api.js
DonaldFang 方士碩 a068ef9704 fix: 改進 LLM API JSON 解析錯誤處理
- 增加智能 JSON 提取:自動查找首尾大括號
- 更詳細的錯誤訊息:顯示原始響應前 200 字符
- 更新錯誤提示建議
- 防止亂碼導致的解析失敗

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 01:29:44 +08:00

292 lines
8.1 KiB
JavaScript
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.

/**
* 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;
}
}