錯誤處理概述
正確的錯誤處理是建構穩健應用程式的關鍵。「聽有 AI」API 提供詳細的錯誤資訊,幫助您快速診斷和解決問題。本指南將介紹常見的錯誤類型、處理方式和最佳實踐。錯誤回應格式
所有 API 錯誤都遵循統一的格式:Copy
{
"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 金鑰無效或格式錯誤Copy
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 不存在或無權限存取Copy
if (error.code === 'PROJECT_NOT_FOUND') {
console.error('專案不存在,請檢查專案 ID');
// 提示使用者選擇正確的專案
await selectProject();
}
TOKEN_EXPIRED
存取權杖已過期Copy
if (error.code === 'TOKEN_EXPIRED') {
console.log('權杖已過期,正在重新整理...');
await refreshToken();
// 重試原始請求
return await retryRequest();
}
2. 配額與限制錯誤
QUOTA_EXCEEDED
使用配額已用完Copy
if (error.code === 'QUOTA_EXCEEDED') {
const resetDate = error.details.quota_reset_date;
console.error(`配額已用完,將於 ${resetDate} 重置`);
// 提示使用者升級方案或等待重置
await handleQuotaExceeded(resetDate);
}
RATE_LIMIT_EXCEEDED
請求頻率超過限制Copy
if (error.code === 'RATE_LIMIT_EXCEEDED') {
const retryAfter = error.details.retry_after_seconds;
console.log(`請求頻率過高,${retryAfter} 秒後重試`);
// 實作指數退避重試
await exponentialBackoff(retryAfter);
}
CONCURRENT_LIMIT_EXCEEDED
並發連線數超過限制Copy
if (error.code === 'CONCURRENT_LIMIT_EXCEEDED') {
const maxConnections = error.details.max_concurrent_connections;
console.error(`並發連線數超過限制 (${maxConnections})`);
// 等待其他連線結束或實作連線池
await waitForAvailableConnection();
}
3. 音訊相關錯誤
AUDIO_TOO_SHORT
音訊檔案太短Copy
if (error.code === 'AUDIO_TOO_SHORT') {
const minDuration = error.details.min_duration_seconds;
console.error(`音訊太短,最少需要 ${minDuration} 秒`);
// 提示使用者重新錄製或合併音檔
await handleShortAudio(minDuration);
}
AUDIO_TOO_LONG
音訊檔案太長Copy
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
不支援的音訊格式Copy
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
音訊品質過低Copy
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
未偵測到語音內容Copy
if (error.code === 'NO_SPEECH_DETECTED') {
console.warn('未偵測到語音內容');
// 檢查是否為靜音檔案
const hasAudio = await detectAudioActivity(audioData);
if (!hasAudio) {
throw new Error('音檔為靜音,請檢查錄音設備');
}
}
4. 網路與連線錯誤
CONNECTION_TIMEOUT
連線逾時Copy
if (error.code === 'CONNECTION_TIMEOUT') {
console.log('連線逾時,正在重試...');
// 實作重試機制
return await retryWithBackoff(originalRequest, 3);
}
NETWORK_ERROR
網路錯誤Copy
if (error.code === 'NETWORK_ERROR') {
console.error('網路連線問題');
// 檢查網路狀態
const isOnline = await checkNetworkStatus();
if (!isOnline) {
await waitForNetworkReconnection();
}
}
SERVICE_UNAVAILABLE
服務暫時無法使用Copy
if (error.code === 'SERVICE_UNAVAILABLE') {
const estimatedRecovery = error.details.estimated_recovery_time;
console.error(`服務暫時無法使用,預計 ${estimatedRecovery} 恢復`);
// 實作服務降級或使用備用服務
return await fallbackService(audioData);
}
錯誤處理策略
1. 重試機制
指數退避重試
Copy
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']); // 額外的可重試錯誤
智能重試策略
Copy
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. 斷路器模式
Copy
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. 優雅降級
Copy
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. 健壯的語音辨識客戶端
Copy
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. 錯誤監控和報告
Copy
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. 錯誤分類處理
Copy
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. 使用者友善的錯誤訊息
Copy
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. 完整的錯誤處理範例
Copy
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
