diff --git a/apps/web/public/sw-debug.html b/apps/web/public/sw-debug.html index 221c6240..d41f29fe 100644 --- a/apps/web/public/sw-debug.html +++ b/apps/web/public/sw-debug.html @@ -26,7 +26,7 @@ diff --git a/apps/web/public/sw-debug/analysis-mode.js b/apps/web/public/sw-debug/analysis-mode.js new file mode 100644 index 00000000..0c56f062 --- /dev/null +++ b/apps/web/public/sw-debug/analysis-mode.js @@ -0,0 +1,720 @@ +/** + * SW Debug Panel - Analysis Mode + * 分析模式功能模块 + */ + +import { state, elements, updateElements } from './state.js'; +import { escapeHtml, truncateUrl, parseUserAgent, formatBytes } from './common.js'; +import { showToast } from './toast.js'; +import { + enableDebug, + refreshStatus, + loadConsoleLogs, + loadPostMessageLogs, + loadCacheStats, +} from './sw-communication.js'; + +// Forward declarations - will be set by app.js +let renderLogs = null; +let renderConsoleLogs = null; +let renderPostmessageLogs = null; +let renderCrashLogs = null; +let renderLLMApiLogs = null; +let updateConsoleCount = null; +let updatePostmessageCount = null; +let updateCrashCount = null; +let updateErrorDots = null; +let updateMessageTypeOptions = null; +let loadCrashLogs = null; +let loadLLMApiLogs = null; +let updateMemoryDisplay = null; + +/** + * Set render callbacks from app.js + */ +export function setAnalysisModeCallbacks(callbacks) { + renderLogs = callbacks.renderLogs; + renderConsoleLogs = callbacks.renderConsoleLogs; + renderPostmessageLogs = callbacks.renderPostmessageLogs; + renderCrashLogs = callbacks.renderCrashLogs; + renderLLMApiLogs = callbacks.renderLLMApiLogs; + updateConsoleCount = callbacks.updateConsoleCount; + updatePostmessageCount = callbacks.updatePostmessageCount; + updateCrashCount = callbacks.updateCrashCount; + updateErrorDots = callbacks.updateErrorDots; + updateMessageTypeOptions = callbacks.updateMessageTypeOptions; + loadCrashLogs = callbacks.loadCrashLogs; + loadLLMApiLogs = callbacks.loadLLMApiLogs; + updateMemoryDisplay = callbacks.updateMemoryDisplay; +} + +/** + * Toggle analysis mode (for analyzing user-provided logs) + * In analysis mode: + * - SW connection is disabled + * - Only imported log data is displayed + * - Debug-related buttons are hidden + */ +export function toggleAnalysisMode() { + state.isAnalysisMode = !state.isAnalysisMode; + updateAnalysisModeUI(); + + if (state.isAnalysisMode) { + // Clear all existing data when entering analysis mode + clearAllLogsForAnalysisMode(); + } else { + // Restore normal mode - reconnect to SW + exitAnalysisMode(); + } +} + +/** + * Update UI based on analysis mode state + */ +export function updateAnalysisModeUI() { + const isAnalysis = state.isAnalysisMode; + + // Update mode indicator + if (elements.analysisModeIndicator) { + elements.analysisModeIndicator.style.display = isAnalysis ? 'inline-flex' : 'none'; + } + + // Update SW status indicator + if (elements.swStatus) { + elements.swStatus.style.display = isAnalysis ? 'none' : 'inline-flex'; + } + + // Update title + if (elements.panelTitle) { + elements.panelTitle.textContent = isAnalysis ? '日志分析模式' : 'Service Worker 调试面板'; + } + + // Show/hide import button + if (elements.importLogsBtn) { + elements.importLogsBtn.style.display = isAnalysis ? 'inline-flex' : 'none'; + } + + // Update toggle button appearance + if (elements.toggleAnalysisModeBtn) { + if (isAnalysis) { + elements.toggleAnalysisModeBtn.classList.add('active'); + elements.toggleAnalysisModeBtn.title = '退出分析模式,返回调试模式'; + } else { + elements.toggleAnalysisModeBtn.classList.remove('active'); + elements.toggleAnalysisModeBtn.title = '切换到分析模式(导入用户日志)'; + } + } + + // Hide/show debug-mode-only buttons + document.querySelectorAll('.debug-mode-only').forEach(el => { + el.style.display = isAnalysis ? 'none' : ''; + }); + + // In analysis mode, left panel shows user info instead of SW status + // The panel is always visible, but content changes based on mode + const leftPanel = document.querySelector('.left-panel'); + if (leftPanel) { + // Always show left panel, content will be different in analysis mode + leftPanel.style.display = ''; + } + + // Adjust panels grid - always use two-column layout + const panels = document.querySelector('.panels'); + if (panels) { + panels.style.gridTemplateColumns = '280px 1fr'; + } + + // Update left panel content for analysis mode + if (isAnalysis) { + showAnalysisModeLeftPanel(); + } else { + restoreDebugModeLeftPanel(); + } + + // Add/remove body class for analysis mode styling + document.body.classList.toggle('analysis-mode', isAnalysis); +} + +/** + * Clear display logs when entering analysis mode + * Current logs are preserved in liveLogs buffer for restoration later + */ +function clearAllLogsForAnalysisMode() { + // Save current logs to live buffer before clearing display + state.liveLogs.logs = [...state.logs]; + state.liveLogs.consoleLogs = [...state.consoleLogs]; + state.liveLogs.postmessageLogs = [...state.postmessageLogs]; + state.liveLogs.crashLogs = [...state.crashLogs]; + state.liveLogs.llmapiLogs = [...state.llmapiLogs]; + + // Clear display state for imported logs + state.logs = []; + state.consoleLogs = []; + state.postmessageLogs = []; + state.crashLogs = []; + state.llmapiLogs = []; + state.importedLogData = null; + + // Re-render all tabs + if (renderLogs) renderLogs(); + if (renderConsoleLogs) renderConsoleLogs(); + if (renderPostmessageLogs) renderPostmessageLogs(); + if (renderCrashLogs) renderCrashLogs(); + if (renderLLMApiLogs) renderLLMApiLogs(); + + // Show import prompt + showImportPrompt(); +} + +/** + * Show analysis mode left panel (placeholder for user info) + */ +function showAnalysisModeLeftPanel() { + const leftPanel = document.querySelector('.left-panel'); + if (!leftPanel) return; + + leftPanel.innerHTML = ` +
请导入用户日志文件
+导入后将显示用户信息
+${escapeHtml(userAgent)}
+在此模式下,您可以导入用户提供的日志文件进行分析
++ 支持从"导出日志"功能生成的 JSON 文件 +
+ ++ 点击右上角搜索图标可退出分析模式 +
+${filename}
++ 共 ${totalLogs} 条日志 + ${counts.fetch > 0 ? `| Fetch: ${counts.fetch}` : ''} + ${counts.console > 0 ? `| 控制台: ${counts.console}` : ''} + ${counts.postmessage > 0 ? `| PostMessage: ${counts.postmessage}` : ''} + ${counts.memory > 0 ? `| 内存: ${counts.memory}` : ''} + ${counts.llmapi > 0 ? `| LLM API: ${counts.llmapi}` : ''} +
+ ${data.exportTime ? `导出时间: ${new Date(data.exportTime).toLocaleString('zh-CN')}
` : ''} +${escapeHtml(message)}
-${filename}
-- 文件大小: ${size} MB - ${stats.boardCount > 0 ? `| 画板: ${stats.boardCount}` : ''} - ${stats.folderCount > 0 ? `| 文件夹: ${stats.folderCount}` : ''} - ${stats.assetCount > 0 ? `| 素材: ${stats.assetCount}` : ''} - ${stats.taskCount > 0 ? `| 任务: ${stats.taskCount}` : ''} - ${stats.imagePromptCount > 0 ? `| 图片提示词: ${stats.imagePromptCount}` : ''} - ${stats.videoPromptCount > 0 ? `| 视频提示词: ${stats.videoPromptCount}` : ''} -
-请导入用户日志文件
-导入后将显示用户信息
-${escapeHtml(userAgent)}
-在此模式下,您可以导入用户提供的日志文件进行分析
-- 支持从"导出日志"功能生成的 JSON 文件 -
- -- 点击右上角搜索图标可退出分析模式 -
-${filename}
-- 共 ${totalLogs} 条日志 - ${counts.fetch > 0 ? `| Fetch: ${counts.fetch}` : ''} - ${counts.console > 0 ? `| 控制台: ${counts.console}` : ''} - ${counts.postmessage > 0 ? `| PostMessage: ${counts.postmessage}` : ''} - ${counts.memory > 0 ? `| 内存: ${counts.memory}` : ''} - ${counts.llmapi > 0 ? `| LLM API: ${counts.llmapi}` : ''} -
- ${data.exportTime ? `导出时间: ${new Date(data.exportTime).toLocaleString('zh-CN')}
` : ''} -${state.debugEnabled ? '暂无匹配的日志' : '请先启用调试模式'}
- ${!state.debugEnabled ? '' : ''} -暂无 LLM API 调用记录
-图片/视频/对话等 AI 接口调用会自动记录
-| ID | -${log.id} | -
| Endpoint | -${log.endpoint} | -
| 模型 | -${log.model} | -
| 类型 | -${log.taskType} | -
| HTTP 状态 | -${log.httpStatus || '-'} | -
| 耗时 | -${durationText} | -
| 参考图 | -${log.referenceImageCount || 0} 张 | -
| 结果类型 | -${log.resultType} | -
| 任务 ID | -${log.taskId} | -
| 结果 URL | -- - ${log.resultUrl.length > 80 ? log.resultUrl.substring(0, 80) + '...' : log.resultUrl} - - - | -
| 错误信息 | -${escapeHtml(log.errorMessage)} | -
${escapeHtml(log.prompt)}
- ${escapeHtml(log.requestBody)}
- ${escapeHtml(log.resultText)}
- ${escapeHtml(log.responseBody)}
- 暂无内存日志
-页面启动、内存超限、错误和关闭时的快照会自动记录
-ID: ${log.id}
-时间: ${new Date(log.timestamp).toLocaleString('zh-CN')}
-URL: ${log.url || '-'}
- 已用: ${(log.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)} MB
-总计: ${(log.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)} MB
-限制: ${(log.memory.jsHeapSizeLimit / (1024 * 1024)).toFixed(1)} MB
-使用率: ${memoryPercent.toFixed(1)}%
- DOM节点: ${log.pageStats.domNodeCount || 0}
-Canvas: ${log.pageStats.canvasCount || 0}
-图片: ${log.pageStats.imageCount || 0}
-视频: ${log.pageStats.videoCount || 0}
-iframe: ${log.pageStats.iframeCount || 0}${log.pageStats.plaitElementCount !== undefined ? `\nPlait元素: ${log.pageStats.plaitElementCount}` : ''}
- ${log.performance.longTaskDuration ? `长任务时长: ${log.performance.longTaskDuration.toFixed(0)}ms` : ''}${log.performance.freezeDuration ? `卡死时长: ${(log.performance.freezeDuration / 1000).toFixed(1)}s` : ''}${log.performance.fps !== undefined ? `\nFPS: ${log.performance.fps}` : ''}
- ${log.error.type}: ${escapeHtml(log.error.message)}
- ${log.error.stack ? `${escapeHtml(log.error.stack)}` : ''}
- ${JSON.stringify(log.customData, null, 2)}
- ${filename}
++ 文件大小: ${size} MB + ${stats.boardCount > 0 ? `| 画板: ${stats.boardCount}` : ''} + ${stats.folderCount > 0 ? `| 文件夹: ${stats.folderCount}` : ''} + ${stats.assetCount > 0 ? `| 素材: ${stats.assetCount}` : ''} + ${stats.taskCount > 0 ? `| 任务: ${stats.taskCount}` : ''} + ${stats.imagePromptCount > 0 ? `| 图片提示词: ${stats.imagePromptCount}` : ''} + ${stats.videoPromptCount > 0 ? `| 视频提示词: ${stats.videoPromptCount}` : ''} +
+${state.debugEnabled ? '暂无匹配的日志' : '请先启用调试模式'}
+ ${!state.debugEnabled ? '' : ''} +暂无 LLM API 调用记录
+图片/视频/对话等 AI 接口调用会自动记录
+| ID | +${log.id} | +
| Endpoint | +${log.endpoint} | +
| 模型 | +${log.model} | +
| 类型 | +${log.taskType} | +
| HTTP 状态 | +${log.httpStatus || '-'} | +
| 耗时 | +${durationText} | +
| 参考图 | +${log.referenceImageCount || 0} 张 | +
| 结果类型 | +${log.resultType} | +
| 任务 ID | +${log.taskId} | +
| 结果 URL | ++ + ${log.resultUrl.length > 80 ? log.resultUrl.substring(0, 80) + '...' : log.resultUrl} + + + | +
| 错误信息 | +${escapeHtml(log.errorMessage)} | +
${escapeHtml(log.prompt)}
+ ${escapeHtml(log.requestBody)}
+ ${escapeHtml(log.resultText)}
+ ${escapeHtml(log.responseBody)}
+ 暂无内存日志
+页面启动、内存超限、错误和关闭时的快照会自动记录
+ID: ${log.id}
+时间: ${new Date(log.timestamp).toLocaleString('zh-CN')}
+URL: ${log.url || '-'}
+ 已用: ${(log.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)} MB
+总计: ${(log.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)} MB
+限制: ${(log.memory.jsHeapSizeLimit / (1024 * 1024)).toFixed(1)} MB
+使用率: ${memoryPercent.toFixed(1)}%
+ DOM节点: ${log.pageStats.domNodeCount || 0}
+Canvas: ${log.pageStats.canvasCount || 0}
+图片: ${log.pageStats.imageCount || 0}
+视频: ${log.pageStats.videoCount || 0}
+iframe: ${log.pageStats.iframeCount || 0}${log.pageStats.plaitElementCount !== undefined ? `\nPlait元素: ${log.pageStats.plaitElementCount}` : ''}
+ ${log.performance.longTaskDuration ? `长任务时长: ${log.performance.longTaskDuration.toFixed(0)}ms` : ''}${log.performance.freezeDuration ? `卡死时长: ${(log.performance.freezeDuration / 1000).toFixed(1)}s` : ''}${log.performance.fps !== undefined ? `\nFPS: ${log.performance.fps}` : ''}
+ ${log.error.type}: ${escapeHtml(log.error.message)}
+ ${log.error.stack ? `${escapeHtml(log.error.stack)}` : ''}
+ ${JSON.stringify(log.customData, null, 2)}
+ ${escapeHtml(message)}
+