/** * 錯誤處理工具 * 統一處理應用程式中的錯誤 */ class ErrorHandler extends Error { constructor(statusCode, message, details = null) { super(message); this.statusCode = statusCode; this.details = details; this.timestamp = new Date().toISOString(); Error.captureStackTrace(this, this.constructor); } } /** * 錯誤類型定義 */ const ErrorTypes = { // 4xx Client Errors BAD_REQUEST: { code: 400, message: '請求參數錯誤' }, UNAUTHORIZED: { code: 401, message: '未授權訪問' }, FORBIDDEN: { code: 403, message: '禁止訪問' }, NOT_FOUND: { code: 404, message: '資源不存在' }, CONFLICT: { code: 409, message: '資源衝突' }, VALIDATION_ERROR: { code: 422, message: '資料驗證失敗' }, // 5xx Server Errors INTERNAL_ERROR: { code: 500, message: '伺服器內部錯誤' }, DATABASE_ERROR: { code: 500, message: '資料庫錯誤' }, LLM_API_ERROR: { code: 500, message: 'LLM API 錯誤' }, EXTERNAL_API_ERROR: { code: 502, message: '外部 API 錯誤' }, SERVICE_UNAVAILABLE: { code: 503, message: '服務暫時無法使用' }, }; /** * 建立錯誤實例 */ function createError(type, customMessage = null, details = null) { const errorType = ErrorTypes[type] || ErrorTypes.INTERNAL_ERROR; const message = customMessage || errorType.message; return new ErrorHandler(errorType.code, message, details); } /** * Express 錯誤處理中介層 */ function handleError(err, req, res, next) { const statusCode = err.statusCode || 500; const message = err.message || '未知錯誤'; // 記錄錯誤日誌 console.error('[Error]', { timestamp: new Date().toISOString(), path: req.path, method: req.method, statusCode, message, details: err.details, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, }); // 回傳錯誤訊息 res.status(statusCode).json({ success: false, error: { statusCode, message, details: err.details, timestamp: err.timestamp || new Date().toISOString(), path: req.path, // 只在開發環境顯示堆疊資訊 ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), }, }); } /** * 捕獲非同步錯誤的包裝器 */ function asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; } /** * 驗證錯誤處理 */ function validationError(errors) { return createError('VALIDATION_ERROR', '資料驗證失敗', errors); } /** * 資料庫錯誤處理 */ function databaseError(error) { const message = error.code === 'ER_DUP_ENTRY' ? '資料重複,請檢查輸入的內容' : '資料庫操作失敗'; return createError('DATABASE_ERROR', message, { code: error.code, sqlMessage: error.sqlMessage, }); } /** * LLM API 錯誤處理 */ function llmApiError(provider, error) { return createError('LLM_API_ERROR', `${provider} API 錯誤`, { provider, error: error.message, }); } /** * 前端錯誤處理輔助函數 */ const FrontendErrorHandler = { /** * 顯示錯誤訊息 */ showError(error, options = {}) { const { title = '錯誤', duration = 5000, showDetails = false, } = options; const errorData = error.response?.data?.error || error; const message = errorData.message || error.message || '發生未知錯誤'; const details = showDetails ? errorData.details : null; // 這裡可以整合不同的前端 UI 框架 return { title, message, details, statusCode: errorData.statusCode, timestamp: errorData.timestamp, duration, }; }, /** * 處理 API 錯誤 */ handleApiError(error) { if (error.response) { // 伺服器回應錯誤 const { status, data } = error.response; switch (status) { case 400: return this.showError(error, { title: '請求錯誤' }); case 401: return this.showError(error, { title: '未授權' }); case 403: return this.showError(error, { title: '禁止訪問' }); case 404: return this.showError(error, { title: '資源不存在' }); case 422: return this.showError(error, { title: '資料驗證失敗', showDetails: true }); case 500: return this.showError(error, { title: '伺服器錯誤' }); default: return this.showError(error); } } else if (error.request) { // 請求已發送但沒有收到回應 return { title: '網路錯誤', message: '無法連接到伺服器,請檢查網路連線', duration: 5000, }; } else { // 其他錯誤 return this.showError(error, { title: '錯誤' }); } }, /** * 驗證表單錯誤 */ handleValidationErrors(errors) { if (Array.isArray(errors)) { return errors.map(err => ({ field: err.field || err.param, message: err.message || err.msg, })); } return []; }, }; module.exports = { ErrorHandler, ErrorTypes, createError, handleError, asyncHandler, validationError, databaseError, llmApiError, FrontendErrorHandler, };