/** * ServiceWorker Mock Worker * This file will be compiled to dist/sw-mock-worker.js */ // Import types for ServiceWorker context /// // 多响应配置相关类型定义 interface MultiResponseConfig { default: MockResponseData; responses: ConditionalResponse[]; global?: { delay?: number | [number, number]; errorRate?: number; headers?: Record; }; } interface ConditionalResponse { name?: string; conditions: MatchCondition[]; response: MockResponseData; weight?: number; } interface MatchCondition { type: 'query' | 'body' | 'header' | 'method' | 'path'; rule: MatchRule; target: string; value: any; required?: boolean; } interface MatchRule { mode: 'exact' | 'contains' | 'startsWith' | 'endsWith' | 'regex' | 'range' | 'exists'; ignoreCase?: boolean; regexFlags?: string; } interface MockResponseData { status?: number; data: any; headers?: Record; delay?: number | [number, number]; errorRate?: number; errorStatus?: number | number[]; description?: string; } interface RequestContext { method: string; url: string; pathname: string; query: Record; headers: Record; body?: any; timestamp: number; } interface MatchResult { matched: boolean; response?: MockResponseData; matchedConditions?: string[]; weight?: number; responseName?: string; } interface SwMockConfig { enabled: boolean; debug: boolean; defaultDelay: number; interceptPatterns: string[]; excludePatterns: string[]; endpoints: Record; globalDelay: number | null; errorRate: number; mockDataPath: string; networkFailure: { enabled: boolean; scenarios: { httpErrors: { enabled: boolean; statusCodes: number[]; rate: number; distribution: 'random' | 'sequential' | 'weighted'; }; timeout: { enabled: boolean; duration: number; rate: number; }; connectionFailure: { enabled: boolean; rate: number; }; responseDelay: { enabled: boolean; minDelay: number; maxDelay: number; rate: number; }; intermittentFailure: { enabled: boolean; failureDuration: number; recoveryDuration: number; rate: number; }; }; }; } // Configuration constants const CONFIG_STORE_NAME = 'sw-mock-config'; const CONFIG_VERSION = 1; const DEFAULT_CONFIG: SwMockConfig = { enabled: false, debug: false, defaultDelay: 500, interceptPatterns: [ '/api/', 'microgrids-service', 'digital-portal', 'old-microgrids-service' ], excludePatterns: [ '/sw-mock-worker.js', '/mock/', '.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot' ], endpoints: {}, globalDelay: null, errorRate: 0, mockDataPath: '/mock', includeInterceptPrefix: true, networkFailure: { enabled: false, scenarios: { httpErrors: { enabled: false, statusCodes: [400, 401, 403, 404, 500, 502, 503, 504], rate: 0.1, distribution: 'random' }, timeout: { enabled: false, duration: 30000, rate: 0.05 }, connectionFailure: { enabled: false, rate: 0.02 }, responseDelay: { enabled: false, minDelay: 1000, maxDelay: 5000, rate: 0.3 }, intermittentFailure: { enabled: false, failureDuration: 10000, recoveryDuration: 30000, rate: 0.1 } } } }; // Global variables let config: SwMockConfig = { ...DEFAULT_CONFIG }; let dbInstance: IDBDatabase | null = null; let capturedRequests: any[] = []; let isCapturing = false; // Network failure state let networkFailureState = { httpErrorIndex: 0, intermittentFailureStartTime: null as number | null, isInFailureState: false, lastFailureCheck: 0 }; /** * Ensure configuration structure integrity */ function ensureConfigStructure(configData: Partial): SwMockConfig { const mergedConfig = { ...DEFAULT_CONFIG, ...configData }; // Ensure networkFailure structure is complete if (!mergedConfig.networkFailure) { mergedConfig.networkFailure = { ...DEFAULT_CONFIG.networkFailure }; } else { mergedConfig.networkFailure = { ...DEFAULT_CONFIG.networkFailure, ...mergedConfig.networkFailure }; // Ensure scenarios structure is complete if (!mergedConfig.networkFailure.scenarios) { mergedConfig.networkFailure.scenarios = { ...DEFAULT_CONFIG.networkFailure.scenarios }; } else { mergedConfig.networkFailure.scenarios = { ...DEFAULT_CONFIG.networkFailure.scenarios, ...mergedConfig.networkFailure.scenarios }; } } return mergedConfig; } /** * IndexedDB operations */ class ConfigDB { private dbName = 'sw-mock-db'; private version = CONFIG_VERSION; async init(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.version); request.onerror = () => { this.log('IndexedDB open failed', request.error); reject(request.error); }; request.onsuccess = () => { dbInstance = request.result; this.log('IndexedDB connected successfully'); resolve(dbInstance); }; request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; this.log(`IndexedDB upgrade: from version ${event.oldVersion} to ${event.newVersion}`); // Delete old object store if exists if (db.objectStoreNames.contains(CONFIG_STORE_NAME)) { db.deleteObjectStore(CONFIG_STORE_NAME); this.log('Deleted old object store'); } // Create new object store db.createObjectStore(CONFIG_STORE_NAME); this.log('Created new object store'); // Ensure transaction completion (event.target as IDBOpenDBRequest).transaction!.oncomplete = () => { this.log('IndexedDB upgrade completed'); }; }; }); } async getConfig(): Promise { try { if (!dbInstance) { await this.init(); } return new Promise((resolve, reject) => { try { const transaction = dbInstance!.transaction([CONFIG_STORE_NAME], 'readonly'); const store = transaction.objectStore(CONFIG_STORE_NAME); const request = store.get('config'); request.onerror = () => { this.log('Read config failed', request.error); reject(request.error); }; request.onsuccess = () => { const rawResult = request.result || { ...DEFAULT_CONFIG }; const result = ensureConfigStructure(rawResult); this.log('Config read successfully', result); resolve(result); }; } catch (error) { this.log('Create transaction failed', error); // If transaction creation fails, return default config resolve(ensureConfigStructure({})); } }); } catch (error) { this.log('Get config failed, using default config', error); return ensureConfigStructure({}); } } async saveConfig(newConfig: SwMockConfig): Promise { try { if (!dbInstance) { await this.init(); } return new Promise((resolve, reject) => { try { const transaction = dbInstance!.transaction([CONFIG_STORE_NAME], 'readwrite'); const store = transaction.objectStore(CONFIG_STORE_NAME); const request = store.put(newConfig, 'config'); request.onerror = () => { this.log('Save config failed', request.error); reject(request.error); }; request.onsuccess = () => { this.log('Config saved successfully'); resolve(); }; } catch (error) { this.log('Create save transaction failed', error); reject(error); } }); } catch (error) { this.log('Save config failed', error); throw error; } } async clearConfig(): Promise { try { if (!dbInstance) { await this.init(); } return new Promise((resolve, reject) => { try { const transaction = dbInstance!.transaction([CONFIG_STORE_NAME], 'readwrite'); const store = transaction.objectStore(CONFIG_STORE_NAME); const request = store.clear(); request.onerror = () => { this.log('Clear config failed', request.error); reject(request.error); }; request.onsuccess = () => { this.log('Config cleared successfully'); resolve(); }; } catch (error) { this.log('Create clear transaction failed', error); reject(error); } }); } catch (error) { this.log('Clear config failed', error); throw error; } } private log(message: string, data?: any): void { if (config.debug) { console.log(`[SW Mock DB] ${message}`, data || ''); } } } const configDB = new ConfigDB(); /** * Logging utilities */ function log(message: string, data?: any): void { if (config.debug) { console.log(`[SW Mock] ${message}`, data || ''); } } function logError(message: string, error?: any): void { console.error(`[SW Mock Error] ${message}`, error || ''); } /** * URL matching functions */ function shouldIntercept(url: string): boolean { // Check exclude patterns first for (const pattern of config.excludePatterns) { if (url.includes(pattern)) { return false; } } // Check intercept patterns for (const pattern of config.interceptPatterns) { if (url.includes(pattern)) { return true; } } return false; } /** * Generate mock file path from URL * 支持两种模式: * 1. 嵌套目录结构:/api/users/list -> /mock/api/users/list.json 或 /mock/users/list.json * 2. 扁平化结构:/api/users/list -> /mock/api_users_list.json 或 /mock/users_list.json (兼容旧版本) */ function generateMockFilePath(url: string, useNestedStructure: boolean = true): string | null { try { const urlObj = new URL(url); let path = urlObj.pathname; // Remove leading slash if (path.startsWith('/')) { path = path.substring(1); } // Remove service prefixes const servicePrefixes = ['microgrids-service', 'digital-portal', 'old-microgrids-service']; for (const prefix of servicePrefixes) { if (path.startsWith(prefix + '/')) { path = path.substring(prefix.length + 1); } } // 根据 includeInterceptPrefix 配置决定是否移除拦截前缀 if (!config.includeInterceptPrefix && config.interceptPatterns) { for (const pattern of config.interceptPatterns) { // 移除前导和尾随的斜杠,获取纯前缀 const prefix = pattern.replace(/^\/+|\/+$/g, ''); if (prefix && path.startsWith(prefix + '/')) { path = path.substring(prefix.length + 1); log(`Removed intercept prefix '${prefix}' from path: ${urlObj.pathname} -> ${path}`); break; // 只移除第一个匹配的前缀 } } } // 获取配置的mock数据路径,确保以/开头但不以/结尾 let mockDataPath = config.mockDataPath || '/mock'; if (!mockDataPath.startsWith('/')) { mockDataPath = '/' + mockDataPath; } if (mockDataPath.endsWith('/')) { mockDataPath = mockDataPath.slice(0, -1); } if (useNestedStructure) { // 嵌套目录结构:保持URL路径结构 return `${mockDataPath}/${path}.json`; } else { // 扁平化结构:将斜杠替换为下划线(兼容旧版本) const flatPath = path.replace(/\//g, '_'); return `${mockDataPath}/${flatPath}.json`; } } catch (error) { logError('Generate mock file path failed', error); return null; } } /** * 从请求中提取上下文信息 */ async function extractRequestContext(request: Request): Promise { const url = new URL(request.url); const query: Record = {}; // 提取 query 参数 for (const [key, value] of url.searchParams.entries()) { if (query[key]) { // 如果已存在,转换为数组 if (Array.isArray(query[key])) { (query[key] as string[]).push(value); } else { query[key] = [query[key] as string, value]; } } else { query[key] = value; } } // 提取请求头 const headers: Record = {}; for (const [key, value] of request.headers.entries()) { headers[key.toLowerCase()] = value; } // 提取请求体 let body: any = null; if (request.method !== 'GET' && request.method !== 'HEAD') { try { const contentType = headers['content-type'] || ''; if (contentType.includes('application/json')) { const clonedRequest = request.clone(); const text = await clonedRequest.text(); if (text) { body = JSON.parse(text); } } else if (contentType.includes('application/x-www-form-urlencoded')) { const clonedRequest = request.clone(); const formData = await clonedRequest.formData(); body = {}; for (const [key, value] of formData.entries()) { body[key] = value; } } else if (contentType.includes('multipart/form-data')) { const clonedRequest = request.clone(); const formData = await clonedRequest.formData(); body = {}; for (const [key, value] of formData.entries()) { body[key] = value; } } else { const clonedRequest = request.clone(); body = await clonedRequest.text(); } } catch (error) { log('Failed to extract request body:', error); } } return { method: request.method, url: request.url, pathname: url.pathname, query, headers, body, timestamp: Date.now() }; } /** * 参数匹配引擎 */ function matchValue(actualValue: any, expectedValue: any, rule: MatchRule): boolean { // 处理 null 和 undefined if (actualValue == null && expectedValue == null) { return true; } if (actualValue == null || expectedValue == null) { return rule.mode === 'exists' ? actualValue != null : false; } // 转换为字符串进行匹配(除了 range 模式) let actual = String(actualValue); let expected = String(expectedValue); // 处理大小写忽略 if (rule.ignoreCase && typeof actualValue === 'string' && typeof expectedValue === 'string') { actual = actual.toLowerCase(); expected = expected.toLowerCase(); } switch (rule.mode) { case 'exact': return actual === expected; case 'contains': return actual.includes(expected); case 'startsWith': return actual.startsWith(expected); case 'endsWith': return actual.endsWith(expected); case 'regex': try { const flags = rule.regexFlags || (rule.ignoreCase ? 'i' : ''); const regex = new RegExp(expected, flags); return regex.test(actual); } catch (error) { log(`Invalid regex pattern: ${expected}`, error); return false; } case 'range': // 数值范围匹配 if (Array.isArray(expectedValue) && expectedValue.length === 2) { const numValue = Number(actualValue); const [min, max] = expectedValue.map(Number); return !isNaN(numValue) && numValue >= min && numValue <= max; } return false; case 'exists': return actualValue != null; default: return false; } } /** * 从请求上下文中获取目标值 */ function getTargetValue(context: RequestContext, condition: MatchCondition): any { switch (condition.type) { case 'query': return context.query[condition.target]; case 'header': return context.headers[condition.target.toLowerCase()]; case 'method': return context.method; case 'path': return condition.target === 'pathname' ? context.pathname : context.url; case 'body': if (!context.body) return null; // 支持嵌套属性访问,如 "user.name" const keys = condition.target.split('.'); let value = context.body; for (const key of keys) { if (value && typeof value === 'object' && key in value) { value = value[key]; } else { return null; } } return value; default: return null; } } /** * 检查单个条件是否匹配 */ function checkCondition(context: RequestContext, condition: MatchCondition): boolean { const actualValue = getTargetValue(context, condition); return matchValue(actualValue, condition.value, condition.rule); } /** * 检查条件响应是否匹配 */ function checkConditionalResponse(context: RequestContext, response: ConditionalResponse): MatchResult { const matchedConditions: string[] = []; let allRequired = true; for (const condition of response.conditions) { const isMatch = checkCondition(context, condition); const isRequired = condition.required !== false; // 默认为 true if (isMatch) { matchedConditions.push(`${condition.type}:${condition.target}`); } else if (isRequired) { allRequired = false; break; } } // 必须所有必需条件都匹配,且至少有一个条件匹配 const matched = allRequired && (matchedConditions.length > 0 || response.conditions.length === 0); return { matched, response: matched ? response.response : undefined, matchedConditions, weight: response.weight || 0, responseName: response.name }; } /** * 选择最佳匹配的响应 */ function selectBestResponse(context: RequestContext, multiConfig: MultiResponseConfig): MatchResult { const matchResults: MatchResult[] = []; // 检查所有条件响应 for (const response of multiConfig.responses) { const result = checkConditionalResponse(context, response); if (result.matched) { matchResults.push(result); } } if (matchResults.length === 0) { // 没有匹配的条件响应,使用默认响应 log('No conditional response matched, using default response'); return { matched: true, response: multiConfig.default, matchedConditions: [], weight: 0, responseName: 'default' }; } // 按权重排序,权重高的优先 matchResults.sort((a, b) => { const weightA = a.weight || 0; const weightB = b.weight || 0; if (weightA !== weightB) { return weightB - weightA; // 降序 } // 权重相同时,按匹配条件数量排序 return (b.matchedConditions?.length || 0) - (a.matchedConditions?.length || 0); }); const bestMatch = matchResults[0]; log(`Selected response: ${bestMatch.responseName}, weight: ${bestMatch.weight}, matched conditions: ${bestMatch.matchedConditions?.join(', ')}`); return bestMatch; } /** * 检查数据是否为多响应配置 */ function isMultiResponseConfig(data: any): data is MultiResponseConfig { return data && typeof data === 'object' && 'default' in data && 'responses' in data && Array.isArray(data.responses); } /** * Load mock data from file * 支持多种路径格式的回退机制 */ async function loadMockData(url: string): Promise { // 尝试多种路径格式 const pathsToTry = [ generateMockFilePath(url, true), // 嵌套目录结构 generateMockFilePath(url, false) // 扁平化结构(兼容旧版本) ].filter(Boolean) as string[]; for (const mockFilePath of pathsToTry) { try { const response = await fetch(mockFilePath); if (response.ok) { const data = await response.json(); log(`Mock data loaded successfully: ${mockFilePath}`, data); return data; } else { log(`Mock file not found: ${mockFilePath}`); } } catch (error) { log(`Load mock data failed: ${mockFilePath}`, error); } } log(`No mock data found for URL: ${url}`); return null; } /** * Network failure simulation engine */ function shouldTriggerNetworkFailure(url: string): any { // Check if network failure configuration exists and is enabled if (!config.networkFailure || !config.networkFailure.enabled) { return null; } const scenarios = config.networkFailure.scenarios; if (!scenarios) { return null; } // Check scenarios in priority order // 1. Intermittent failure check if (scenarios.intermittentFailure && scenarios.intermittentFailure.enabled) { const intermittentResult = checkIntermittentFailure(); if (intermittentResult) { return intermittentResult; } } // 2. Connection failure check if (scenarios.connectionFailure && scenarios.connectionFailure.enabled && Math.random() < scenarios.connectionFailure.rate) { log(`Trigger connection failure: ${url}`); return { type: 'connectionFailure' }; } // 3. Timeout check if (scenarios.timeout && scenarios.timeout.enabled && Math.random() < scenarios.timeout.rate) { log(`Trigger timeout: ${url}, duration: ${scenarios.timeout.duration}ms`); return { type: 'timeout', duration: scenarios.timeout.duration }; } // 4. HTTP error check if (scenarios.httpErrors && scenarios.httpErrors.enabled && Math.random() < scenarios.httpErrors.rate) { const statusCode = selectHttpErrorStatusCode(); log(`Trigger HTTP error: ${url}, status code: ${statusCode}`); return { type: 'httpError', statusCode }; } // 5. Response delay check if (scenarios.responseDelay && scenarios.responseDelay.enabled && Math.random() < scenarios.responseDelay.rate) { const delay = generateRandomDelay(); log(`Trigger response delay: ${url}, delay: ${delay}ms`); return { type: 'responseDelay', delay }; } return null; } function checkIntermittentFailure(): any { if (!config.networkFailure || !config.networkFailure.scenarios || !config.networkFailure.scenarios.intermittentFailure) { return null; } const scenario = config.networkFailure.scenarios.intermittentFailure; const now = Date.now(); // If not in failure state, check if should enter failure state if (!networkFailureState.isInFailureState) { if (Math.random() < scenario.rate) { networkFailureState.isInFailureState = true; networkFailureState.intermittentFailureStartTime = now; log('Enter intermittent failure state'); return { type: 'connectionFailure' }; } } else { // Check if should exit failure state const failureElapsed = now - (networkFailureState.intermittentFailureStartTime || 0); if (failureElapsed >= scenario.failureDuration) { networkFailureState.isInFailureState = false; networkFailureState.intermittentFailureStartTime = now; // Start recovery period log('Exit intermittent failure state, enter recovery period'); } else { // Still in failure period return { type: 'connectionFailure' }; } } // Check if in recovery period if (networkFailureState.intermittentFailureStartTime) { const recoveryElapsed = now - networkFailureState.intermittentFailureStartTime; if (recoveryElapsed >= scenario.recoveryDuration) { networkFailureState.intermittentFailureStartTime = null; log('Intermittent failure recovery period ended'); } } return null; } function selectHttpErrorStatusCode(): number { if (!config.networkFailure || !config.networkFailure.scenarios || !config.networkFailure.scenarios.httpErrors) { return 500; // Default return 500 error } const scenario = config.networkFailure.scenarios.httpErrors; const statusCodes = scenario.statusCodes || [500]; switch (scenario.distribution) { case 'sequential': const statusCode = statusCodes[networkFailureState.httpErrorIndex % statusCodes.length]; networkFailureState.httpErrorIndex++; return statusCode; case 'weighted': // Server errors have higher weight const weightedCodes = [ ...statusCodes.filter(code => code >= 500), // Server error weight 1 ...statusCodes.filter(code => code >= 500), // Server error weight 2 ...statusCodes.filter(code => code < 500) // Client error weight 1 ]; return weightedCodes[Math.floor(Math.random() * weightedCodes.length)]; case 'random': default: return statusCodes[Math.floor(Math.random() * statusCodes.length)]; } } function generateRandomDelay(): number { if (!config.networkFailure || !config.networkFailure.scenarios || !config.networkFailure.scenarios.responseDelay) { return 1000; // Default return 1 second delay } const scenario = config.networkFailure.scenarios.responseDelay; const minDelay = scenario.minDelay || 1000; const maxDelay = scenario.maxDelay || 5000; return Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay; } /** * Create network failure response */ async function createNetworkFailureResponse(failureInfo: any, url: string): Promise { switch (failureInfo.type) { case 'connectionFailure': // Throw connection failure exception throw new TypeError('Failed to fetch'); case 'timeout': // Wait for timeout duration then throw exception await new Promise(resolve => setTimeout(resolve, failureInfo.duration)); throw new TypeError('Network timeout'); case 'httpError': // Return HTTP error response return new Response(JSON.stringify({ error: 'Network Error', message: `HTTP ${failureInfo.statusCode} Error`, url: url }), { status: failureInfo.statusCode, statusText: getStatusText(failureInfo.statusCode), headers: { 'Content-Type': 'application/json', 'X-Mock-Network-Failure': 'true' } }); case 'responseDelay': // Add delay but continue normal processing await new Promise(resolve => setTimeout(resolve, failureInfo.delay)); return null; // Return null means continue normal processing default: return null; } } /** * Get HTTP status text for status code */ function getStatusText(statusCode: number): string { const statusTexts: Record = { 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout' }; return statusTexts[statusCode] || 'Error'; } /** * 处理多响应配置的模拟行为 */ async function applyMultiResponseBehavior(context: RequestContext, mockData: any): Promise { if (isMultiResponseConfig(mockData)) { // 多响应配置 const matchResult = selectBestResponse(context, mockData); if (!matchResult.matched || !matchResult.response) { throw new Error('No matching response found'); } const selectedResponse = matchResult.response; const globalConfig = mockData.global || {}; // 应用延迟 const delay = selectedResponse.delay || globalConfig.delay || config.globalDelay || config.defaultDelay; if (delay) { const actualDelay = Array.isArray(delay) ? Math.floor(Math.random() * (delay[1] - delay[0] + 1)) + delay[0] : delay; if (actualDelay > 0) { await new Promise(resolve => setTimeout(resolve, actualDelay)); } } // 应用错误率 const errorRate = selectedResponse.errorRate || globalConfig.errorRate || config.errorRate; if (errorRate > 0 && Math.random() < errorRate) { const errorStatus = selectedResponse.errorStatus || 500; const status = Array.isArray(errorStatus) ? errorStatus[Math.floor(Math.random() * errorStatus.length)] : errorStatus; throw new Error(`Simulated error - status code: ${status}`); } // 合并响应头 const headers = { ...globalConfig.headers, ...selectedResponse.headers }; return { ...selectedResponse.data, _mockInfo: { responseName: matchResult.responseName, matchedConditions: matchResult.matchedConditions, weight: matchResult.weight, status: selectedResponse.status || 200, headers } }; } else { // 传统单响应配置 return applyMockBehavior(mockData); } } /** * Apply mock behavior (delay, error simulation) - 传统方式 */ async function applyMockBehavior(mockData: any): Promise { const mockConfig = mockData._mock || {}; // Apply delay const delay = mockConfig.delay || config.globalDelay || config.defaultDelay; if (delay > 0) { await new Promise(resolve => setTimeout(resolve, delay)); } // Apply error rate const errorRate = mockConfig.errorRate || config.errorRate; if (errorRate > 0 && Math.random() < errorRate) { const errorStatus = mockConfig.errorStatus || 500; throw new Error(`Simulated error - status code: ${errorStatus}`); } // Remove _mock field and return actual data const { _mock, ...responseData } = mockData; return responseData; } /** * Create mock response */ function createMockResponse(data: any, status = 200, customHeaders: Record = {}): Response { const headers = { 'Content-Type': 'application/json', 'X-Mock-Response': 'true', ...customHeaders }; return new Response(JSON.stringify(data), { status, statusText: 'OK', headers }); } /** * Handle request interception */ async function handleRequest(event: any): Promise { const { request } = event; const url = request.url; log(`Intercept request: ${request.method} ${url}`); // Record request (if capturing) if (isCapturing) { capturedRequests.push({ method: request.method, url: url, timestamp: new Date().toISOString(), headers: Object.fromEntries(request.headers.entries()) }); } // Check if should intercept if (!config.enabled || !shouldIntercept(url)) { log(`Skip interception: ${url}`); return fetch(request); } // Check network failure simulation const failureInfo = shouldTriggerNetworkFailure(url); if (failureInfo) { try { const failureResponse = await createNetworkFailureResponse(failureInfo, url); if (failureResponse) { return failureResponse; } // If returns null, it's response delay, continue normal processing } catch (error) { // Network exceptions (connection failure, timeout, etc.) throw error; } } // Load mock data (尝试多种路径格式) const mockData = await loadMockData(url); if (!mockData) { log(`No mock data found, forward to real server: ${url}`); return fetch(request); } try { // 提取请求上下文 const context = await extractRequestContext(request); // Apply mock behavior (支持多响应配置) const responseData = await applyMultiResponseBehavior(context, mockData); log(`Return mock response: ${url}`, responseData); // 检查是否有自定义状态码和响应头 const mockInfo = responseData._mockInfo; const status = mockInfo?.status || 200; const customHeaders = mockInfo?.headers || {}; // 移除内部信息 const { _mockInfo, ...cleanResponseData } = responseData; return createMockResponse(cleanResponseData, status, customHeaders); } catch (error) { logError(`Mock processing failed: ${url}`, error); // If it's simulated error, return error response if (error instanceof Error && error.message.includes('Simulated error')) { const status = parseInt(error.message.match(/\d+/)?.[0] || '500'); return createMockResponse({ error: 'Simulated server error' }, status); } // Other errors, forward to real server return fetch(request); } } /** * Message handling */ (self as any).addEventListener('message', async (event: any) => { const { type, payload } = event.data; log(`[SW Mock] Received message: ${type}`, payload); try { // Ensure we have a valid port to respond to if (!event.ports || !event.ports[0]) { logError('No message port available for response'); return; } switch (type) { case 'GET_CONFIG': log(`[SW Mock] Sending config response`, config); event.ports[0].postMessage({ success: true, data: config }); break; case 'UPDATE_CONFIG': config = ensureConfigStructure({ ...config, ...payload }); await configDB.saveConfig(config); event.ports[0].postMessage({ success: true }); log('Configuration updated', config); break; case 'RESET_CONFIG': config = ensureConfigStructure({}); await configDB.saveConfig(config); event.ports[0].postMessage({ success: true }); log('Configuration reset'); break; case 'START_CAPTURE': isCapturing = true; capturedRequests = []; event.ports[0].postMessage({ success: true }); log('Request capture started'); break; case 'STOP_CAPTURE': isCapturing = false; event.ports[0].postMessage({ success: true, data: capturedRequests }); log('Request capture stopped', capturedRequests); break; case 'ENABLE_NETWORK_FAILURE': config.networkFailure.enabled = true; await configDB.saveConfig(config); event.ports[0].postMessage({ success: true }); log('Network failure simulation enabled'); break; case 'DISABLE_NETWORK_FAILURE': config.networkFailure.enabled = false; await configDB.saveConfig(config); event.ports[0].postMessage({ success: true }); log('Network failure simulation disabled'); break; case 'UPDATE_NETWORK_FAILURE_CONFIG': config = ensureConfigStructure({ ...config, networkFailure: { ...config.networkFailure, ...payload } }); await configDB.saveConfig(config); event.ports[0].postMessage({ success: true }); log('Network failure configuration updated', config.networkFailure); break; case 'RESET_NETWORK_FAILURE_STATE': networkFailureState = { httpErrorIndex: 0, intermittentFailureStartTime: null, isInFailureState: false, lastFailureCheck: 0 }; event.ports[0].postMessage({ success: true }); log('Network failure state reset'); break; case 'CLEAR_DATABASE': try { await configDB.clearConfig(); config = ensureConfigStructure({}); event.ports[0].postMessage({ success: true }); log('Database cleared, configuration reset'); } catch (clearError) { logError('Clear database failed', clearError); event.ports[0].postMessage({ success: false, error: (clearError as Error).message }); } break; default: logError(`Unknown message type: ${type}`); event.ports[0].postMessage({ success: false, error: `Unknown message type: ${type}` }); } } catch (error) { logError('Message processing failed', error); try { if (event.ports && event.ports[0]) { event.ports[0].postMessage({ success: false, error: (error as Error).message }); } } catch (responseError) { logError('Failed to send error response', responseError); } } }); /** * ServiceWorker event listeners */ (self as any).addEventListener('install', () => { log('ServiceWorker installing...'); (self as any).skipWaiting(); }); (self as any).addEventListener('activate', (event: any) => { log('ServiceWorker activating...'); event.waitUntil( (async () => { try { log('Loading configuration from database...'); const savedConfig = await configDB.getConfig(); config = ensureConfigStructure(savedConfig); log('Configuration loaded successfully', config); } catch (error) { logError('Load configuration failed, using default configuration', error); config = ensureConfigStructure({}); // Try to clear possibly corrupted database try { await configDB.clearConfig(); log('Cleared corrupted configuration data'); } catch (clearError) { logError('Clear configuration failed', clearError); } } log('ServiceWorker activation complete, ready to handle messages'); })() ); (self as any).clients.claim(); }); (self as any).addEventListener('fetch', (event: any) => { event.respondWith(handleRequest(event)); });