Skip to main content

錯誤處理概述

正確的錯誤處理是建構穩健應用程式的關鍵。「聽有 AI」API 提供詳細的錯誤資訊,幫助您快速診斷和解決問題。本指南將介紹常見的錯誤類型、處理方式和最佳實踐。

錯誤回應格式

所有 API 錯誤都遵循統一的格式:
{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "提供的 API 金鑰無效",
    "details": {
      "timestamp": "2024-01-15T10:30:00Z",
      "request_id": "req_1234567890abcdef",
      "suggestion": "請檢查您的 API 金鑰是否正確"
    }
  }
}

錯誤類型分類

1. 身份驗證錯誤

INVALID_API_KEY

API 金鑰無效或格式錯誤
try {
  const result = await client.transcribe(audioData);
} catch (error) {
  if (error.code === 'INVALID_API_KEY') {
    console.error('API 金鑰無效,請檢查設定');
    // 重新設定 API 金鑰或提示使用者
    await refreshApiKey();
  }
}

PROJECT_NOT_FOUND

專案 ID 不存在或無權限存取
if (error.code === 'PROJECT_NOT_FOUND') {
  console.error('專案不存在,請檢查專案 ID');
  // 提示使用者選擇正確的專案
  await selectProject();
}

TOKEN_EXPIRED

存取權杖已過期
if (error.code === 'TOKEN_EXPIRED') {
  console.log('權杖已過期,正在重新整理...');
  await refreshToken();
  // 重試原始請求
  return await retryRequest();
}

2. 配額與限制錯誤

QUOTA_EXCEEDED

使用配額已用完
if (error.code === 'QUOTA_EXCEEDED') {
  const resetDate = error.details.quota_reset_date;
  console.error(`配額已用完,將於 ${resetDate} 重置`);
  
  // 提示使用者升級方案或等待重置
  await handleQuotaExceeded(resetDate);
}

RATE_LIMIT_EXCEEDED

請求頻率超過限制
if (error.code === 'RATE_LIMIT_EXCEEDED') {
  const retryAfter = error.details.retry_after_seconds;
  console.log(`請求頻率過高,${retryAfter} 秒後重試`);
  
  // 實作指數退避重試
  await exponentialBackoff(retryAfter);
}

CONCURRENT_LIMIT_EXCEEDED

並發連線數超過限制
if (error.code === 'CONCURRENT_LIMIT_EXCEEDED') {
  const maxConnections = error.details.max_concurrent_connections;
  console.error(`並發連線數超過限制 (${maxConnections})`);
  
  // 等待其他連線結束或實作連線池
  await waitForAvailableConnection();
}

3. 音訊相關錯誤

AUDIO_TOO_SHORT

音訊檔案太短
if (error.code === 'AUDIO_TOO_SHORT') {
  const minDuration = error.details.min_duration_seconds;
  console.error(`音訊太短,最少需要 ${minDuration} 秒`);
  
  // 提示使用者重新錄製或合併音檔
  await handleShortAudio(minDuration);
}

AUDIO_TOO_LONG

音訊檔案太長
if (error.code === 'AUDIO_TOO_LONG') {
  const maxDuration = error.details.max_duration_seconds;
  console.error(`音訊太長,最多支援 ${maxDuration} 秒`);
  
  // 自動分割音檔
  const chunks = await splitAudio(audioData, maxDuration);
  return await processChunks(chunks);
}

UNSUPPORTED_FORMAT

不支援的音訊格式
if (error.code === 'UNSUPPORTED_FORMAT') {
  const supportedFormats = error.details.supported_formats;
  console.error(`不支援的格式,支援格式: ${supportedFormats.join(', ')}`);
  
  // 自動轉換格式
  const convertedAudio = await convertAudioFormat(audioData, 'wav');
  return await client.transcribe(convertedAudio);
}

POOR_AUDIO_QUALITY

音訊品質過低
if (error.code === 'POOR_AUDIO_QUALITY') {
  const qualityScore = error.details.quality_score;
  console.warn(`音訊品質較低 (${qualityScore}/100)`);
  
  // 嘗試音訊增強
  const enhancedAudio = await enhanceAudio(audioData);
  return await client.transcribe(enhancedAudio);
}

NO_SPEECH_DETECTED

未偵測到語音內容
if (error.code === 'NO_SPEECH_DETECTED') {
  console.warn('未偵測到語音內容');
  
  // 檢查是否為靜音檔案
  const hasAudio = await detectAudioActivity(audioData);
  if (!hasAudio) {
    throw new Error('音檔為靜音,請檢查錄音設備');
  }
}

4. 網路與連線錯誤

CONNECTION_TIMEOUT

連線逾時
if (error.code === 'CONNECTION_TIMEOUT') {
  console.log('連線逾時,正在重試...');
  
  // 實作重試機制
  return await retryWithBackoff(originalRequest, 3);
}

NETWORK_ERROR

網路錯誤
if (error.code === 'NETWORK_ERROR') {
  console.error('網路連線問題');
  
  // 檢查網路狀態
  const isOnline = await checkNetworkStatus();
  if (!isOnline) {
    await waitForNetworkReconnection();
  }
}

SERVICE_UNAVAILABLE

服務暫時無法使用
if (error.code === 'SERVICE_UNAVAILABLE') {
  const estimatedRecovery = error.details.estimated_recovery_time;
  console.error(`服務暫時無法使用,預計 ${estimatedRecovery} 恢復`);
  
  // 實作服務降級或使用備用服務
  return await fallbackService(audioData);
}

錯誤處理策略

1. 重試機制

指數退避重試

class RetryHandler {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }
  
  async executeWithRetry(operation, retryableErrors = []) {
    let lastError;
    
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        
        // 檢查是否為可重試的錯誤
        if (!this.isRetryableError(error, retryableErrors)) {
          throw error;
        }
        
        if (attempt === this.maxRetries) {
          break;
        }
        
        // 計算延遲時間(指數退避)
        const delay = this.baseDelay * Math.pow(2, attempt - 1);
        const jitter = Math.random() * 0.1 * delay; // 加入隨機性
        
        console.log(`嘗試 ${attempt} 失敗,${delay + jitter}ms 後重試...`);
        await this.sleep(delay + jitter);
      }
    }
    
    throw lastError;
  }
  
  isRetryableError(error, retryableErrors) {
    const defaultRetryableErrors = [
      'CONNECTION_TIMEOUT',
      'NETWORK_ERROR',
      'SERVICE_UNAVAILABLE',
      'RATE_LIMIT_EXCEEDED'
    ];
    
    const allRetryableErrors = [...defaultRetryableErrors, ...retryableErrors];
    return allRetryableErrors.includes(error.code);
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// 使用範例
const retryHandler = new RetryHandler(3, 1000);

const result = await retryHandler.executeWithRetry(async () => {
  return await client.transcribe(audioData);
}, ['POOR_AUDIO_QUALITY']); // 額外的可重試錯誤

智能重試策略

class SmartRetryHandler extends RetryHandler {
  async executeWithRetry(operation, context = {}) {
    let lastError;
    
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        
        // 根據錯誤類型調整策略
        const strategy = this.getRetryStrategy(error, attempt, context);
        
        if (!strategy.shouldRetry) {
          throw error;
        }
        
        if (strategy.preRetryAction) {
          await strategy.preRetryAction();
        }
        
        if (attempt < this.maxRetries) {
          await this.sleep(strategy.delay);
        }
      }
    }
    
    throw lastError;
  }
  
  getRetryStrategy(error, attempt, context) {
    switch (error.code) {
      case 'RATE_LIMIT_EXCEEDED':
        return {
          shouldRetry: true,
          delay: error.details.retry_after_seconds * 1000,
          preRetryAction: null
        };
        
      case 'POOR_AUDIO_QUALITY':
        return {
          shouldRetry: attempt <= 2,
          delay: 1000,
          preRetryAction: async () => {
            context.audioData = await enhanceAudio(context.audioData);
          }
        };
        
      case 'CONNECTION_TIMEOUT':
        return {
          shouldRetry: true,
          delay: Math.min(this.baseDelay * Math.pow(2, attempt), 30000),
          preRetryAction: async () => {
            await this.checkNetworkHealth();
          }
        };
        
      default:
        return {
          shouldRetry: this.isRetryableError(error, []),
          delay: this.baseDelay * attempt,
          preRetryAction: null
        };
    }
  }
}

2. 斷路器模式

class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000, monitoringPeriod = 10000) {
    this.threshold = threshold;
    this.timeout = timeout;
    this.monitoringPeriod = monitoringPeriod;
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
  }
  
  async execute(operation) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}

// 使用範例
const circuitBreaker = new CircuitBreaker(3, 30000);

try {
  const result = await circuitBreaker.execute(async () => {
    return await client.transcribe(audioData);
  });
} catch (error) {
  if (error.message === 'Circuit breaker is OPEN') {
    // 使用備用服務或快取結果
    return await fallbackTranscription(audioData);
  }
  throw error;
}

3. 優雅降級

class GracefulDegradation {
  constructor() {
    this.fallbackStrategies = new Map();
    this.setupFallbackStrategies();
  }
  
  setupFallbackStrategies() {
    // 服務不可用時的降級策略
    this.fallbackStrategies.set('SERVICE_UNAVAILABLE', async (audioData) => {
      // 使用本地模型或快取結果
      return await this.useLocalModel(audioData);
    });
    
    // 配額用完時的降級策略
    this.fallbackStrategies.set('QUOTA_EXCEEDED', async (audioData) => {
      // 使用免費的備用服務
      return await this.useFreeAlternative(audioData);
    });
    
    // 音訊品質過低時的降級策略
    this.fallbackStrategies.set('POOR_AUDIO_QUALITY', async (audioData) => {
      // 返回部分結果或提示
      return {
        transcript: '[音訊品質過低,無法完整辨識]',
        confidence: 0.1,
        warning: '建議重新錄製音訊'
      };
    });
  }
  
  async handleWithFallback(operation, audioData) {
    try {
      return await operation();
    } catch (error) {
      const fallbackStrategy = this.fallbackStrategies.get(error.code);
      
      if (fallbackStrategy) {
        console.warn(`使用降級策略處理錯誤: ${error.code}`);
        return await fallbackStrategy(audioData);
      }
      
      throw error;
    }
  }
}

實際應用範例

1. 健壯的語音辨識客戶端

class RobustVoiceClient {
  constructor(config) {
    this.client = new SkiesoftVoice(config);
    this.retryHandler = new SmartRetryHandler(3, 1000);
    this.circuitBreaker = new CircuitBreaker(5, 60000);
    this.gracefulDegradation = new GracefulDegradation();
  }
  
  async transcribe(audioData, options = {}) {
    const context = { audioData, options };
    
    return await this.gracefulDegradation.handleWithFallback(
      async () => {
        return await this.circuitBreaker.execute(async () => {
          return await this.retryHandler.executeWithRetry(async () => {
            return await this.client.transcribe(context.audioData, context.options);
          }, context);
        });
      },
      audioData
    );
  }
  
  async createRealTimeStream(options = {}) {
    try {
      const stream = await this.client.createRealTimeStream(options);
      
      // 為串流添加錯誤處理
      stream.on('error', (error) => {
        this.handleStreamError(error, stream);
      });
      
      return stream;
    } catch (error) {
      return await this.handleStreamCreationError(error, options);
    }
  }
  
  handleStreamError(error, stream) {
    switch (error.code) {
      case 'CONNECTION_LOST':
        console.log('連線中斷,嘗試重新連線...');
        this.reconnectStream(stream);
        break;
        
      case 'AUDIO_FORMAT_ERROR':
        console.error('音訊格式錯誤,請檢查設定');
        stream.emit('formatError', error);
        break;
        
      default:
        console.error('串流錯誤:', error);
        stream.emit('unhandledError', error);
    }
  }
  
  async reconnectStream(stream, maxAttempts = 5) {
    let attempts = 0;
    
    while (attempts < maxAttempts) {
      try {
        await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempts)));
        await stream.reconnect();
        console.log('重新連線成功');
        return;
      } catch (error) {
        attempts++;
        console.log(`重新連線失敗 (${attempts}/${maxAttempts})`);
      }
    }
    
    console.error('重新連線失敗,已達最大嘗試次數');
    stream.emit('reconnectFailed');
  }
}

2. 錯誤監控和報告

class ErrorMonitor {
  constructor() {
    this.errorStats = new Map();
    this.alertThresholds = {
      'QUOTA_EXCEEDED': 1, // 立即警報
      'SERVICE_UNAVAILABLE': 3, // 3次後警報
      'RATE_LIMIT_EXCEEDED': 10 // 10次後警報
    };
  }
  
  recordError(error, context = {}) {
    const errorKey = error.code;
    const currentCount = this.errorStats.get(errorKey) || 0;
    const newCount = currentCount + 1;
    
    this.errorStats.set(errorKey, newCount);
    
    // 記錄詳細錯誤資訊
    this.logError(error, context, newCount);
    
    // 檢查是否需要發送警報
    this.checkAlertThreshold(errorKey, newCount);
    
    // 定期重置統計
    this.scheduleStatsReset();
  }
  
  logError(error, context, count) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      errorCode: error.code,
      message: error.message,
      count: count,
      context: context,
      requestId: error.details?.request_id
    };
    
    // 發送到日誌系統
    console.error('API Error:', JSON.stringify(logEntry, null, 2));
    
    // 可以整合到外部監控系統
    // await this.sendToMonitoringSystem(logEntry);
  }
  
  checkAlertThreshold(errorCode, count) {
    const threshold = this.alertThresholds[errorCode];
    
    if (threshold && count >= threshold) {
      this.sendAlert(errorCode, count);
    }
  }
  
  sendAlert(errorCode, count) {
    const alertMessage = `警報: ${errorCode} 錯誤已發生 ${count} 次`;
    
    console.warn(alertMessage);
    
    // 發送到警報系統
    // await this.sendToAlertSystem(alertMessage);
  }
  
  getErrorStats() {
    return Object.fromEntries(this.errorStats);
  }
  
  scheduleStatsReset() {
    // 每小時重置統計
    if (!this.resetTimer) {
      this.resetTimer = setInterval(() => {
        this.errorStats.clear();
      }, 3600000);
    }
  }
}

// 整合到客戶端
class MonitoredVoiceClient extends RobustVoiceClient {
  constructor(config) {
    super(config);
    this.errorMonitor = new ErrorMonitor();
  }
  
  async transcribe(audioData, options = {}) {
    try {
      return await super.transcribe(audioData, options);
    } catch (error) {
      this.errorMonitor.recordError(error, { 
        audioSize: audioData.length,
        options: options 
      });
      throw error;
    }
  }
}

最佳實踐

1. 錯誤分類處理

class ErrorClassifier {
  static classify(error) {
    const userErrors = [
      'INVALID_API_KEY',
      'PROJECT_NOT_FOUND',
      'UNSUPPORTED_FORMAT',
      'AUDIO_TOO_SHORT',
      'AUDIO_TOO_LONG'
    ];
    
    const systemErrors = [
      'SERVICE_UNAVAILABLE',
      'NETWORK_ERROR',
      'CONNECTION_TIMEOUT'
    ];
    
    const retryableErrors = [
      'RATE_LIMIT_EXCEEDED',
      'CONNECTION_TIMEOUT',
      'SERVICE_UNAVAILABLE'
    ];
    
    return {
      isUserError: userErrors.includes(error.code),
      isSystemError: systemErrors.includes(error.code),
      isRetryable: retryableErrors.includes(error.code),
      severity: this.getSeverity(error.code)
    };
  }
  
  static getSeverity(errorCode) {
    const severityMap = {
      'INVALID_API_KEY': 'high',
      'QUOTA_EXCEEDED': 'high',
      'SERVICE_UNAVAILABLE': 'medium',
      'POOR_AUDIO_QUALITY': 'low'
    };
    
    return severityMap[errorCode] || 'medium';
  }
}

2. 使用者友善的錯誤訊息

class UserFriendlyErrorHandler {
  static getDisplayMessage(error) {
    const messages = {
      'INVALID_API_KEY': '身份驗證失敗,請檢查您的 API 設定',
      'QUOTA_EXCEEDED': '使用額度已用完,請升級方案或等待額度重置',
      'AUDIO_TOO_SHORT': '錄音時間太短,請錄製至少 1 秒的音訊',
      'AUDIO_TOO_LONG': '錄音時間太長,請分段處理或聯絡客服',
      'UNSUPPORTED_FORMAT': '不支援此音訊格式,請使用 WAV、MP3 或 FLAC 格式',
      'POOR_AUDIO_QUALITY': '音訊品質較低,建議在安靜環境重新錄製',
      'NO_SPEECH_DETECTED': '未偵測到語音內容,請確認音訊包含語音',
      'NETWORK_ERROR': '網路連線問題,請檢查網路狀態後重試',
      'SERVICE_UNAVAILABLE': '服務暫時無法使用,請稍後再試'
    };
    
    return messages[error.code] || '發生未知錯誤,請聯絡技術支援';
  }
  
  static getActionSuggestion(error) {
    const suggestions = {
      'INVALID_API_KEY': '前往設定頁面重新輸入 API 金鑰',
      'QUOTA_EXCEEDED': '查看使用量統計或升級方案',
      'AUDIO_TOO_SHORT': '重新錄製較長的音訊',
      'POOR_AUDIO_QUALITY': '在安靜環境使用高品質麥克風重新錄製',
      'NETWORK_ERROR': '檢查網路連線後重試'
    };
    
    return suggestions[error.code] || '聯絡技術支援獲得協助';
  }
}

3. 完整的錯誤處理範例

async function handleVoiceRecognition(audioData) {
  const client = new MonitoredVoiceClient({
    apiKey: process.env.SKIESOFT_API_KEY,
    projectId: process.env.SKIESOFT_PROJECT_ID
  });
  
  try {
    const result = await client.transcribe(audioData);
    return {
      success: true,
      data: result
    };
  } catch (error) {
    const classification = ErrorClassifier.classify(error);
    const displayMessage = UserFriendlyErrorHandler.getDisplayMessage(error);
    const suggestion = UserFriendlyErrorHandler.getActionSuggestion(error);
    
    return {
      success: false,
      error: {
        code: error.code,
        message: displayMessage,
        suggestion: suggestion,
        severity: classification.severity,
        retryable: classification.isRetryable,
        technical: error.message // 供開發者參考
      }
    };
  }
}

監控和警報

設定監控指標

  • 錯誤率:監控各種錯誤的發生頻率
  • 回應時間:追蹤 API 回應時間
  • 可用性:監控服務可用性
  • 配額使用:追蹤 API 配額使用情況

警報設定

  • 高錯誤率:錯誤率超過 5% 時發送警報
  • 服務不可用:連續失敗超過 3 次時警報
  • 配額警告:使用量達到 80% 時提醒
  • 異常流量:請求量異常增加時警報

需要錯誤處理相關技術支援?請聯絡我們:support@skiesoft.com