import express from 'express'; import axios from 'axios'; import Analysis from '../models/Analysis.js'; import AuditLog from '../models/AuditLog.js'; import { asyncHandler } from '../middleware/errorHandler.js'; import { requireAuth } from '../middleware/auth.js'; import { ollamaConfig, query } from '../config.js'; const router = express.Router(); /** * 從資料庫取得啟用的 LLM 配置 */ async function getActiveLLMConfig() { const [config] = await query( `SELECT provider_name, api_endpoint, api_key, model_name, temperature, max_tokens, timeout_seconds FROM llm_configs WHERE is_active = 1 LIMIT 1` ); // 如果沒有資料庫配置,使用環境變數的 Ollama 配置 if (!config) { return { provider_name: 'Ollama', api_endpoint: ollamaConfig.apiUrl, api_key: null, model_name: ollamaConfig.model, temperature: ollamaConfig.temperature, max_tokens: ollamaConfig.maxTokens, timeout_seconds: ollamaConfig.timeout / 1000 }; } return config; } /** * POST /api/analyze * 執行 5 Why 分析 */ router.post('/', requireAuth, asyncHandler(async (req, res) => { const { finding, jobContent, outputLanguage = 'zh-TW' } = req.body; const userId = req.session.userId; // 驗證輸入 if (!finding || !jobContent) { return res.status(400).json({ success: false, error: '請填寫所有必填欄位' }); } const startTime = Date.now(); try { // 取得啟用的 LLM 配置 const llmConfig = await getActiveLLMConfig(); // 建立分析記錄 const analysis = await Analysis.create({ user_id: userId, finding, job_content: jobContent, output_language: outputLanguage }); // 更新狀態為處理中 await Analysis.updateStatus(analysis.id, 'processing'); // 建立 AI 提示詞 const languageNames = { 'zh-TW': '繁體中文', 'zh-CN': '简体中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어', 'vi': 'Tiếng Việt', 'th': 'ภาษาไทย' }; const langName = languageNames[outputLanguage] || '繁體中文'; const prompt = `你是一位專精於「5 Why 根因分析法」的資深顧問。請嚴格遵循以下五大執行要項進行分析: ## 五大執行要項 ### 1. 精準定義問題(描述現象,而非結論) - 第一步必須客觀描述「發生了什麼事」,而非直接跳入「我認為是甚麼問題」 - 具體化:包含人、事、時、地、物(5W1H) ### 2. 聚焦於「流程」與「系統」,而非「人」 - 若答案是「人為疏失」,請繼續追問:「為什麼系統允許這個疏失發生?」 - 原則:解決問題的機制,而非責備犯錯的人 ### 3. 基於「事實」與「現場」,拒絕「猜測」 - 每一個「為什麼」的回答,都必須是可查證的事實 - 若無法確認,應標註需要驗證的假設 ### 4. 邏輯的「雙向檢核」 - 順向檢查:若原因 X 發生,是否必然導致結果 Y? - 逆向檢查:若消除了原因 X,結果 Y 是否就不會發生? ### 5. 止於「可執行的對策」 - 根本原因必須能對應到一個「永久性對策」(不再發生) - 不僅是「暫時性對策」(如:重新訓練、加強宣導) --- ## 待分析內容 **Finding(發現的問題/現象):** ${finding} **工作內容背景:** ${jobContent} --- ## 輸出要求 請提供 **三個不同角度** 的 5 Why 分析,每個分析從不同的切入點出發(例如:流程面、系統面、管理面、設備面、環境面等)。 注意: - 5 Why 的目的不是「湊滿五個問題」,而是穿透表面症狀直達根本原因 - 若在第 3 或第 4 個 Why 就已找到真正的根本原因,可以停止(設為 null) - 每個 Why 必須標註是「已驗證事實」還是「待驗證假設」 - 最終對策必須是「永久性對策」 ⚠️ 重要:請使用 **${langName}** 語言回覆所有內容。 請用以下 JSON 格式回覆(不要加任何 markdown 標記): { "problemRestatement": "根據 5W1H 重新描述的問題定義", "analyses": [ { "perspective": "分析角度(如:流程面)", "perspectiveIcon": "適合的 emoji", "whys": [ { "level": 1, "question": "為什麼...?", "answer": "因為...", "isVerified": true, "verificationNote": "已確認/需驗證:說明" } ], "rootCause": "根本原因(系統/流程層面)", "logicCheck": { "forward": "順向檢核:如果[原因]發生,則[結果]必然發生", "backward": "逆向檢核:如果消除[原因],則[結果]不會發生", "isValid": true }, "countermeasure": { "permanent": "永久性對策(系統性解決方案)", "actionItems": ["具體行動項目1", "具體行動項目2"], "avoidList": ["避免的暫時性做法(如:加強宣導)"] } } ] }`; // 呼叫 LLM API(支援 DeepSeek, Ollama 等) const response = await axios.post( `${llmConfig.api_endpoint}/v1/chat/completions`, { model: llmConfig.model_name, messages: [ { role: 'system', content: 'You are an expert consultant specializing in 5 Why root cause analysis. You always respond in valid JSON format without any markdown code blocks.' }, { role: 'user', content: prompt } ], temperature: llmConfig.temperature, max_tokens: llmConfig.max_tokens, stream: false }, { timeout: llmConfig.timeout_seconds * 1000, headers: { 'Content-Type': 'application/json', ...(llmConfig.api_key && { 'Authorization': `Bearer ${llmConfig.api_key}` }) } } ); // 處理回應 if (!response.data || !response.data.choices || !response.data.choices[0]) { throw new Error(`Invalid response from ${llmConfig.provider_name} API`); } const content = response.data.choices[0].message.content; const cleanContent = content.replace(/```json|```/g, '').trim(); const result = JSON.parse(cleanContent); // 計算處理時間 const processingTime = Math.floor((Date.now() - startTime) / 1000); // 儲存結果 await Analysis.saveResult(analysis.id, { problem_restatement: result.problemRestatement, analysis_result: result, processing_time: processingTime }); // 記錄稽核日誌 await AuditLog.logCreate( userId, 'analysis', analysis.id, { finding, outputLanguage }, req.ip, req.get('user-agent') ); res.json({ success: true, message: '分析完成', data: { id: analysis.id, problemRestatement: result.problemRestatement, analyses: result.analyses, processingTime } }); } catch (error) { console.error('Analysis error:', error); // 更新分析狀態為失敗 if (analysis && analysis.id) { await Analysis.updateStatus(analysis.id, 'failed', error.message); } res.status(500).json({ success: false, error: '分析失敗', message: error.message }); } })); /** * POST /api/analyze/translate * 翻譯分析結果 */ router.post('/translate', requireAuth, asyncHandler(async (req, res) => { const { analysisId, targetLanguage } = req.body; if (!analysisId || !targetLanguage) { return res.status(400).json({ success: false, error: '請提供分析 ID 和目標語言' }); } try { // 取得啟用的 LLM 配置 const llmConfig = await getActiveLLMConfig(); // 取得分析結果 const analysis = await Analysis.findById(analysisId); if (!analysis) { return res.status(404).json({ success: false, error: '找不到分析記錄' }); } const languageNames = { 'zh-TW': '繁體中文', 'zh-CN': '简体中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어', 'vi': 'Tiếng Việt', 'th': 'ภาษาไทย' }; const langName = languageNames[targetLanguage] || '繁體中文'; const prompt = `請將以下 5 Why 分析結果翻譯成 **${langName}**。 原始內容: ${JSON.stringify(analysis.analysis_result, null, 2)} 請保持完全相同的 JSON 結構,只翻譯文字內容。 請用以下 JSON 格式回覆(不要加任何 markdown 標記): { "problemRestatement": "...", "analyses": [...] }`; const response = await axios.post( `${llmConfig.api_endpoint}/v1/chat/completions`, { model: llmConfig.model_name, messages: [ { role: 'system', content: 'You are a professional translator. You always respond in valid JSON format without any markdown code blocks.' }, { role: 'user', content: prompt } ], temperature: 0.3, max_tokens: llmConfig.max_tokens, stream: false }, { timeout: llmConfig.timeout_seconds * 1000, headers: { 'Content-Type': 'application/json', ...(llmConfig.api_key && { 'Authorization': `Bearer ${llmConfig.api_key}` }) } } ); const content = response.data.choices[0].message.content; const cleanContent = content.replace(/```json|```/g, '').trim(); const result = JSON.parse(cleanContent); res.json({ success: true, message: '翻譯完成', data: result }); } catch (error) { console.error('Translation error:', error); res.status(500).json({ success: false, error: '翻譯失敗', message: error.message }); } })); /** * GET /api/analyze/history * 取得分析歷史 */ router.get('/history', requireAuth, asyncHandler(async (req, res) => { const userId = req.session.userId; const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const filters = { status: req.query.status, date_from: req.query.date_from, date_to: req.query.date_to, search: req.query.search }; const result = await Analysis.getByUserId(userId, page, limit, filters); res.json({ success: true, ...result }); })); /** * GET /api/analyze/:id * 取得特定分析詳細資料 */ router.get('/:id', requireAuth, asyncHandler(async (req, res) => { const analysisId = parseInt(req.params.id); const userId = req.session.userId; const userRole = req.session.userRole; const analysis = await Analysis.getFullAnalysis(analysisId); if (!analysis) { return res.status(404).json({ success: false, error: '找不到分析記錄' }); } // 檢查權限:只能查看自己的分析,除非是管理者 if (analysis.user_id !== userId && userRole !== 'admin' && userRole !== 'super_admin') { return res.status(403).json({ success: false, error: '無權存取此分析' }); } res.json({ success: true, data: analysis }); })); /** * DELETE /api/analyze/:id * 刪除分析記錄 */ router.delete('/:id', requireAuth, asyncHandler(async (req, res) => { const analysisId = parseInt(req.params.id); const userId = req.session.userId; const userRole = req.session.userRole; const analysis = await Analysis.findById(analysisId); if (!analysis) { return res.status(404).json({ success: false, error: '找不到分析記錄' }); } // 檢查權限 if (analysis.user_id !== userId && userRole !== 'admin' && userRole !== 'super_admin') { return res.status(403).json({ success: false, error: '無權刪除此分析' }); } await Analysis.delete(analysisId); // 記錄稽核日誌 await AuditLog.logDelete( userId, 'analysis', analysisId, { finding: analysis.finding }, req.ip, req.get('user-agent') ); res.json({ success: true, message: '已刪除分析記錄' }); })); export default router;