變更內容: - 所有資料表加上 HR_position_ 前綴 - 整理完整欄位顯示名稱與 ID 對照表 - 模組化 JS 檔案 (admin.js, ai.js, csv.js 等) - 專案結構優化 (docs/, scripts/, tests/ 等) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
309 lines
8.8 KiB
JavaScript
309 lines
8.8 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;
|
||
|
||
// 移除 DeepSeek-R1 等模型的思考標籤 <think>...</think>
|
||
responseText = responseText.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
||
|
||
// 移除 Markdown 代碼塊標記
|
||
responseText = responseText.replace(/```json\n?/gi, '').replace(/```\n?/g, '').trim();
|
||
|
||
// 移除可能的前導文字(如 "Here is the JSON:" 等)
|
||
responseText = responseText.replace(/^[\s\S]*?(?=\{)/i, '');
|
||
|
||
// 嘗試提取 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);
|
||
|
||
// 嘗試修復常見的 JSON 問題
|
||
try {
|
||
// 移除控制字符
|
||
const cleanedText = responseText
|
||
.replace(/[\x00-\x1F\x7F]/g, '') // 移除控制字符
|
||
.replace(/,\s*}/g, '}') // 移除末尾逗號
|
||
.replace(/,\s*]/g, ']'); // 移除陣列末尾逗號
|
||
return JSON.parse(cleanedText);
|
||
} catch (retryError) {
|
||
throw new Error(`LLM 返回的內容不是有效的 JSON 格式。\n\n原始響應前 300 字符:\n${responseText.substring(0, 300)}...`);
|
||
}
|
||
}
|
||
} 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;
|
||
}
|
||
}
|