aitu-app 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.DS_Store +0 -0
- package/README.md +47 -0
- package/_headers +84 -0
- package/_redirects +2 -0
- package/assets/ChatMessagesArea-CkUX81uB.js +251 -0
- package/assets/ChatMessagesArea-Di0Z80Zh.css +1 -0
- package/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/assets/Tableau10-B-NsZVaP.js +1 -0
- package/assets/Tableau10-Dnlau_Wv.js +1 -0
- package/assets/ToolboxDrawer-By1XMh8B.js +87 -0
- package/assets/ToolboxDrawer-fPqvDLQE.css +1 -0
- package/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/assets/ai-analyze-Db-iXol6.js +1 -0
- package/assets/arc-BZXVqUcI.js +1 -0
- package/assets/arc-ajYHRRnk.js +1 -0
- package/assets/array-B5oSNiGi.js +1 -0
- package/assets/array-BKyUJesY.js +1 -0
- package/assets/batch-image-generation-Baqb01Lm.js +6 -0
- package/assets/batch-image-generation-CbLMWmjk.css +1 -0
- package/assets/blockDiagram-38ab4fdb-BT3H_WVv.js +118 -0
- package/assets/blockDiagram-38ab4fdb-u0xYP3Lt.js +118 -0
- package/assets/c4Diagram-3d4e48cf-CBvM6zjM.js +10 -0
- package/assets/c4Diagram-3d4e48cf-WOIEVidH.js +10 -0
- package/assets/channel-BP25wTsw.js +1 -0
- package/assets/channel-HzrLNFUg.js +1 -0
- package/assets/classDiagram-70f12bd4-BMutcvFi.js +2 -0
- package/assets/classDiagram-70f12bd4-Cl9U1r5F.js +2 -0
- package/assets/classDiagram-v2-f2320105-C0agtbR4.js +2 -0
- package/assets/classDiagram-v2-f2320105-tCBzATK6.js +2 -0
- package/assets/clone-B69pF7Y_.js +1 -0
- package/assets/clone-oX7o-l4R.js +1 -0
- package/assets/createText-2e5e7dd3-CZ9_fscE.js +5 -0
- package/assets/createText-2e5e7dd3-idrqgJjU.js +7 -0
- package/assets/edges-e0da2a9e-C-RyePMV.js +4 -0
- package/assets/edges-e0da2a9e-DJXAjJSL.js +4 -0
- package/assets/erDiagram-9861fffd-DWJR_3zL.js +51 -0
- package/assets/erDiagram-9861fffd-x2Kcy95-.js +51 -0
- package/assets/flowDb-956e92f1-BgKjOIdz.js +10 -0
- package/assets/flowDb-956e92f1-CF6y18Tn.js +10 -0
- package/assets/flowDiagram-66a62f08-BPPw0wPU.js +4 -0
- package/assets/flowDiagram-66a62f08-CSAllSFf.js +4 -0
- package/assets/flowDiagram-v2-96b9c2cf-B-UGyXRi.js +1 -0
- package/assets/flowDiagram-v2-96b9c2cf-Cm596kxZ.js +1 -0
- package/assets/flowchart-elk-definition-4a651766-9XSRJbsr.js +139 -0
- package/assets/flowchart-elk-definition-4a651766-DWFN9DN3.js +139 -0
- package/assets/ganttDiagram-c361ad54-D9tbz9tQ.js +257 -0
- package/assets/ganttDiagram-c361ad54-ot5pUYpT.js +257 -0
- package/assets/gitGraphDiagram-72cf32ee-BFV3Mt8C.js +70 -0
- package/assets/gitGraphDiagram-72cf32ee-C6qFzgGh.js +70 -0
- package/assets/graph-BxwlF7JS.js +1 -0
- package/assets/graph-D-2Ldvxg.js +1 -0
- package/assets/grid-image-cM9AmYC8.js +1 -0
- package/assets/has-CgdIPiQG.js +1 -0
- package/assets/hasIn-4iY02rGN.js +1 -0
- package/assets/index-3862675e-CVZnpwDN.js +1 -0
- package/assets/index-3862675e-DqdI9cab.js +1 -0
- package/assets/index-B2dvADz8.css +1 -0
- package/assets/index-BicRPzXC.js +1 -0
- package/assets/index-Bs7-jmv6.css +1 -0
- package/assets/index-BwSGXyRr.js +99 -0
- package/assets/index-C1XdOOAn.css +1 -0
- package/assets/index-C4AKKbpQ.css +1 -0
- package/assets/index-CkpXFt8n.js +1 -0
- package/assets/index-CrxF9gFe.js +42 -0
- package/assets/index-DBWqXBIQ.js +93 -0
- package/assets/index-DI_5V2-m.js +3 -0
- package/assets/index-DWUAFoZG.js +2064 -0
- package/assets/index-Dn0YtZ2R.js +3 -0
- package/assets/index-e05Rs4M6.js +12 -0
- package/assets/index.dom-C3-224fz.js +1 -0
- package/assets/infoDiagram-f8f76790-CnrpwoOt.js +7 -0
- package/assets/infoDiagram-f8f76790-FKC1Sy9Y.js +7 -0
- package/assets/init-A0kIFD9x.js +1 -0
- package/assets/init-Gi6I4Gst.js +1 -0
- package/assets/inspiration-board-B_-BBBHt.js +1 -0
- package/assets/isEmpty-Dj2GV0v-.js +1 -0
- package/assets/journeyDiagram-49397b02-B7fP21sU.js +139 -0
- package/assets/journeyDiagram-49397b02-Dp3X9XWq.js +139 -0
- package/assets/katex-BbEIqZs1.js +261 -0
- package/assets/katex-Cu_Erd72.js +261 -0
- package/assets/layout-BD3yCK_X.js +1 -0
- package/assets/layout-DHHYqX7p.js +1 -0
- package/assets/line-B3bNrkzn.js +1 -0
- package/assets/line-B86HLuqu.js +1 -0
- package/assets/linear-DU2Ciymb.js +1 -0
- package/assets/linear-wCAlMhOS.js +1 -0
- package/assets/mermaid.core-DfVvnpgz.js +91 -0
- package/assets/mindmap-definition-fc14e90a-D1sxE3xG.js +425 -0
- package/assets/mindmap-definition-fc14e90a-YuSOJC7P.js +425 -0
- package/assets/ordinal-BRr1uYdk.js +1 -0
- package/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/path-CY0bYimO.js +1 -0
- package/assets/path-CbwjOpE9.js +1 -0
- package/assets/photo-wall-splitter-BVU2e0aS.js +1 -0
- package/assets/pick-Cvlwra4g.js +1 -0
- package/assets/pieDiagram-8a3498a8-B6mJUqro.js +35 -0
- package/assets/pieDiagram-8a3498a8-B91bWgo_.js +35 -0
- package/assets/quadrantDiagram-120e2f19-BxS8fQEz.js +7 -0
- package/assets/quadrantDiagram-120e2f19-DwudONqx.js +7 -0
- package/assets/requirementDiagram-deff3bca-DygaMIoy.js +52 -0
- package/assets/requirementDiagram-deff3bca-v9xlgfS8.js +52 -0
- package/assets/sankeyDiagram-04a897e0-BV23dp4l.js +8 -0
- package/assets/sankeyDiagram-04a897e0-BXCiXiyw.js +8 -0
- package/assets/sequenceDiagram-704730f1-CObRpNi4.js +122 -0
- package/assets/sequenceDiagram-704730f1-Ck69A6wI.js +122 -0
- package/assets/settings-dialog-BlCO49C4.js +1 -0
- package/assets/settings-dialog-QUxXj54T.css +1 -0
- package/assets/stateDiagram-587899a1-J_G6I0oo.js +1 -0
- package/assets/stateDiagram-587899a1-z-tKclr3.js +1 -0
- package/assets/stateDiagram-v2-d93cdb3a-DsThtOzP.js +1 -0
- package/assets/stateDiagram-v2-d93cdb3a-XIvq5t8a.js +1 -0
- package/assets/styles-6aaf32cf-1fjuNMUk.js +207 -0
- package/assets/styles-6aaf32cf-DT2rVNfQ.js +207 -0
- package/assets/styles-9a916d00-fLeUSina.js +160 -0
- package/assets/styles-9a916d00-q64Umkis.js +160 -0
- package/assets/styles-c10674c1-BWlxVc3Q.js +116 -0
- package/assets/styles-c10674c1-CtYpjMYU.js +116 -0
- package/assets/svgDrawCommon-08f97a94-C_DhKfny.js +1 -0
- package/assets/svgDrawCommon-08f97a94-DSBqmUv2.js +1 -0
- package/assets/timeline-definition-85554ec2-AKpzwLPN.js +61 -0
- package/assets/timeline-definition-85554ec2-dTkYwoLF.js +61 -0
- package/assets/ttd-dialog-CxiaIUuJ.js +47 -0
- package/assets/ttd-dialog-DCapefb6.css +1 -0
- package/assets/upload-4sxUU7q_.js +1 -0
- package/assets/video-recovery-service-BckHbSyK.js +1 -0
- package/assets/web-vitals-DcvjKPr-.js +1 -0
- package/assets/winbox.bundle.min-CoRPjCs5.js +1 -0
- package/assets/xlsx-CkFp8p6R.js +105 -0
- package/assets/xychartDiagram-e933f94c-DCmvL0ag.js +7 -0
- package/assets/xychartDiagram-e933f94c-aqOiXp_u.js +7 -0
- package/batch-image.html +1616 -0
- package/favicon.ico +0 -0
- package/icons/README.md +55 -0
- package/icons/aitu10.png +0 -0
- package/icons/android-chrome-192x192.png +0 -0
- package/icons/android-chrome-512x512.png +0 -0
- package/icons/apple-touch-icon.png +0 -0
- package/icons/favicon-16x16.png +0 -0
- package/icons/favicon-16x16.svg +539 -0
- package/icons/favicon-32x32.png +0 -0
- package/icons/favicon-32x32.svg +539 -0
- package/icons/favicon-new.svg +539 -0
- package/icons/favicon-new.svg.png +0 -0
- package/icons/icon-192x192.svg +539 -0
- package/icons/icon-512x512.svg +539 -0
- package/icons/icon-96x96.png +0 -0
- package/icons/icon-96x96.svg +539 -0
- package/iframe-test.html +340 -0
- package/index.html +105 -0
- package/init.json +6 -0
- package/logo/cardid.jpg +0 -0
- package/logo/group-qr.png +0 -0
- package/logo/logo_drawnix_h.svg +539 -0
- package/logo/logo_drawnix_h_dark.svg +539 -0
- package/logo/logo_drawnix_new.svg +539 -0
- package/manifest.json +52 -0
- package/package.json +31 -0
- package/product_showcase/aitu-01.png +0 -0
- package/product_showcase/aitu-02.png +0 -0
- package/product_showcase/aitu-03.png +0 -0
- package/product_showcase/aitu-04.png +0 -0
- package/product_showcase/aitu-05.png +0 -0
- package/product_showcase/aitu-06.png +0 -0
- package/product_showcase/case-1.png +0 -0
- package/product_showcase/case-2.png +0 -0
- package/robots.txt +13 -0
- package/sitemap.xml +29 -0
- package/sw-debug/app.js +3069 -0
- package/sw-debug/console-entry.js +80 -0
- package/sw-debug/log-entry.js +452 -0
- package/sw-debug/log-panel.js +309 -0
- package/sw-debug/postmessage-entry.js +117 -0
- package/sw-debug/status-panel.js +125 -0
- package/sw-debug/styles.css +2103 -0
- package/sw-debug/sw-communication.js +208 -0
- package/sw-debug/utils.js +112 -0
- package/sw-debug.html +685 -0
- package/sw.js +58 -0
- package/version.json +10 -0
package/sw-debug/app.js
ADDED
|
@@ -0,0 +1,3069 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SW Debug Panel - Main Application
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { downloadJson, formatTime } from './utils.js';
|
|
6
|
+
import { createLogEntry } from './log-entry.js';
|
|
7
|
+
import { createConsoleEntry } from './console-entry.js';
|
|
8
|
+
import { createPostMessageEntry } from './postmessage-entry.js';
|
|
9
|
+
import { updateSwStatus, updateStatusPanel, updateDebugButton } from './status-panel.js';
|
|
10
|
+
import { extractUniqueTypes, updateTypeSelectOptions } from './log-panel.js';
|
|
11
|
+
import {
|
|
12
|
+
enableDebug,
|
|
13
|
+
disableDebug,
|
|
14
|
+
refreshStatus,
|
|
15
|
+
clearFetchLogs,
|
|
16
|
+
clearConsoleLogs,
|
|
17
|
+
loadConsoleLogs,
|
|
18
|
+
loadPostMessageLogs,
|
|
19
|
+
clearPostMessageLogs as clearPostMessageLogsInSW,
|
|
20
|
+
checkSwReady,
|
|
21
|
+
registerMessageHandlers,
|
|
22
|
+
onControllerChange,
|
|
23
|
+
setPostMessageLogCallback,
|
|
24
|
+
} from './sw-communication.js';
|
|
25
|
+
|
|
26
|
+
// Domain blacklist - requests from these domains will be hidden
|
|
27
|
+
const DOMAIN_BLACKLIST = [
|
|
28
|
+
'us.i.posthog.com',
|
|
29
|
+
'us-assets.i.posthog.com',
|
|
30
|
+
'posthog.com',
|
|
31
|
+
'google-analytics.com',
|
|
32
|
+
'googletagmanager.com',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if URL is in the domain blacklist
|
|
37
|
+
* @param {string} url
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
function isBlacklistedUrl(url) {
|
|
41
|
+
if (!url) return false;
|
|
42
|
+
try {
|
|
43
|
+
const urlObj = new URL(url);
|
|
44
|
+
return DOMAIN_BLACKLIST.some(domain =>
|
|
45
|
+
urlObj.hostname === domain || urlObj.hostname.endsWith('.' + domain)
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Filter logs by time range
|
|
54
|
+
* @param {Array} logs
|
|
55
|
+
* @param {string} timeRangeMinutes - Minutes as string, or empty for all
|
|
56
|
+
* @returns {Array}
|
|
57
|
+
*/
|
|
58
|
+
function filterByTimeRange(logs, timeRangeMinutes) {
|
|
59
|
+
if (!timeRangeMinutes) return logs;
|
|
60
|
+
|
|
61
|
+
const minutes = parseInt(timeRangeMinutes);
|
|
62
|
+
if (isNaN(minutes)) return logs;
|
|
63
|
+
|
|
64
|
+
const cutoffTime = Date.now() - (minutes * 60 * 1000);
|
|
65
|
+
return logs.filter(log => log.timestamp >= cutoffTime);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a request is slow (> 1 second)
|
|
70
|
+
* @param {number} duration - Duration in milliseconds
|
|
71
|
+
* @returns {'normal'|'slow'|'very-slow'}
|
|
72
|
+
*/
|
|
73
|
+
function getSpeedClass(duration) {
|
|
74
|
+
if (!duration) return 'normal';
|
|
75
|
+
if (duration >= 3000) return 'very-slow';
|
|
76
|
+
if (duration >= 1000) return 'slow';
|
|
77
|
+
return 'normal';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Toggle pause state
|
|
82
|
+
*/
|
|
83
|
+
function togglePause() {
|
|
84
|
+
state.isPaused = !state.isPaused;
|
|
85
|
+
updatePauseButton();
|
|
86
|
+
|
|
87
|
+
if (!state.isPaused && state.pendingLogs.length > 0) {
|
|
88
|
+
// Apply pending logs when resuming
|
|
89
|
+
state.pendingLogs.forEach(log => {
|
|
90
|
+
addOrUpdateLog(log, true); // true = skip render
|
|
91
|
+
});
|
|
92
|
+
state.pendingLogs = [];
|
|
93
|
+
renderLogs();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Toggle slow request filter
|
|
99
|
+
*/
|
|
100
|
+
function toggleSlowRequestFilter() {
|
|
101
|
+
state.filterSlowOnly = !state.filterSlowOnly;
|
|
102
|
+
updateSlowRequestsUI();
|
|
103
|
+
renderLogs();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update slow requests UI (highlight when filter is active)
|
|
108
|
+
*/
|
|
109
|
+
function updateSlowRequestsUI() {
|
|
110
|
+
const wrapper = elements.statSlowRequestsWrapper;
|
|
111
|
+
if (wrapper) {
|
|
112
|
+
if (state.filterSlowOnly) {
|
|
113
|
+
wrapper.classList.add('active');
|
|
114
|
+
} else {
|
|
115
|
+
wrapper.classList.remove('active');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Update pause button appearance
|
|
122
|
+
*/
|
|
123
|
+
function updatePauseButton() {
|
|
124
|
+
const btn = elements.togglePauseBtn;
|
|
125
|
+
if (!btn) return;
|
|
126
|
+
|
|
127
|
+
if (state.isPaused) {
|
|
128
|
+
btn.textContent = `⏸️ 暂停`;
|
|
129
|
+
if (state.pendingLogs.length > 0) {
|
|
130
|
+
btn.textContent += ` (${state.pendingLogs.length})`;
|
|
131
|
+
}
|
|
132
|
+
btn.classList.add('paused');
|
|
133
|
+
} else {
|
|
134
|
+
btn.textContent = '▶️ 实时';
|
|
135
|
+
btn.classList.remove('paused');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Update fetch statistics panel
|
|
141
|
+
*/
|
|
142
|
+
function updateFetchStats() {
|
|
143
|
+
const logs = state.logs.filter(l => !isBlacklistedUrl(l.url));
|
|
144
|
+
|
|
145
|
+
// Total requests
|
|
146
|
+
if (elements.statTotalRequests) {
|
|
147
|
+
elements.statTotalRequests.textContent = logs.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Success rate
|
|
151
|
+
if (elements.statSuccessRate) {
|
|
152
|
+
const successCount = logs.filter(l => l.status >= 200 && l.status < 400).length;
|
|
153
|
+
const rate = logs.length > 0 ? ((successCount / logs.length) * 100).toFixed(1) : 0;
|
|
154
|
+
elements.statSuccessRate.textContent = `${rate}%`;
|
|
155
|
+
elements.statSuccessRate.style.color = rate >= 95 ? 'var(--success-color)' : (rate >= 80 ? 'var(--warning-color)' : 'var(--error-color)');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Average duration
|
|
159
|
+
if (elements.statAvgDuration) {
|
|
160
|
+
const durations = logs.filter(l => l.duration > 0).map(l => l.duration);
|
|
161
|
+
const avg = durations.length > 0 ? (durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
|
|
162
|
+
elements.statAvgDuration.textContent = avg > 0 ? `${Math.round(avg)}ms` : '-';
|
|
163
|
+
elements.statAvgDuration.style.color = avg < 500 ? 'var(--success-color)' : (avg < 1000 ? 'var(--warning-color)' : 'var(--error-color)');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Cache hit rate
|
|
167
|
+
if (elements.statCacheHit) {
|
|
168
|
+
const cachedCount = logs.filter(l => l.cached).length;
|
|
169
|
+
const rate = logs.length > 0 ? ((cachedCount / logs.length) * 100).toFixed(1) : 0;
|
|
170
|
+
elements.statCacheHit.textContent = `${rate}%`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Slow requests count
|
|
174
|
+
if (elements.statSlowRequests) {
|
|
175
|
+
const slowCount = logs.filter(l => l.duration >= 1000).length;
|
|
176
|
+
elements.statSlowRequests.textContent = slowCount;
|
|
177
|
+
elements.statSlowRequests.style.color = slowCount === 0 ? 'var(--success-color)' : 'var(--warning-color)';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Duration distribution chart
|
|
181
|
+
updateDurationChart(logs);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Update duration distribution chart
|
|
186
|
+
*/
|
|
187
|
+
function updateDurationChart(logs) {
|
|
188
|
+
const logsWithDuration = logs.filter(l => l.duration > 0);
|
|
189
|
+
const total = logsWithDuration.length;
|
|
190
|
+
|
|
191
|
+
if (total === 0) {
|
|
192
|
+
if (elements.chartFast) elements.chartFast.style.width = '0%';
|
|
193
|
+
if (elements.chartMedium) elements.chartMedium.style.width = '0%';
|
|
194
|
+
if (elements.chartSlow) elements.chartSlow.style.width = '0%';
|
|
195
|
+
if (elements.chartVerySlow) elements.chartVerySlow.style.width = '0%';
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Categorize by duration
|
|
200
|
+
const fast = logsWithDuration.filter(l => l.duration < 100).length;
|
|
201
|
+
const medium = logsWithDuration.filter(l => l.duration >= 100 && l.duration < 500).length;
|
|
202
|
+
const slow = logsWithDuration.filter(l => l.duration >= 500 && l.duration < 1000).length;
|
|
203
|
+
const verySlow = logsWithDuration.filter(l => l.duration >= 1000).length;
|
|
204
|
+
|
|
205
|
+
// Calculate percentages
|
|
206
|
+
const fastPct = (fast / total) * 100;
|
|
207
|
+
const mediumPct = (medium / total) * 100;
|
|
208
|
+
const slowPct = (slow / total) * 100;
|
|
209
|
+
const verySlowPct = (verySlow / total) * 100;
|
|
210
|
+
|
|
211
|
+
// Update chart bars
|
|
212
|
+
if (elements.chartFast) {
|
|
213
|
+
elements.chartFast.style.width = `${fastPct}%`;
|
|
214
|
+
elements.chartFast.title = `<100ms: ${fast} (${fastPct.toFixed(1)}%)`;
|
|
215
|
+
}
|
|
216
|
+
if (elements.chartMedium) {
|
|
217
|
+
elements.chartMedium.style.width = `${mediumPct}%`;
|
|
218
|
+
elements.chartMedium.title = `100-500ms: ${medium} (${mediumPct.toFixed(1)}%)`;
|
|
219
|
+
}
|
|
220
|
+
if (elements.chartSlow) {
|
|
221
|
+
elements.chartSlow.style.width = `${slowPct}%`;
|
|
222
|
+
elements.chartSlow.title = `500ms-1s: ${slow} (${slowPct.toFixed(1)}%)`;
|
|
223
|
+
}
|
|
224
|
+
if (elements.chartVerySlow) {
|
|
225
|
+
elements.chartVerySlow.style.width = `${verySlowPct}%`;
|
|
226
|
+
elements.chartVerySlow.title = `>1s: ${verySlow} (${verySlowPct.toFixed(1)}%)`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Export fetch logs as CSV
|
|
232
|
+
*/
|
|
233
|
+
function exportFetchCSV() {
|
|
234
|
+
const filteredLogs = getFilteredFetchLogs();
|
|
235
|
+
if (filteredLogs.length === 0) {
|
|
236
|
+
alert('没有可导出的日志');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// CSV header
|
|
241
|
+
const headers = ['时间', '方法', '状态', 'URL', '耗时(ms)', '类型', '缓存'];
|
|
242
|
+
const rows = [headers.join(',')];
|
|
243
|
+
|
|
244
|
+
// CSV rows
|
|
245
|
+
filteredLogs.forEach(log => {
|
|
246
|
+
const time = new Date(log.timestamp).toLocaleString('zh-CN', { hour12: false });
|
|
247
|
+
const row = [
|
|
248
|
+
`"${time}"`,
|
|
249
|
+
log.method || 'GET',
|
|
250
|
+
log.status || '-',
|
|
251
|
+
`"${(log.url || '').replace(/"/g, '""')}"`,
|
|
252
|
+
log.duration || '',
|
|
253
|
+
log.requestType || '-',
|
|
254
|
+
log.cached ? '是' : '否'
|
|
255
|
+
];
|
|
256
|
+
rows.push(row.join(','));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const csvContent = '\uFEFF' + rows.join('\n'); // BOM for Excel
|
|
260
|
+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
261
|
+
const url = URL.createObjectURL(blob);
|
|
262
|
+
const a = document.createElement('a');
|
|
263
|
+
a.href = url;
|
|
264
|
+
a.download = `fetch-logs-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
265
|
+
a.click();
|
|
266
|
+
URL.revokeObjectURL(url);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Show shortcuts modal
|
|
271
|
+
*/
|
|
272
|
+
function showShortcutsModal() {
|
|
273
|
+
if (elements.shortcutsModalOverlay) {
|
|
274
|
+
elements.shortcutsModalOverlay.style.display = 'flex';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Close shortcuts modal
|
|
280
|
+
*/
|
|
281
|
+
function closeShortcutsModal() {
|
|
282
|
+
if (elements.shortcutsModalOverlay) {
|
|
283
|
+
elements.shortcutsModalOverlay.style.display = 'none';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Toggle bookmark for a log entry
|
|
289
|
+
* @param {string} logId
|
|
290
|
+
*/
|
|
291
|
+
function toggleBookmark(logId) {
|
|
292
|
+
if (state.bookmarkedLogIds.has(logId)) {
|
|
293
|
+
state.bookmarkedLogIds.delete(logId);
|
|
294
|
+
} else {
|
|
295
|
+
state.bookmarkedLogIds.add(logId);
|
|
296
|
+
}
|
|
297
|
+
// Save to localStorage
|
|
298
|
+
saveBookmarks();
|
|
299
|
+
renderLogs();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Save bookmarks to localStorage
|
|
304
|
+
*/
|
|
305
|
+
function saveBookmarks() {
|
|
306
|
+
try {
|
|
307
|
+
localStorage.setItem('sw-debug-bookmarks', JSON.stringify([...state.bookmarkedLogIds]));
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.error('Failed to save bookmarks:', e);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Load bookmarks from localStorage
|
|
315
|
+
*/
|
|
316
|
+
function loadBookmarks() {
|
|
317
|
+
try {
|
|
318
|
+
const saved = localStorage.getItem('sw-debug-bookmarks');
|
|
319
|
+
if (saved) {
|
|
320
|
+
state.bookmarkedLogIds = new Set(JSON.parse(saved));
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
console.error('Failed to load bookmarks:', e);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Toggle theme between light and dark
|
|
329
|
+
*/
|
|
330
|
+
function toggleTheme() {
|
|
331
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
332
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
333
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
334
|
+
updateThemeIcon(newTheme);
|
|
335
|
+
saveTheme(newTheme);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Update theme icon (SVG)
|
|
340
|
+
*/
|
|
341
|
+
function updateThemeIcon(theme) {
|
|
342
|
+
if (elements.themeIcon) {
|
|
343
|
+
if (theme === 'dark') {
|
|
344
|
+
// Sun icon for dark mode (click to switch to light)
|
|
345
|
+
elements.themeIcon.innerHTML = '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>';
|
|
346
|
+
} else {
|
|
347
|
+
// Moon icon for light mode (click to switch to dark)
|
|
348
|
+
elements.themeIcon.innerHTML = '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>';
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Save theme preference
|
|
355
|
+
*/
|
|
356
|
+
function saveTheme(theme) {
|
|
357
|
+
try {
|
|
358
|
+
localStorage.setItem('sw-debug-theme', theme);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
console.error('Failed to save theme:', e);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Load saved theme preference
|
|
366
|
+
*/
|
|
367
|
+
function loadTheme() {
|
|
368
|
+
try {
|
|
369
|
+
const savedTheme = localStorage.getItem('sw-debug-theme');
|
|
370
|
+
// Also check system preference
|
|
371
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
372
|
+
const theme = savedTheme || (prefersDark ? 'dark' : 'light');
|
|
373
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
374
|
+
updateThemeIcon(theme);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
console.error('Failed to load theme:', e);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Show settings modal
|
|
382
|
+
*/
|
|
383
|
+
function showSettingsModal() {
|
|
384
|
+
if (elements.settingsModalOverlay) {
|
|
385
|
+
// Populate current values
|
|
386
|
+
if (elements.settingMaxLogs) {
|
|
387
|
+
elements.settingMaxLogs.value = state.settings.maxLogs.toString();
|
|
388
|
+
}
|
|
389
|
+
if (elements.settingAutoClean) {
|
|
390
|
+
elements.settingAutoClean.value = state.settings.autoCleanMinutes.toString();
|
|
391
|
+
}
|
|
392
|
+
if (elements.settingKeepBookmarks) {
|
|
393
|
+
elements.settingKeepBookmarks.checked = state.settings.keepBookmarks;
|
|
394
|
+
}
|
|
395
|
+
elements.settingsModalOverlay.style.display = 'flex';
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Close settings modal
|
|
401
|
+
*/
|
|
402
|
+
function closeSettingsModal() {
|
|
403
|
+
if (elements.settingsModalOverlay) {
|
|
404
|
+
elements.settingsModalOverlay.style.display = 'none';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Save settings
|
|
410
|
+
*/
|
|
411
|
+
function saveSettings() {
|
|
412
|
+
const newSettings = {
|
|
413
|
+
maxLogs: parseInt(elements.settingMaxLogs?.value || '500'),
|
|
414
|
+
autoCleanMinutes: parseInt(elements.settingAutoClean?.value || '0'),
|
|
415
|
+
keepBookmarks: elements.settingKeepBookmarks?.checked ?? true,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
state.settings = newSettings;
|
|
419
|
+
|
|
420
|
+
// Save to localStorage
|
|
421
|
+
try {
|
|
422
|
+
localStorage.setItem('sw-debug-settings', JSON.stringify(newSettings));
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.error('Failed to save settings:', e);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Apply new max logs limit
|
|
428
|
+
applyMaxLogsLimit();
|
|
429
|
+
|
|
430
|
+
// Setup auto clean timer
|
|
431
|
+
setupAutoCleanTimer();
|
|
432
|
+
|
|
433
|
+
closeSettingsModal();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Load saved settings
|
|
438
|
+
*/
|
|
439
|
+
function loadSettings() {
|
|
440
|
+
try {
|
|
441
|
+
const saved = localStorage.getItem('sw-debug-settings');
|
|
442
|
+
if (saved) {
|
|
443
|
+
state.settings = { ...state.settings, ...JSON.parse(saved) };
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.error('Failed to load settings:', e);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Setup auto clean timer based on saved settings
|
|
450
|
+
setupAutoCleanTimer();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Apply max logs limit to current logs
|
|
455
|
+
*/
|
|
456
|
+
function applyMaxLogsLimit() {
|
|
457
|
+
const maxLogs = state.settings.maxLogs;
|
|
458
|
+
|
|
459
|
+
if (state.logs.length > maxLogs) {
|
|
460
|
+
// Keep bookmarked logs if setting is enabled
|
|
461
|
+
if (state.settings.keepBookmarks) {
|
|
462
|
+
const bookmarked = state.logs.filter(l => state.bookmarkedLogIds.has(l.id));
|
|
463
|
+
const nonBookmarked = state.logs.filter(l => !state.bookmarkedLogIds.has(l.id));
|
|
464
|
+
state.logs = [...bookmarked, ...nonBookmarked.slice(0, maxLogs - bookmarked.length)];
|
|
465
|
+
} else {
|
|
466
|
+
state.logs = state.logs.slice(0, maxLogs);
|
|
467
|
+
}
|
|
468
|
+
renderLogs();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Setup auto clean timer
|
|
474
|
+
*/
|
|
475
|
+
function setupAutoCleanTimer() {
|
|
476
|
+
// Clear existing timer
|
|
477
|
+
if (state.autoCleanTimerId) {
|
|
478
|
+
clearInterval(state.autoCleanTimerId);
|
|
479
|
+
state.autoCleanTimerId = null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const minutes = state.settings.autoCleanMinutes;
|
|
483
|
+
if (minutes <= 0) return;
|
|
484
|
+
|
|
485
|
+
// Run cleanup every minute
|
|
486
|
+
state.autoCleanTimerId = setInterval(() => {
|
|
487
|
+
autoCleanLogs();
|
|
488
|
+
}, 60000);
|
|
489
|
+
|
|
490
|
+
// Also run immediately
|
|
491
|
+
autoCleanLogs();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Auto clean old logs
|
|
496
|
+
*/
|
|
497
|
+
function autoCleanLogs() {
|
|
498
|
+
const minutes = state.settings.autoCleanMinutes;
|
|
499
|
+
if (minutes <= 0) return;
|
|
500
|
+
|
|
501
|
+
const cutoffTime = Date.now() - (minutes * 60 * 1000);
|
|
502
|
+
const beforeCount = state.logs.length;
|
|
503
|
+
|
|
504
|
+
if (state.settings.keepBookmarks) {
|
|
505
|
+
state.logs = state.logs.filter(l =>
|
|
506
|
+
l.timestamp >= cutoffTime || state.bookmarkedLogIds.has(l.id)
|
|
507
|
+
);
|
|
508
|
+
} else {
|
|
509
|
+
state.logs = state.logs.filter(l => l.timestamp >= cutoffTime);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (state.logs.length !== beforeCount) {
|
|
513
|
+
renderLogs();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Toggle select mode for batch operations
|
|
519
|
+
*/
|
|
520
|
+
function toggleSelectMode() {
|
|
521
|
+
state.isSelectMode = !state.isSelectMode;
|
|
522
|
+
state.selectedLogIds.clear();
|
|
523
|
+
updateSelectModeUI();
|
|
524
|
+
renderLogs();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Update select mode UI
|
|
529
|
+
*/
|
|
530
|
+
function updateSelectModeUI() {
|
|
531
|
+
if (elements.toggleSelectModeBtn) {
|
|
532
|
+
elements.toggleSelectModeBtn.textContent = state.isSelectMode ? '✅ 取消' : '☑️ 选择';
|
|
533
|
+
elements.toggleSelectModeBtn.style.background = state.isSelectMode ? 'var(--primary-color)' : '';
|
|
534
|
+
elements.toggleSelectModeBtn.style.color = state.isSelectMode ? '#fff' : '';
|
|
535
|
+
}
|
|
536
|
+
if (elements.batchActionsEl) {
|
|
537
|
+
elements.batchActionsEl.style.display = state.isSelectMode ? 'flex' : 'none';
|
|
538
|
+
}
|
|
539
|
+
updateSelectedCount();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Update selected count display
|
|
544
|
+
*/
|
|
545
|
+
function updateSelectedCount() {
|
|
546
|
+
if (elements.selectedCountEl) {
|
|
547
|
+
elements.selectedCountEl.textContent = `已选 ${state.selectedLogIds.size} 条`;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Toggle selection of a log entry
|
|
553
|
+
*/
|
|
554
|
+
function toggleLogSelection(logId) {
|
|
555
|
+
if (state.selectedLogIds.has(logId)) {
|
|
556
|
+
state.selectedLogIds.delete(logId);
|
|
557
|
+
} else {
|
|
558
|
+
state.selectedLogIds.add(logId);
|
|
559
|
+
}
|
|
560
|
+
updateSelectedCount();
|
|
561
|
+
// Update checkbox in DOM
|
|
562
|
+
const checkbox = document.querySelector(`.log-select-checkbox[data-id="${logId}"]`);
|
|
563
|
+
if (checkbox) {
|
|
564
|
+
checkbox.checked = state.selectedLogIds.has(logId);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Select all visible logs
|
|
570
|
+
*/
|
|
571
|
+
function selectAllLogs() {
|
|
572
|
+
const filteredLogs = getFilteredFetchLogs();
|
|
573
|
+
const allSelected = filteredLogs.every(l => state.selectedLogIds.has(l.id));
|
|
574
|
+
|
|
575
|
+
if (allSelected) {
|
|
576
|
+
// Deselect all
|
|
577
|
+
filteredLogs.forEach(l => state.selectedLogIds.delete(l.id));
|
|
578
|
+
} else {
|
|
579
|
+
// Select all
|
|
580
|
+
filteredLogs.forEach(l => state.selectedLogIds.add(l.id));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
updateSelectedCount();
|
|
584
|
+
renderLogs();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Batch bookmark selected logs
|
|
589
|
+
*/
|
|
590
|
+
function batchBookmarkLogs() {
|
|
591
|
+
if (state.selectedLogIds.size === 0) {
|
|
592
|
+
alert('请先选择日志');
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
state.selectedLogIds.forEach(id => {
|
|
597
|
+
state.bookmarkedLogIds.add(id);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
saveBookmarks();
|
|
601
|
+
state.selectedLogIds.clear();
|
|
602
|
+
updateSelectedCount();
|
|
603
|
+
renderLogs();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Batch delete selected logs
|
|
608
|
+
*/
|
|
609
|
+
function batchDeleteLogs() {
|
|
610
|
+
if (state.selectedLogIds.size === 0) {
|
|
611
|
+
alert('请先选择日志');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (!confirm(`确定要删除选中的 ${state.selectedLogIds.size} 条日志吗?`)) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
state.logs = state.logs.filter(l => !state.selectedLogIds.has(l.id));
|
|
620
|
+
|
|
621
|
+
// Also remove from bookmarks
|
|
622
|
+
state.selectedLogIds.forEach(id => {
|
|
623
|
+
state.bookmarkedLogIds.delete(id);
|
|
624
|
+
});
|
|
625
|
+
saveBookmarks();
|
|
626
|
+
|
|
627
|
+
state.selectedLogIds.clear();
|
|
628
|
+
updateSelectedCount();
|
|
629
|
+
renderLogs();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Find related requests based on URL pattern or timing
|
|
634
|
+
* @param {object} log - The log entry to find related requests for
|
|
635
|
+
* @returns {Array} - Array of related log entries
|
|
636
|
+
*/
|
|
637
|
+
function findRelatedRequests(log) {
|
|
638
|
+
if (!log.url) return [];
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
const urlObj = new URL(log.url);
|
|
642
|
+
const basePath = urlObj.pathname.split('/').slice(0, 3).join('/'); // First 2 path segments
|
|
643
|
+
const timestamp = log.timestamp;
|
|
644
|
+
const timeWindow = 5000; // 5 second window
|
|
645
|
+
|
|
646
|
+
return state.logs.filter(l => {
|
|
647
|
+
if (l.id === log.id) return false;
|
|
648
|
+
if (!l.url) return false;
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
const otherUrl = new URL(l.url);
|
|
652
|
+
|
|
653
|
+
// Same host
|
|
654
|
+
if (otherUrl.hostname !== urlObj.hostname) return false;
|
|
655
|
+
|
|
656
|
+
// Similar path OR within time window
|
|
657
|
+
const otherBasePath = otherUrl.pathname.split('/').slice(0, 3).join('/');
|
|
658
|
+
const pathMatch = otherBasePath === basePath;
|
|
659
|
+
const timeMatch = Math.abs(l.timestamp - timestamp) <= timeWindow;
|
|
660
|
+
|
|
661
|
+
return pathMatch || timeMatch;
|
|
662
|
+
} catch {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}).slice(0, 10); // Limit to 10 related requests
|
|
666
|
+
} catch {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Render related requests section for log details
|
|
673
|
+
* @param {object} log - The log entry
|
|
674
|
+
* @returns {string} - HTML string
|
|
675
|
+
*/
|
|
676
|
+
function renderRelatedRequests(log) {
|
|
677
|
+
const related = findRelatedRequests(log);
|
|
678
|
+
if (related.length === 0) return '';
|
|
679
|
+
|
|
680
|
+
const items = related.map(r => {
|
|
681
|
+
const time = new Date(r.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
|
|
682
|
+
const status = r.status || '...';
|
|
683
|
+
const statusClass = r.status >= 200 && r.status < 400 ? 'success' : (r.status >= 400 ? 'error' : '');
|
|
684
|
+
const duration = r.duration ? `${r.duration}ms` : '-';
|
|
685
|
+
const displayUrl = r.url?.substring(0, 60) + (r.url?.length > 60 ? '...' : '');
|
|
686
|
+
|
|
687
|
+
return `
|
|
688
|
+
<div class="related-request" data-id="${r.id}" style="padding: 4px 8px; cursor: pointer; border-radius: 4px; margin-bottom: 4px; background: var(--bg-tertiary);">
|
|
689
|
+
<span style="color: var(--text-muted); font-size: 11px;">${time}</span>
|
|
690
|
+
<span class="log-status ${statusClass}" style="font-size: 11px; margin-left: 8px;">${status}</span>
|
|
691
|
+
<span style="margin-left: 8px; font-size: 12px;">${displayUrl}</span>
|
|
692
|
+
<span style="color: var(--text-muted); font-size: 11px; margin-left: 8px;">${duration}</span>
|
|
693
|
+
</div>
|
|
694
|
+
`;
|
|
695
|
+
}).join('');
|
|
696
|
+
|
|
697
|
+
return `
|
|
698
|
+
<div class="detail-section" style="margin-top: 12px;">
|
|
699
|
+
<h4>🔗 相关请求 (${related.length})</h4>
|
|
700
|
+
<div class="related-requests-list" style="margin-top: 8px; max-height: 200px; overflow-y: auto;">
|
|
701
|
+
${items}
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Make renderRelatedRequests available globally
|
|
708
|
+
window.renderRelatedRequests = renderRelatedRequests;
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Update error dot indicators
|
|
712
|
+
*/
|
|
713
|
+
function updateErrorDots() {
|
|
714
|
+
// Console errors
|
|
715
|
+
if (elements.consoleErrorDot) {
|
|
716
|
+
const hasErrors = state.consoleLogs.some(l => l.logLevel === 'error');
|
|
717
|
+
elements.consoleErrorDot.style.display = (hasErrors && state.activeTab !== 'console') ? 'inline-block' : 'none';
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// LLM API errors
|
|
721
|
+
if (elements.llmapiErrorDot) {
|
|
722
|
+
const hasErrors = state.llmapiLogs.some(l => l.status === 'error');
|
|
723
|
+
elements.llmapiErrorDot.style.display = (hasErrors && state.activeTab !== 'llmapi') ? 'inline-block' : 'none';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Crash/memory errors
|
|
727
|
+
if (elements.crashErrorDot) {
|
|
728
|
+
const hasErrors = state.crashLogs.some(l => l.type === 'error' || l.type === 'freeze' || l.type === 'whitescreen');
|
|
729
|
+
elements.crashErrorDot.style.display = (hasErrors && state.activeTab !== 'crash') ? 'inline-block' : 'none';
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Application State
|
|
734
|
+
const state = {
|
|
735
|
+
debugEnabled: false,
|
|
736
|
+
logs: [],
|
|
737
|
+
consoleLogs: [],
|
|
738
|
+
postmessageLogs: [],
|
|
739
|
+
crashLogs: [], // Crash snapshots
|
|
740
|
+
llmapiLogs: [], // LLM API call logs
|
|
741
|
+
swStatus: null, // SW status data for export
|
|
742
|
+
autoScroll: true,
|
|
743
|
+
activeTab: 'fetch',
|
|
744
|
+
expandedLogIds: new Set(), // Track expanded fetch log IDs
|
|
745
|
+
expandedStackIds: new Set(), // Track expanded console stack IDs
|
|
746
|
+
expandedPmIds: new Set(), // Track expanded postmessage log IDs
|
|
747
|
+
expandedCrashIds: new Set(), // Track expanded crash log IDs
|
|
748
|
+
expandedLLMApiIds: new Set(), // Track expanded LLM API log IDs
|
|
749
|
+
// New states for enhanced features
|
|
750
|
+
bookmarkedLogIds: new Set(), // Bookmarked/starred log IDs
|
|
751
|
+
showBookmarksOnly: false, // Filter to show only bookmarked logs
|
|
752
|
+
filterSlowOnly: false, // Filter to show only slow requests (>1s)
|
|
753
|
+
isSelectMode: false, // Batch select mode
|
|
754
|
+
selectedLogIds: new Set(), // Selected log IDs for batch operations
|
|
755
|
+
isPaused: false, // Pause real-time updates
|
|
756
|
+
pendingLogs: [], // Logs received while paused
|
|
757
|
+
hasNewErrors: false, // Track new errors for tab indicator
|
|
758
|
+
hasNewCrashLogs: false, // Track new crash logs
|
|
759
|
+
hasNewLLMApiErrors: false, // Track new LLM API errors
|
|
760
|
+
// Settings
|
|
761
|
+
settings: {
|
|
762
|
+
maxLogs: 500,
|
|
763
|
+
autoCleanMinutes: 0,
|
|
764
|
+
keepBookmarks: true,
|
|
765
|
+
},
|
|
766
|
+
autoCleanTimerId: null,
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
// Memory monitoring interval
|
|
770
|
+
let memoryMonitorInterval = null;
|
|
771
|
+
|
|
772
|
+
// DOM Elements cache
|
|
773
|
+
let elements = {};
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Cache DOM elements
|
|
777
|
+
*/
|
|
778
|
+
function cacheElements() {
|
|
779
|
+
elements = {
|
|
780
|
+
swStatus: document.getElementById('swStatus'),
|
|
781
|
+
toggleDebugBtn: document.getElementById('toggleDebug'),
|
|
782
|
+
exportLogsBtn: document.getElementById('exportLogs'),
|
|
783
|
+
clearLogsBtn: document.getElementById('clearLogs'),
|
|
784
|
+
refreshStatusBtn: document.getElementById('refreshStatus'),
|
|
785
|
+
refreshCacheBtn: document.getElementById('refreshCache'),
|
|
786
|
+
enableDebugBtn: document.getElementById('enableDebugBtn'),
|
|
787
|
+
logsContainer: document.getElementById('logsContainer'),
|
|
788
|
+
consoleLogsContainer: document.getElementById('consoleLogsContainer'),
|
|
789
|
+
filterType: document.getElementById('filterType'),
|
|
790
|
+
filterStatus: document.getElementById('filterStatus'),
|
|
791
|
+
filterTimeRange: document.getElementById('filterTimeRange'),
|
|
792
|
+
filterUrl: document.getElementById('filterUrl'),
|
|
793
|
+
filterUrlRegex: document.getElementById('filterUrlRegex'),
|
|
794
|
+
togglePauseBtn: document.getElementById('togglePause'),
|
|
795
|
+
toggleSelectModeBtn: document.getElementById('toggleSelectMode'),
|
|
796
|
+
batchActionsEl: document.getElementById('batchActions'),
|
|
797
|
+
selectAllBtn: document.getElementById('selectAll'),
|
|
798
|
+
batchBookmarkBtn: document.getElementById('batchBookmark'),
|
|
799
|
+
batchDeleteBtn: document.getElementById('batchDelete'),
|
|
800
|
+
selectedCountEl: document.getElementById('selectedCount'),
|
|
801
|
+
fetchCountEl: document.getElementById('fetchCount'),
|
|
802
|
+
exportFetchCSVBtn: document.getElementById('exportFetchCSV'),
|
|
803
|
+
showShortcutsBtn: document.getElementById('showShortcuts'),
|
|
804
|
+
showBookmarksOnly: document.getElementById('showBookmarksOnly'),
|
|
805
|
+
shortcutsModalOverlay: document.getElementById('shortcutsModalOverlay'),
|
|
806
|
+
closeShortcutsModalBtn: document.getElementById('closeShortcutsModal'),
|
|
807
|
+
toggleThemeBtn: document.getElementById('toggleTheme'),
|
|
808
|
+
themeIcon: document.getElementById('themeIcon'),
|
|
809
|
+
showSettingsBtn: document.getElementById('showSettings'),
|
|
810
|
+
settingsModalOverlay: document.getElementById('settingsModalOverlay'),
|
|
811
|
+
closeSettingsModalBtn: document.getElementById('closeSettingsModal'),
|
|
812
|
+
saveSettingsBtn: document.getElementById('saveSettings'),
|
|
813
|
+
settingMaxLogs: document.getElementById('settingMaxLogs'),
|
|
814
|
+
settingAutoClean: document.getElementById('settingAutoClean'),
|
|
815
|
+
settingKeepBookmarks: document.getElementById('settingKeepBookmarks'),
|
|
816
|
+
// Stats elements
|
|
817
|
+
statTotalRequests: document.getElementById('statTotalRequests'),
|
|
818
|
+
statSuccessRate: document.getElementById('statSuccessRate'),
|
|
819
|
+
statAvgDuration: document.getElementById('statAvgDuration'),
|
|
820
|
+
statCacheHit: document.getElementById('statCacheHit'),
|
|
821
|
+
statSlowRequests: document.getElementById('statSlowRequests'),
|
|
822
|
+
statSlowRequestsWrapper: document.getElementById('statSlowRequestsWrapper'),
|
|
823
|
+
// Duration chart elements
|
|
824
|
+
chartFast: document.getElementById('chartFast'),
|
|
825
|
+
chartMedium: document.getElementById('chartMedium'),
|
|
826
|
+
chartSlow: document.getElementById('chartSlow'),
|
|
827
|
+
chartVerySlow: document.getElementById('chartVerySlow'),
|
|
828
|
+
filterConsoleLevel: document.getElementById('filterConsoleLevel'),
|
|
829
|
+
filterConsoleText: document.getElementById('filterConsoleText'),
|
|
830
|
+
clearConsoleLogsBtn: document.getElementById('clearConsoleLogs'),
|
|
831
|
+
copyConsoleLogsBtn: document.getElementById('copyConsoleLogs'),
|
|
832
|
+
autoScrollCheckbox: document.getElementById('autoScroll'),
|
|
833
|
+
consoleCountEl: document.getElementById('consoleCount'),
|
|
834
|
+
postmessageCountEl: document.getElementById('postmessageCount'),
|
|
835
|
+
postmessageLogsContainer: document.getElementById('postmessageLogsContainer'),
|
|
836
|
+
filterMessageDirection: document.getElementById('filterMessageDirection'),
|
|
837
|
+
filterMessageTypeSelect: document.getElementById('filterMessageTypeSelect'),
|
|
838
|
+
filterPmTimeRange: document.getElementById('filterPmTimeRange'),
|
|
839
|
+
filterMessageType: document.getElementById('filterMessageType'),
|
|
840
|
+
// Error dot indicators
|
|
841
|
+
consoleErrorDot: document.getElementById('consoleErrorDot'),
|
|
842
|
+
llmapiErrorDot: document.getElementById('llmapiErrorDot'),
|
|
843
|
+
crashErrorDot: document.getElementById('crashErrorDot'),
|
|
844
|
+
clearPostmessageLogsBtn: document.getElementById('clearPostmessageLogs'),
|
|
845
|
+
copyPostmessageLogsBtn: document.getElementById('copyPostmessageLogs'),
|
|
846
|
+
copyFetchLogsBtn: document.getElementById('copyFetchLogs'),
|
|
847
|
+
// Status panel elements
|
|
848
|
+
swVersion: document.getElementById('swVersion'),
|
|
849
|
+
debugMode: document.getElementById('debugMode'),
|
|
850
|
+
pendingImages: document.getElementById('pendingImages'),
|
|
851
|
+
pendingVideos: document.getElementById('pendingVideos'),
|
|
852
|
+
videoBlobCache: document.getElementById('videoBlobCache'),
|
|
853
|
+
completedRequests: document.getElementById('completedRequests'),
|
|
854
|
+
workflowHandler: document.getElementById('workflowHandler'),
|
|
855
|
+
debugLogsCount: document.getElementById('debugLogsCount'),
|
|
856
|
+
failedDomainsSection: document.getElementById('failedDomainsSection'),
|
|
857
|
+
failedDomains: document.getElementById('failedDomains'),
|
|
858
|
+
cacheList: document.getElementById('cacheList'),
|
|
859
|
+
// Export modal elements
|
|
860
|
+
exportModalOverlay: document.getElementById('exportModalOverlay'),
|
|
861
|
+
closeExportModalBtn: document.getElementById('closeExportModal'),
|
|
862
|
+
cancelExportBtn: document.getElementById('cancelExport'),
|
|
863
|
+
doExportBtn: document.getElementById('doExport'),
|
|
864
|
+
selectAllExport: document.getElementById('selectAllExport'),
|
|
865
|
+
// LLM API logs elements
|
|
866
|
+
llmapiLogsContainer: document.getElementById('llmapiLogsContainer'),
|
|
867
|
+
filterLLMApiType: document.getElementById('filterLLMApiType'),
|
|
868
|
+
filterLLMApiStatus: document.getElementById('filterLLMApiStatus'),
|
|
869
|
+
refreshLLMApiLogsBtn: document.getElementById('refreshLLMApiLogs'),
|
|
870
|
+
copyLLMApiLogsBtn: document.getElementById('copyLLMApiLogs'),
|
|
871
|
+
exportLLMApiLogsBtn: document.getElementById('exportLLMApiLogs'),
|
|
872
|
+
clearLLMApiLogsBtn: document.getElementById('clearLLMApiLogs'),
|
|
873
|
+
// Crash logs elements
|
|
874
|
+
crashCountEl: document.getElementById('crashCount'),
|
|
875
|
+
crashLogsContainer: document.getElementById('crashLogsContainer'),
|
|
876
|
+
filterCrashType: document.getElementById('filterCrashType'),
|
|
877
|
+
refreshCrashLogsBtn: document.getElementById('refreshCrashLogs'),
|
|
878
|
+
copyCrashLogsBtn: document.getElementById('copyCrashLogs'),
|
|
879
|
+
clearCrashLogsBtn: document.getElementById('clearCrashLogs'),
|
|
880
|
+
exportCrashLogsBtn: document.getElementById('exportCrashLogs'),
|
|
881
|
+
// Memory monitoring elements
|
|
882
|
+
memoryUsed: document.getElementById('memoryUsed'),
|
|
883
|
+
memoryTotal: document.getElementById('memoryTotal'),
|
|
884
|
+
memoryLimit: document.getElementById('memoryLimit'),
|
|
885
|
+
memoryPercent: document.getElementById('memoryPercent'),
|
|
886
|
+
memoryWarning: document.getElementById('memoryWarning'),
|
|
887
|
+
memoryNotSupported: document.getElementById('memoryNotSupported'),
|
|
888
|
+
memoryUpdateTime: document.getElementById('memoryUpdateTime'),
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Render fetch logs
|
|
894
|
+
*/
|
|
895
|
+
function renderLogs() {
|
|
896
|
+
const typeFilter = elements.filterType?.value || '';
|
|
897
|
+
const statusFilter = elements.filterStatus?.value || '';
|
|
898
|
+
const timeRangeFilter = elements.filterTimeRange?.value || '';
|
|
899
|
+
const urlFilter = (elements.filterUrl?.value || '').toLowerCase();
|
|
900
|
+
|
|
901
|
+
let filteredLogs = state.logs;
|
|
902
|
+
|
|
903
|
+
// Filter out blacklisted domains
|
|
904
|
+
filteredLogs = filteredLogs.filter(l => !isBlacklistedUrl(l.url));
|
|
905
|
+
|
|
906
|
+
if (typeFilter) {
|
|
907
|
+
filteredLogs = filteredLogs.filter(l => l.requestType === typeFilter);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (statusFilter) {
|
|
911
|
+
if (statusFilter === 'error') {
|
|
912
|
+
// Filter failed requests (4xx, 5xx, or has error)
|
|
913
|
+
filteredLogs = filteredLogs.filter(l => l.status >= 400 || l.error);
|
|
914
|
+
} else if (statusFilter === '500') {
|
|
915
|
+
filteredLogs = filteredLogs.filter(l => l.status >= 500);
|
|
916
|
+
} else if (statusFilter === 'slow') {
|
|
917
|
+
// Filter slow requests (> 1 second)
|
|
918
|
+
filteredLogs = filteredLogs.filter(l => l.duration >= 1000);
|
|
919
|
+
} else {
|
|
920
|
+
filteredLogs = filteredLogs.filter(l => l.status === parseInt(statusFilter));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Apply time range filter
|
|
925
|
+
filteredLogs = filterByTimeRange(filteredLogs, timeRangeFilter);
|
|
926
|
+
|
|
927
|
+
// URL filter with optional regex support
|
|
928
|
+
if (urlFilter) {
|
|
929
|
+
const useRegex = elements.filterUrlRegex?.checked || false;
|
|
930
|
+
if (useRegex) {
|
|
931
|
+
try {
|
|
932
|
+
const regex = new RegExp(urlFilter, 'i');
|
|
933
|
+
filteredLogs = filteredLogs.filter(l => regex.test(l.url || ''));
|
|
934
|
+
} catch (e) {
|
|
935
|
+
// Invalid regex, fall back to simple match
|
|
936
|
+
filteredLogs = filteredLogs.filter(l => l.url?.toLowerCase().includes(urlFilter));
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
filteredLogs = filteredLogs.filter(l => l.url?.toLowerCase().includes(urlFilter));
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Bookmarks filter
|
|
944
|
+
if (state.showBookmarksOnly) {
|
|
945
|
+
filteredLogs = filteredLogs.filter(l => state.bookmarkedLogIds.has(l.id));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Slow requests filter (via stats bar click)
|
|
949
|
+
if (state.filterSlowOnly) {
|
|
950
|
+
filteredLogs = filteredLogs.filter(l => l.duration >= 1000);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Update statistics panel
|
|
954
|
+
updateFetchStats();
|
|
955
|
+
|
|
956
|
+
// Update fetch count
|
|
957
|
+
if (elements.fetchCountEl) {
|
|
958
|
+
const slowCount = filteredLogs.filter(l => l.duration >= 1000).length;
|
|
959
|
+
const errorCount = filteredLogs.filter(l => l.status >= 400 || l.error).length;
|
|
960
|
+
let countText = `(${filteredLogs.length})`;
|
|
961
|
+
if (slowCount > 0 || errorCount > 0) {
|
|
962
|
+
const parts = [];
|
|
963
|
+
if (errorCount > 0) parts.push(`<span style="color:var(--error-color)">${errorCount} err</span>`);
|
|
964
|
+
if (slowCount > 0) parts.push(`<span style="color:var(--warning-color)">${slowCount} slow</span>`);
|
|
965
|
+
countText = `(${parts.join(', ')})`;
|
|
966
|
+
}
|
|
967
|
+
elements.fetchCountEl.innerHTML = countText;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (filteredLogs.length === 0) {
|
|
971
|
+
elements.logsContainer.innerHTML = `
|
|
972
|
+
<div class="empty-state">
|
|
973
|
+
<span class="icon">📋</span>
|
|
974
|
+
<p>${state.debugEnabled ? '暂无匹配的日志' : '请先启用调试模式'}</p>
|
|
975
|
+
${!state.debugEnabled ? '<button id="enableDebugBtn2" class="primary">启用调试</button>' : ''}
|
|
976
|
+
</div>
|
|
977
|
+
`;
|
|
978
|
+
const btn = document.getElementById('enableDebugBtn2');
|
|
979
|
+
if (btn) {
|
|
980
|
+
btn.addEventListener('click', toggleDebug);
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
elements.logsContainer.innerHTML = '';
|
|
986
|
+
filteredLogs.slice(0, 200).forEach(log => {
|
|
987
|
+
const isExpanded = state.expandedLogIds.has(log.id);
|
|
988
|
+
const isBookmarked = state.bookmarkedLogIds.has(log.id);
|
|
989
|
+
const isSelected = state.selectedLogIds.has(log.id);
|
|
990
|
+
const entry = createLogEntry(
|
|
991
|
+
log,
|
|
992
|
+
isExpanded,
|
|
993
|
+
(id, expanded) => {
|
|
994
|
+
// Update expanded state
|
|
995
|
+
if (expanded) {
|
|
996
|
+
state.expandedLogIds.add(id);
|
|
997
|
+
} else {
|
|
998
|
+
state.expandedLogIds.delete(id);
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
isBookmarked,
|
|
1002
|
+
toggleBookmark,
|
|
1003
|
+
state.isSelectMode,
|
|
1004
|
+
isSelected,
|
|
1005
|
+
toggleLogSelection
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
// Add slow request class for highlighting
|
|
1009
|
+
const speedClass = getSpeedClass(log.duration);
|
|
1010
|
+
if (speedClass !== 'normal') {
|
|
1011
|
+
entry.classList.add('slow-request');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
elements.logsContainer.appendChild(entry);
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Render console logs
|
|
1020
|
+
*/
|
|
1021
|
+
function renderConsoleLogs() {
|
|
1022
|
+
const levelFilter = elements.filterConsoleLevel?.value || '';
|
|
1023
|
+
const textFilter = (elements.filterConsoleText?.value || '').toLowerCase();
|
|
1024
|
+
|
|
1025
|
+
let filteredLogs = state.consoleLogs;
|
|
1026
|
+
|
|
1027
|
+
if (levelFilter) {
|
|
1028
|
+
filteredLogs = filteredLogs.filter(l => l.logLevel === levelFilter);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (textFilter) {
|
|
1032
|
+
filteredLogs = filteredLogs.filter(l =>
|
|
1033
|
+
l.logMessage?.toLowerCase().includes(textFilter) ||
|
|
1034
|
+
l.logStack?.toLowerCase().includes(textFilter)
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Update count
|
|
1039
|
+
updateConsoleCount();
|
|
1040
|
+
|
|
1041
|
+
if (filteredLogs.length === 0) {
|
|
1042
|
+
elements.consoleLogsContainer.innerHTML = `
|
|
1043
|
+
<div class="empty-state">
|
|
1044
|
+
<span class="icon">📝</span>
|
|
1045
|
+
<p>暂无控制台日志</p>
|
|
1046
|
+
</div>
|
|
1047
|
+
`;
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
elements.consoleLogsContainer.innerHTML = '';
|
|
1052
|
+
filteredLogs.slice(0, 200).forEach(log => {
|
|
1053
|
+
const isExpanded = state.expandedStackIds.has(log.id);
|
|
1054
|
+
const entry = createConsoleEntry(log, isExpanded, (id, expanded) => {
|
|
1055
|
+
if (expanded) {
|
|
1056
|
+
state.expandedStackIds.add(id);
|
|
1057
|
+
} else {
|
|
1058
|
+
state.expandedStackIds.delete(id);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
elements.consoleLogsContainer.appendChild(entry);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Update console log count indicator
|
|
1067
|
+
*/
|
|
1068
|
+
function updateConsoleCount() {
|
|
1069
|
+
const errorCount = state.consoleLogs.filter(l => l.logLevel === 'error').length;
|
|
1070
|
+
const warnCount = state.consoleLogs.filter(l => l.logLevel === 'warn').length;
|
|
1071
|
+
|
|
1072
|
+
if (errorCount > 0) {
|
|
1073
|
+
elements.consoleCountEl.innerHTML = `(<span style="color:var(--error-color)">${errorCount} errors</span>)`;
|
|
1074
|
+
} else if (warnCount > 0) {
|
|
1075
|
+
elements.consoleCountEl.innerHTML = `(<span style="color:var(--warning-color)">${warnCount} warns</span>)`;
|
|
1076
|
+
} else {
|
|
1077
|
+
elements.consoleCountEl.textContent = `(${state.consoleLogs.length})`;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* 判断日志是否为问题请求(错误、慢请求)
|
|
1083
|
+
*/
|
|
1084
|
+
function isProblemLog(log) {
|
|
1085
|
+
// 错误请求:状态码 >= 400 或有错误信息
|
|
1086
|
+
if (log.status >= 400 || log.error) return true;
|
|
1087
|
+
// 慢请求:耗时 >= 1秒
|
|
1088
|
+
if (log.duration >= 1000) return true;
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* 裁剪日志,优先保留问题请求和收藏
|
|
1094
|
+
*/
|
|
1095
|
+
function trimLogsWithPriority(maxLogs) {
|
|
1096
|
+
if (state.logs.length <= maxLogs) return;
|
|
1097
|
+
|
|
1098
|
+
// 分类日志
|
|
1099
|
+
const bookmarked = [];
|
|
1100
|
+
const problems = [];
|
|
1101
|
+
const normal = [];
|
|
1102
|
+
|
|
1103
|
+
state.logs.forEach(log => {
|
|
1104
|
+
if (state.bookmarkedLogIds.has(log.id)) {
|
|
1105
|
+
bookmarked.push(log);
|
|
1106
|
+
} else if (isProblemLog(log)) {
|
|
1107
|
+
problems.push(log);
|
|
1108
|
+
} else {
|
|
1109
|
+
normal.push(log);
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
// 计算需要保留的数量
|
|
1114
|
+
const mustKeep = bookmarked.length + problems.length;
|
|
1115
|
+
|
|
1116
|
+
if (mustKeep >= maxLogs) {
|
|
1117
|
+
// 问题请求太多,只保留收藏 + 部分问题请求
|
|
1118
|
+
const problemsToKeep = Math.max(0, maxLogs - bookmarked.length);
|
|
1119
|
+
state.logs = [...bookmarked, ...problems.slice(0, problemsToKeep)];
|
|
1120
|
+
} else {
|
|
1121
|
+
// 保留所有收藏和问题请求,剩余空间给正常请求
|
|
1122
|
+
const normalToKeep = maxLogs - mustKeep;
|
|
1123
|
+
state.logs = [...bookmarked, ...problems, ...normal.slice(0, normalToKeep)];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// 按时间排序(最新的在前)
|
|
1127
|
+
state.logs.sort((a, b) => b.timestamp - a.timestamp);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Add or update a log entry
|
|
1132
|
+
* @param {object} entry
|
|
1133
|
+
* @param {boolean} skipRender - Skip rendering (for batch updates)
|
|
1134
|
+
*/
|
|
1135
|
+
function addOrUpdateLog(entry, skipRender = false) {
|
|
1136
|
+
// Skip blacklisted URLs
|
|
1137
|
+
if (isBlacklistedUrl(entry.url)) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// If paused, add to pending queue
|
|
1142
|
+
if (state.isPaused && !skipRender) {
|
|
1143
|
+
state.pendingLogs.push(entry);
|
|
1144
|
+
updatePauseButton();
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const existingIndex = state.logs.findIndex(l => l.id === entry.id);
|
|
1149
|
+
if (existingIndex !== -1) {
|
|
1150
|
+
state.logs[existingIndex] = { ...state.logs[existingIndex], ...entry };
|
|
1151
|
+
} else {
|
|
1152
|
+
state.logs.unshift(entry);
|
|
1153
|
+
// Use configurable max logs limit
|
|
1154
|
+
const maxLogs = state.settings?.maxLogs || 500;
|
|
1155
|
+
if (state.logs.length > maxLogs) {
|
|
1156
|
+
// 优先保留问题请求(错误、慢请求、收藏)
|
|
1157
|
+
trimLogsWithPriority(maxLogs);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (!skipRender) {
|
|
1162
|
+
renderLogs();
|
|
1163
|
+
|
|
1164
|
+
if (state.autoScroll) {
|
|
1165
|
+
elements.logsContainer.scrollTop = 0;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Add a console log entry (real-time)
|
|
1172
|
+
* @param {object} entry
|
|
1173
|
+
*/
|
|
1174
|
+
function addConsoleLog(entry) {
|
|
1175
|
+
// Check for duplicates (in case of race condition with initial load)
|
|
1176
|
+
if (state.consoleLogs.some(l => l.id === entry.id)) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
state.consoleLogs.unshift(entry);
|
|
1181
|
+
if (state.consoleLogs.length > 500) {
|
|
1182
|
+
state.consoleLogs.pop();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (state.activeTab === 'console') {
|
|
1186
|
+
renderConsoleLogs();
|
|
1187
|
+
} else {
|
|
1188
|
+
updateConsoleCount();
|
|
1189
|
+
// Update error dot if new error
|
|
1190
|
+
if (entry.logLevel === 'error') {
|
|
1191
|
+
updateErrorDots();
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Update message type select options based on current logs
|
|
1198
|
+
*/
|
|
1199
|
+
function updateMessageTypeOptions() {
|
|
1200
|
+
const types = extractUniqueTypes(state.postmessageLogs, 'messageType');
|
|
1201
|
+
if (elements.filterMessageTypeSelect) {
|
|
1202
|
+
updateTypeSelectOptions(elements.filterMessageTypeSelect, types);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Render postmessage logs
|
|
1208
|
+
*/
|
|
1209
|
+
function renderPostmessageLogs() {
|
|
1210
|
+
const directionFilter = elements.filterMessageDirection?.value || '';
|
|
1211
|
+
const typeSelectFilter = elements.filterMessageTypeSelect?.value || '';
|
|
1212
|
+
const timeRangeFilter = elements.filterPmTimeRange?.value || '';
|
|
1213
|
+
const typeFilter = (elements.filterMessageType?.value || '').toLowerCase();
|
|
1214
|
+
|
|
1215
|
+
let filteredLogs = state.postmessageLogs;
|
|
1216
|
+
|
|
1217
|
+
if (directionFilter) {
|
|
1218
|
+
filteredLogs = filteredLogs.filter(l => l.direction === directionFilter);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// 下拉选择器过滤(精确匹配)
|
|
1222
|
+
if (typeSelectFilter) {
|
|
1223
|
+
filteredLogs = filteredLogs.filter(l => l.messageType === typeSelectFilter);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// 时间范围过滤
|
|
1227
|
+
filteredLogs = filterByTimeRange(filteredLogs, timeRangeFilter);
|
|
1228
|
+
|
|
1229
|
+
// 搜索框过滤(模糊匹配)
|
|
1230
|
+
if (typeFilter) {
|
|
1231
|
+
filteredLogs = filteredLogs.filter(l =>
|
|
1232
|
+
l.messageType?.toLowerCase().includes(typeFilter)
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Update count
|
|
1237
|
+
updatePostmessageCount();
|
|
1238
|
+
|
|
1239
|
+
if (filteredLogs.length === 0) {
|
|
1240
|
+
elements.postmessageLogsContainer.innerHTML = `
|
|
1241
|
+
<div class="empty-state">
|
|
1242
|
+
<span class="icon">📨</span>
|
|
1243
|
+
<p>暂无 PostMessage 日志</p>
|
|
1244
|
+
<p style="font-size: 12px; opacity: 0.7;">记录主线程与 Service Worker 之间的消息通信</p>
|
|
1245
|
+
</div>
|
|
1246
|
+
`;
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
elements.postmessageLogsContainer.innerHTML = '';
|
|
1251
|
+
filteredLogs.slice(0, 200).forEach(log => {
|
|
1252
|
+
const isExpanded = state.expandedPmIds.has(log.id);
|
|
1253
|
+
const entry = createPostMessageEntry(log, isExpanded, (id, expanded) => {
|
|
1254
|
+
if (expanded) {
|
|
1255
|
+
state.expandedPmIds.add(id);
|
|
1256
|
+
} else {
|
|
1257
|
+
state.expandedPmIds.delete(id);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
elements.postmessageLogsContainer.appendChild(entry);
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Update postmessage log count indicator
|
|
1266
|
+
*/
|
|
1267
|
+
function updatePostmessageCount() {
|
|
1268
|
+
const sendCount = state.postmessageLogs.filter(l => l.direction === 'send').length;
|
|
1269
|
+
const receiveCount = state.postmessageLogs.filter(l => l.direction === 'receive').length;
|
|
1270
|
+
|
|
1271
|
+
if (state.postmessageLogs.length > 0) {
|
|
1272
|
+
elements.postmessageCountEl.innerHTML = `(<span style="color:var(--primary-color)">${sendCount}→</span> <span style="color:var(--success-color)">←${receiveCount}</span>)`;
|
|
1273
|
+
} else {
|
|
1274
|
+
elements.postmessageCountEl.textContent = '(0)';
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Add a postmessage log entry
|
|
1280
|
+
* @param {object} entry
|
|
1281
|
+
*/
|
|
1282
|
+
function addPostmessageLog(entry) {
|
|
1283
|
+
// Check for duplicates
|
|
1284
|
+
if (state.postmessageLogs.some(l => l.id === entry.id)) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
state.postmessageLogs.unshift(entry);
|
|
1289
|
+
if (state.postmessageLogs.length > 500) {
|
|
1290
|
+
state.postmessageLogs.pop();
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// 更新消息类型下拉选项
|
|
1294
|
+
updateMessageTypeOptions();
|
|
1295
|
+
|
|
1296
|
+
if (state.activeTab === 'postmessage') {
|
|
1297
|
+
renderPostmessageLogs();
|
|
1298
|
+
} else {
|
|
1299
|
+
updatePostmessageCount();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Clear postmessage logs
|
|
1305
|
+
*/
|
|
1306
|
+
function handleClearPostmessageLogs() {
|
|
1307
|
+
state.postmessageLogs = [];
|
|
1308
|
+
clearPostMessageLogsInSW(); // Also clear in SW
|
|
1309
|
+
renderPostmessageLogs();
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Get filtered postmessage logs based on current filters
|
|
1314
|
+
*/
|
|
1315
|
+
function getFilteredPostmessageLogs() {
|
|
1316
|
+
const directionFilter = elements.filterMessageDirection?.value || '';
|
|
1317
|
+
const typeSelectFilter = elements.filterMessageTypeSelect?.value || '';
|
|
1318
|
+
const typeFilter = (elements.filterMessageType?.value || '').toLowerCase();
|
|
1319
|
+
|
|
1320
|
+
let filteredLogs = state.postmessageLogs;
|
|
1321
|
+
|
|
1322
|
+
if (directionFilter) {
|
|
1323
|
+
filteredLogs = filteredLogs.filter(l => l.direction === directionFilter);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (typeSelectFilter) {
|
|
1327
|
+
filteredLogs = filteredLogs.filter(l => l.messageType === typeSelectFilter);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (typeFilter) {
|
|
1331
|
+
filteredLogs = filteredLogs.filter(l =>
|
|
1332
|
+
l.messageType?.toLowerCase().includes(typeFilter)
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return filteredLogs;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Copy filtered postmessage logs to clipboard
|
|
1341
|
+
*/
|
|
1342
|
+
async function handleCopyPostmessageLogs() {
|
|
1343
|
+
const filteredLogs = getFilteredPostmessageLogs();
|
|
1344
|
+
|
|
1345
|
+
if (filteredLogs.length === 0) {
|
|
1346
|
+
alert('没有可复制的日志');
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Format logs as text
|
|
1351
|
+
const logText = filteredLogs.map(log => {
|
|
1352
|
+
const time = new Date(log.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
|
|
1353
|
+
const direction = log.direction === 'send' ? '→ SW' : '← 主线程';
|
|
1354
|
+
const type = log.messageType || 'unknown';
|
|
1355
|
+
const data = log.data ? JSON.stringify(log.data, null, 2) : '';
|
|
1356
|
+
const response = log.response !== undefined ? `\n 响应: ${JSON.stringify(log.response, null, 2)}` : '';
|
|
1357
|
+
const error = log.error ? `\n 错误: ${log.error}` : '';
|
|
1358
|
+
return `${time} [${direction}] ${type}\n 数据: ${data}${response}${error}`;
|
|
1359
|
+
}).join('\n\n');
|
|
1360
|
+
|
|
1361
|
+
try {
|
|
1362
|
+
await navigator.clipboard.writeText(logText);
|
|
1363
|
+
// Visual feedback
|
|
1364
|
+
const btn = elements.copyPostmessageLogsBtn;
|
|
1365
|
+
const originalText = btn.textContent;
|
|
1366
|
+
btn.textContent = '✅ 已复制';
|
|
1367
|
+
setTimeout(() => {
|
|
1368
|
+
btn.textContent = originalText;
|
|
1369
|
+
}, 1500);
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
console.error('Failed to copy:', err);
|
|
1372
|
+
alert('复制失败');
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Get filtered fetch logs based on current filters
|
|
1378
|
+
*/
|
|
1379
|
+
function getFilteredFetchLogs() {
|
|
1380
|
+
const typeFilter = elements.filterType?.value || '';
|
|
1381
|
+
const statusFilter = elements.filterStatus?.value || '';
|
|
1382
|
+
const urlFilter = (elements.filterUrl?.value || '').toLowerCase();
|
|
1383
|
+
|
|
1384
|
+
let filteredLogs = state.logs.filter(l => !isBlacklistedUrl(l.url));
|
|
1385
|
+
|
|
1386
|
+
if (typeFilter) {
|
|
1387
|
+
filteredLogs = filteredLogs.filter(l => l.requestType === typeFilter);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (statusFilter) {
|
|
1391
|
+
if (statusFilter === '500') {
|
|
1392
|
+
filteredLogs = filteredLogs.filter(l => l.status >= 500);
|
|
1393
|
+
} else {
|
|
1394
|
+
filteredLogs = filteredLogs.filter(l => l.status === parseInt(statusFilter));
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (urlFilter) {
|
|
1399
|
+
filteredLogs = filteredLogs.filter(l => l.url?.toLowerCase().includes(urlFilter));
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
return filteredLogs;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Copy filtered fetch logs to clipboard
|
|
1407
|
+
*/
|
|
1408
|
+
async function handleCopyFetchLogs() {
|
|
1409
|
+
const filteredLogs = getFilteredFetchLogs();
|
|
1410
|
+
|
|
1411
|
+
if (filteredLogs.length === 0) {
|
|
1412
|
+
alert('没有可复制的日志');
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Format logs as text
|
|
1417
|
+
const logText = filteredLogs.map(log => {
|
|
1418
|
+
const time = new Date(log.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
|
|
1419
|
+
const method = log.method || 'GET';
|
|
1420
|
+
const status = log.status || '-';
|
|
1421
|
+
const duration = log.duration ? `${log.duration}ms` : '-';
|
|
1422
|
+
const cached = log.cached ? ' [缓存]' : '';
|
|
1423
|
+
const url = log.url || '-';
|
|
1424
|
+
return `${time} ${method} ${status} ${url} (${duration})${cached}`;
|
|
1425
|
+
}).join('\n');
|
|
1426
|
+
|
|
1427
|
+
try {
|
|
1428
|
+
await navigator.clipboard.writeText(logText);
|
|
1429
|
+
const btn = elements.copyFetchLogsBtn;
|
|
1430
|
+
const originalText = btn.textContent;
|
|
1431
|
+
btn.textContent = '✅ 已复制';
|
|
1432
|
+
setTimeout(() => {
|
|
1433
|
+
btn.textContent = originalText;
|
|
1434
|
+
}, 1500);
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
console.error('Failed to copy:', err);
|
|
1437
|
+
alert('复制失败');
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Get filtered LLM API logs based on current filters
|
|
1443
|
+
*/
|
|
1444
|
+
function getFilteredLLMApiLogs() {
|
|
1445
|
+
const typeFilter = elements.filterLLMApiType?.value || '';
|
|
1446
|
+
const statusFilter = elements.filterLLMApiStatus?.value || '';
|
|
1447
|
+
|
|
1448
|
+
let filteredLogs = state.llmapiLogs;
|
|
1449
|
+
|
|
1450
|
+
if (typeFilter) {
|
|
1451
|
+
filteredLogs = filteredLogs.filter(l => l.taskType === typeFilter);
|
|
1452
|
+
}
|
|
1453
|
+
if (statusFilter) {
|
|
1454
|
+
filteredLogs = filteredLogs.filter(l => l.status === statusFilter);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
return filteredLogs;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Copy filtered LLM API logs to clipboard
|
|
1462
|
+
*/
|
|
1463
|
+
async function handleCopyLLMApiLogs() {
|
|
1464
|
+
const filteredLogs = getFilteredLLMApiLogs();
|
|
1465
|
+
|
|
1466
|
+
if (filteredLogs.length === 0) {
|
|
1467
|
+
alert('没有可复制的日志');
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Format logs as text
|
|
1472
|
+
const logText = filteredLogs.map(log => {
|
|
1473
|
+
const time = new Date(log.timestamp).toLocaleString('zh-CN', { hour12: false });
|
|
1474
|
+
const type = log.taskType || 'unknown';
|
|
1475
|
+
const status = log.status || '-';
|
|
1476
|
+
const model = log.model || '-';
|
|
1477
|
+
const duration = log.duration ? `${(log.duration / 1000).toFixed(1)}s` : '-';
|
|
1478
|
+
const prompt = log.prompt ? `\n 提示词: ${log.prompt}` : '';
|
|
1479
|
+
const error = log.errorMessage ? `\n 错误: ${log.errorMessage}` : '';
|
|
1480
|
+
return `${time} [${type}] ${status} | ${model} (${duration})${prompt}${error}`;
|
|
1481
|
+
}).join('\n\n');
|
|
1482
|
+
|
|
1483
|
+
try {
|
|
1484
|
+
await navigator.clipboard.writeText(logText);
|
|
1485
|
+
const btn = elements.copyLLMApiLogsBtn;
|
|
1486
|
+
const originalText = btn.textContent;
|
|
1487
|
+
btn.textContent = '✅ 已复制';
|
|
1488
|
+
setTimeout(() => {
|
|
1489
|
+
btn.textContent = originalText;
|
|
1490
|
+
}, 1500);
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
console.error('Failed to copy:', err);
|
|
1493
|
+
alert('复制失败');
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* Get filtered crash logs based on current filters
|
|
1499
|
+
*/
|
|
1500
|
+
function getFilteredCrashLogs() {
|
|
1501
|
+
const typeFilter = elements.filterCrashType?.value || '';
|
|
1502
|
+
|
|
1503
|
+
let filteredLogs = state.crashLogs;
|
|
1504
|
+
|
|
1505
|
+
if (typeFilter) {
|
|
1506
|
+
filteredLogs = filteredLogs.filter(l => l.type === typeFilter);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return filteredLogs;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* Copy filtered crash logs to clipboard
|
|
1514
|
+
*/
|
|
1515
|
+
async function handleCopyCrashLogs() {
|
|
1516
|
+
const filteredLogs = getFilteredCrashLogs();
|
|
1517
|
+
|
|
1518
|
+
if (filteredLogs.length === 0) {
|
|
1519
|
+
alert('没有可复制的日志');
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Format logs as text
|
|
1524
|
+
const logText = filteredLogs.map(log => {
|
|
1525
|
+
const time = new Date(log.timestamp).toLocaleString('zh-CN', { hour12: false });
|
|
1526
|
+
const type = log.type || 'unknown';
|
|
1527
|
+
let memoryInfo = '';
|
|
1528
|
+
if (log.memory) {
|
|
1529
|
+
const usedMB = (log.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1);
|
|
1530
|
+
const limitMB = (log.memory.jsHeapSizeLimit / (1024 * 1024)).toFixed(1);
|
|
1531
|
+
memoryInfo = ` | 内存: ${usedMB}/${limitMB} MB`;
|
|
1532
|
+
}
|
|
1533
|
+
const error = log.error ? `\n 错误: ${log.error.message}` : '';
|
|
1534
|
+
const stack = log.error?.stack ? `\n Stack: ${log.error.stack}` : '';
|
|
1535
|
+
return `${time} [${type}]${memoryInfo}${error}${stack}`;
|
|
1536
|
+
}).join('\n\n');
|
|
1537
|
+
|
|
1538
|
+
try {
|
|
1539
|
+
await navigator.clipboard.writeText(logText);
|
|
1540
|
+
const btn = elements.copyCrashLogsBtn;
|
|
1541
|
+
const originalText = btn.textContent;
|
|
1542
|
+
btn.textContent = '✅ 已复制';
|
|
1543
|
+
setTimeout(() => {
|
|
1544
|
+
btn.textContent = originalText;
|
|
1545
|
+
}, 1500);
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
console.error('Failed to copy:', err);
|
|
1548
|
+
alert('复制失败');
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// ==================== LLM API Logs ====================
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Render LLM API logs
|
|
1556
|
+
*/
|
|
1557
|
+
function renderLLMApiLogs() {
|
|
1558
|
+
const typeFilter = elements.filterLLMApiType?.value || '';
|
|
1559
|
+
const statusFilter = elements.filterLLMApiStatus?.value || '';
|
|
1560
|
+
|
|
1561
|
+
let filteredLogs = state.llmapiLogs;
|
|
1562
|
+
|
|
1563
|
+
if (typeFilter) {
|
|
1564
|
+
filteredLogs = filteredLogs.filter(l => l.taskType === typeFilter);
|
|
1565
|
+
}
|
|
1566
|
+
if (statusFilter) {
|
|
1567
|
+
filteredLogs = filteredLogs.filter(l => l.status === statusFilter);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (!elements.llmapiLogsContainer) return;
|
|
1571
|
+
|
|
1572
|
+
if (filteredLogs.length === 0) {
|
|
1573
|
+
elements.llmapiLogsContainer.innerHTML = `
|
|
1574
|
+
<div class="empty-state">
|
|
1575
|
+
<span class="icon">🤖</span>
|
|
1576
|
+
<p>暂无 LLM API 调用记录</p>
|
|
1577
|
+
<p style="font-size: 12px; opacity: 0.7;">图片/视频/对话等 AI 接口调用会自动记录</p>
|
|
1578
|
+
</div>
|
|
1579
|
+
`;
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
elements.llmapiLogsContainer.innerHTML = '';
|
|
1584
|
+
filteredLogs.forEach(log => {
|
|
1585
|
+
const isExpanded = state.expandedLLMApiIds.has(log.id);
|
|
1586
|
+
const entry = createLLMApiEntry(log, isExpanded, (id, expanded) => {
|
|
1587
|
+
if (expanded) {
|
|
1588
|
+
state.expandedLLMApiIds.add(id);
|
|
1589
|
+
} else {
|
|
1590
|
+
state.expandedLLMApiIds.delete(id);
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
elements.llmapiLogsContainer.appendChild(entry);
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Create a LLM API log entry element
|
|
1599
|
+
* Uses the same styles as Fetch logs for consistency
|
|
1600
|
+
*/
|
|
1601
|
+
function createLLMApiEntry(log, isExpanded, onToggle) {
|
|
1602
|
+
const entry = document.createElement('div');
|
|
1603
|
+
entry.className = 'log-entry' + (isExpanded ? ' expanded' : '');
|
|
1604
|
+
entry.dataset.id = log.id;
|
|
1605
|
+
|
|
1606
|
+
const time = new Date(log.timestamp).toLocaleTimeString('zh-CN', {
|
|
1607
|
+
hour12: false,
|
|
1608
|
+
hour: '2-digit',
|
|
1609
|
+
minute: '2-digit',
|
|
1610
|
+
second: '2-digit',
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
// Status badge - use log-status class like Fetch logs
|
|
1614
|
+
let statusClass = '';
|
|
1615
|
+
let statusText = '';
|
|
1616
|
+
switch (log.status) {
|
|
1617
|
+
case 'success':
|
|
1618
|
+
statusClass = 'success';
|
|
1619
|
+
statusText = '✓ 成功';
|
|
1620
|
+
break;
|
|
1621
|
+
case 'error':
|
|
1622
|
+
statusClass = 'error';
|
|
1623
|
+
statusText = '✗ 失败';
|
|
1624
|
+
break;
|
|
1625
|
+
case 'pending':
|
|
1626
|
+
statusClass = 'pending';
|
|
1627
|
+
statusText = '⋯ 进行中';
|
|
1628
|
+
break;
|
|
1629
|
+
default:
|
|
1630
|
+
statusText = log.status;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// Task type badge - use log-type-badge class like Fetch logs
|
|
1634
|
+
const typeLabel = {
|
|
1635
|
+
'image': '图片生成',
|
|
1636
|
+
'video': '视频生成',
|
|
1637
|
+
'chat': '对话',
|
|
1638
|
+
'character': '角色',
|
|
1639
|
+
'other': '其他',
|
|
1640
|
+
}[log.taskType] || log.taskType;
|
|
1641
|
+
|
|
1642
|
+
// Duration format - use log-duration class like Fetch logs
|
|
1643
|
+
const durationMs = log.duration || 0;
|
|
1644
|
+
const durationText = log.duration ? `${(log.duration / 1000).toFixed(1)}s` : '-';
|
|
1645
|
+
const durationClass = durationMs >= 3000 ? 'very-slow' : (durationMs >= 1000 ? 'slow' : '');
|
|
1646
|
+
|
|
1647
|
+
// Truncate prompt for header display
|
|
1648
|
+
const promptPreview = log.prompt
|
|
1649
|
+
? (log.prompt.length > 60 ? log.prompt.substring(0, 60) + '...' : log.prompt)
|
|
1650
|
+
: '-';
|
|
1651
|
+
|
|
1652
|
+
entry.innerHTML = `
|
|
1653
|
+
<div class="log-header">
|
|
1654
|
+
<span class="log-toggle" title="展开/收起详情"><span class="arrow">▶</span></span>
|
|
1655
|
+
<span class="log-time">${time}</span>
|
|
1656
|
+
<span class="log-status ${statusClass}">${statusText}</span>
|
|
1657
|
+
<span class="log-type-badge">${typeLabel}</span>
|
|
1658
|
+
<span class="log-type-badge sw-internal">${log.model}</span>
|
|
1659
|
+
<span class="log-url" title="${escapeHtml(log.prompt || '')}">${escapeHtml(promptPreview)}</span>
|
|
1660
|
+
<span class="log-duration ${durationClass}">${durationText}</span>
|
|
1661
|
+
</div>
|
|
1662
|
+
<div class="log-details">
|
|
1663
|
+
<div class="detail-section">
|
|
1664
|
+
<h4>基本信息</h4>
|
|
1665
|
+
<table class="form-data-table">
|
|
1666
|
+
<tbody>
|
|
1667
|
+
<tr>
|
|
1668
|
+
<td class="form-data-name">ID</td>
|
|
1669
|
+
<td><span class="form-data-value" style="font-family: monospace; font-size: 11px;">${log.id}</span></td>
|
|
1670
|
+
</tr>
|
|
1671
|
+
<tr>
|
|
1672
|
+
<td class="form-data-name">Endpoint</td>
|
|
1673
|
+
<td><span class="form-data-value" style="font-family: monospace; font-size: 11px;">${log.endpoint}</span></td>
|
|
1674
|
+
</tr>
|
|
1675
|
+
<tr>
|
|
1676
|
+
<td class="form-data-name">模型</td>
|
|
1677
|
+
<td><span class="form-data-value">${log.model}</span></td>
|
|
1678
|
+
</tr>
|
|
1679
|
+
<tr>
|
|
1680
|
+
<td class="form-data-name">类型</td>
|
|
1681
|
+
<td><span class="form-data-value">${log.taskType}</span></td>
|
|
1682
|
+
</tr>
|
|
1683
|
+
<tr>
|
|
1684
|
+
<td class="form-data-name">HTTP 状态</td>
|
|
1685
|
+
<td><span class="form-data-value">${log.httpStatus || '-'}</span></td>
|
|
1686
|
+
</tr>
|
|
1687
|
+
<tr>
|
|
1688
|
+
<td class="form-data-name">耗时</td>
|
|
1689
|
+
<td><span class="form-data-value">${durationText}</span></td>
|
|
1690
|
+
</tr>
|
|
1691
|
+
${log.hasReferenceImages ? `
|
|
1692
|
+
<tr>
|
|
1693
|
+
<td class="form-data-name">参考图</td>
|
|
1694
|
+
<td><span class="form-data-value">${log.referenceImageCount || 0} 张</span></td>
|
|
1695
|
+
</tr>
|
|
1696
|
+
` : ''}
|
|
1697
|
+
${log.resultType ? `
|
|
1698
|
+
<tr>
|
|
1699
|
+
<td class="form-data-name">结果类型</td>
|
|
1700
|
+
<td><span class="form-data-value">${log.resultType}</span></td>
|
|
1701
|
+
</tr>
|
|
1702
|
+
` : ''}
|
|
1703
|
+
${log.taskId ? `
|
|
1704
|
+
<tr>
|
|
1705
|
+
<td class="form-data-name">任务 ID</td>
|
|
1706
|
+
<td><span class="form-data-value" style="font-family: monospace; font-size: 11px;">${log.taskId}</span></td>
|
|
1707
|
+
</tr>
|
|
1708
|
+
` : ''}
|
|
1709
|
+
${log.resultUrl ? `
|
|
1710
|
+
<tr>
|
|
1711
|
+
<td class="form-data-name">结果 URL</td>
|
|
1712
|
+
<td>
|
|
1713
|
+
<span class="form-data-value" style="display: flex; align-items: center; gap: 8px;">
|
|
1714
|
+
<a href="${log.resultUrl}" target="_blank" class="llm-result-url" style="font-family: monospace; font-size: 11px; word-break: break-all; color: var(--primary-color); cursor: pointer;">${log.resultUrl.length > 80 ? log.resultUrl.substring(0, 80) + '...' : log.resultUrl}</a>
|
|
1715
|
+
<button class="copy-url-btn" data-url="${escapeHtml(log.resultUrl)}" title="复制 URL" style="padding: 2px 6px; font-size: 10px; cursor: pointer; flex-shrink: 0;">
|
|
1716
|
+
<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 12px; height: 12px;"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
1717
|
+
</button>
|
|
1718
|
+
</span>
|
|
1719
|
+
</td>
|
|
1720
|
+
</tr>
|
|
1721
|
+
` : ''}
|
|
1722
|
+
${log.errorMessage ? `
|
|
1723
|
+
<tr>
|
|
1724
|
+
<td class="form-data-name">错误信息</td>
|
|
1725
|
+
<td><span class="form-data-value" style="color: var(--error-color); word-break: break-word;">${escapeHtml(log.errorMessage)}</span></td>
|
|
1726
|
+
</tr>
|
|
1727
|
+
` : ''}
|
|
1728
|
+
</tbody>
|
|
1729
|
+
</table>
|
|
1730
|
+
</div>
|
|
1731
|
+
${log.prompt ? `
|
|
1732
|
+
<div class="detail-section">
|
|
1733
|
+
<h4>提示词</h4>
|
|
1734
|
+
<pre>${escapeHtml(log.prompt)}</pre>
|
|
1735
|
+
</div>
|
|
1736
|
+
` : ''}
|
|
1737
|
+
${log.requestBody ? `
|
|
1738
|
+
<div class="detail-section">
|
|
1739
|
+
<h4>请求体 (Request Body)</h4>
|
|
1740
|
+
<pre>${escapeHtml(log.requestBody)}</pre>
|
|
1741
|
+
</div>
|
|
1742
|
+
` : ''}
|
|
1743
|
+
${log.resultText ? `
|
|
1744
|
+
<div class="detail-section">
|
|
1745
|
+
<h4>响应文本</h4>
|
|
1746
|
+
<pre>${escapeHtml(log.resultText)}</pre>
|
|
1747
|
+
</div>
|
|
1748
|
+
` : ''}
|
|
1749
|
+
${log.responseBody ? `
|
|
1750
|
+
<div class="detail-section">
|
|
1751
|
+
<h4>响应体 (Response Body)</h4>
|
|
1752
|
+
<pre>${escapeHtml(log.responseBody)}</pre>
|
|
1753
|
+
</div>
|
|
1754
|
+
` : ''}
|
|
1755
|
+
</div>
|
|
1756
|
+
`;
|
|
1757
|
+
|
|
1758
|
+
// Toggle function - same as Fetch logs
|
|
1759
|
+
const toggleExpand = () => {
|
|
1760
|
+
const isNowExpanded = entry.classList.toggle('expanded');
|
|
1761
|
+
if (onToggle) {
|
|
1762
|
+
onToggle(log.id, isNowExpanded);
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
// Toggle expand/collapse on button click
|
|
1767
|
+
const toggleBtn = entry.querySelector('.log-toggle');
|
|
1768
|
+
toggleBtn.addEventListener('click', (e) => {
|
|
1769
|
+
e.stopPropagation();
|
|
1770
|
+
toggleExpand();
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
// Toggle on header click (except toggle button)
|
|
1774
|
+
const header = entry.querySelector('.log-header');
|
|
1775
|
+
header.addEventListener('click', (e) => {
|
|
1776
|
+
if (e.target.closest('.log-toggle')) return;
|
|
1777
|
+
toggleExpand();
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
// Copy URL button click handler
|
|
1781
|
+
const copyUrlBtn = entry.querySelector('.copy-url-btn');
|
|
1782
|
+
if (copyUrlBtn) {
|
|
1783
|
+
copyUrlBtn.addEventListener('click', async (e) => {
|
|
1784
|
+
e.stopPropagation();
|
|
1785
|
+
const url = copyUrlBtn.dataset.url;
|
|
1786
|
+
try {
|
|
1787
|
+
await navigator.clipboard.writeText(url);
|
|
1788
|
+
// Show feedback
|
|
1789
|
+
const originalHtml = copyUrlBtn.innerHTML;
|
|
1790
|
+
copyUrlBtn.innerHTML = '✓';
|
|
1791
|
+
copyUrlBtn.style.color = 'var(--success-color)';
|
|
1792
|
+
setTimeout(() => {
|
|
1793
|
+
copyUrlBtn.innerHTML = originalHtml;
|
|
1794
|
+
copyUrlBtn.style.color = '';
|
|
1795
|
+
}, 1500);
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
console.error('Failed to copy URL:', err);
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
return entry;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
|
|
1806
|
+
/**
|
|
1807
|
+
* Load LLM API logs from SW
|
|
1808
|
+
*/
|
|
1809
|
+
function loadLLMApiLogs() {
|
|
1810
|
+
if (navigator.serviceWorker?.controller) {
|
|
1811
|
+
navigator.serviceWorker.controller.postMessage({
|
|
1812
|
+
type: 'SW_DEBUG_GET_LLM_API_LOGS'
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Clear LLM API logs
|
|
1819
|
+
*/
|
|
1820
|
+
function handleClearLLMApiLogs() {
|
|
1821
|
+
if (!confirm('确定要清空所有 LLM API 日志吗?')) return;
|
|
1822
|
+
|
|
1823
|
+
if (navigator.serviceWorker?.controller) {
|
|
1824
|
+
navigator.serviceWorker.controller.postMessage({
|
|
1825
|
+
type: 'SW_DEBUG_CLEAR_LLM_API_LOGS'
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
state.llmapiLogs = [];
|
|
1829
|
+
renderLLMApiLogs();
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* Export LLM API logs with media files (images/videos)
|
|
1834
|
+
* Creates a ZIP file containing:
|
|
1835
|
+
* - llm-api-logs.json: All LLM API logs
|
|
1836
|
+
* - media/: Directory containing cached images and videos
|
|
1837
|
+
*/
|
|
1838
|
+
async function handleExportLLMApiLogs() {
|
|
1839
|
+
if (state.llmapiLogs.length === 0) {
|
|
1840
|
+
alert('暂无 LLM API 日志可导出');
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
const exportBtn = elements.exportLLMApiLogsBtn;
|
|
1845
|
+
const originalText = exportBtn.textContent;
|
|
1846
|
+
|
|
1847
|
+
try {
|
|
1848
|
+
exportBtn.disabled = true;
|
|
1849
|
+
exportBtn.textContent = '⏳ 准备中...';
|
|
1850
|
+
|
|
1851
|
+
// Check if JSZip is available
|
|
1852
|
+
if (typeof JSZip === 'undefined') {
|
|
1853
|
+
// Fallback to JSON-only export
|
|
1854
|
+
console.warn('JSZip not available, falling back to JSON-only export');
|
|
1855
|
+
const filename = `llm-api-logs-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.json`;
|
|
1856
|
+
downloadJson({
|
|
1857
|
+
exportTime: new Date().toISOString(),
|
|
1858
|
+
logs: state.llmapiLogs,
|
|
1859
|
+
mediaNotIncluded: true,
|
|
1860
|
+
reason: 'JSZip not available'
|
|
1861
|
+
}, filename);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const zip = new JSZip();
|
|
1866
|
+
|
|
1867
|
+
// Add logs JSON
|
|
1868
|
+
const logsData = {
|
|
1869
|
+
exportTime: new Date().toISOString(),
|
|
1870
|
+
userAgent: navigator.userAgent,
|
|
1871
|
+
totalLogs: state.llmapiLogs.length,
|
|
1872
|
+
summary: {
|
|
1873
|
+
image: state.llmapiLogs.filter(l => l.taskType === 'image').length,
|
|
1874
|
+
video: state.llmapiLogs.filter(l => l.taskType === 'video').length,
|
|
1875
|
+
chat: state.llmapiLogs.filter(l => l.taskType === 'chat').length,
|
|
1876
|
+
character: state.llmapiLogs.filter(l => l.taskType === 'character').length,
|
|
1877
|
+
success: state.llmapiLogs.filter(l => l.status === 'success').length,
|
|
1878
|
+
error: state.llmapiLogs.filter(l => l.status === 'error').length,
|
|
1879
|
+
},
|
|
1880
|
+
logs: state.llmapiLogs
|
|
1881
|
+
};
|
|
1882
|
+
zip.file('llm-api-logs.json', JSON.stringify(logsData, null, 2));
|
|
1883
|
+
|
|
1884
|
+
// Collect URLs to download
|
|
1885
|
+
const mediaUrls = [];
|
|
1886
|
+
for (const log of state.llmapiLogs) {
|
|
1887
|
+
if (log.resultUrl && log.status === 'success') {
|
|
1888
|
+
mediaUrls.push({
|
|
1889
|
+
url: log.resultUrl,
|
|
1890
|
+
id: log.id,
|
|
1891
|
+
type: log.taskType,
|
|
1892
|
+
timestamp: log.timestamp
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
exportBtn.textContent = `⏳ 下载媒体 0/${mediaUrls.length}...`;
|
|
1898
|
+
|
|
1899
|
+
// Download media files
|
|
1900
|
+
const mediaFolder = zip.folder('media');
|
|
1901
|
+
let downloadedCount = 0;
|
|
1902
|
+
let failedCount = 0;
|
|
1903
|
+
const mediaManifest = [];
|
|
1904
|
+
|
|
1905
|
+
for (const item of mediaUrls) {
|
|
1906
|
+
try {
|
|
1907
|
+
// Handle both absolute and relative URLs
|
|
1908
|
+
let fetchUrl = item.url;
|
|
1909
|
+
if (fetchUrl.startsWith('/')) {
|
|
1910
|
+
fetchUrl = location.origin + fetchUrl;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
const response = await fetch(fetchUrl);
|
|
1914
|
+
if (response.ok) {
|
|
1915
|
+
const blob = await response.blob();
|
|
1916
|
+
const contentType = response.headers.get('content-type') || blob.type;
|
|
1917
|
+
|
|
1918
|
+
// Determine file extension
|
|
1919
|
+
let ext = 'bin';
|
|
1920
|
+
if (contentType.includes('image/png')) ext = 'png';
|
|
1921
|
+
else if (contentType.includes('image/jpeg')) ext = 'jpg';
|
|
1922
|
+
else if (contentType.includes('image/gif')) ext = 'gif';
|
|
1923
|
+
else if (contentType.includes('image/webp')) ext = 'webp';
|
|
1924
|
+
else if (contentType.includes('video/mp4')) ext = 'mp4';
|
|
1925
|
+
else if (contentType.includes('video/webm')) ext = 'webm';
|
|
1926
|
+
|
|
1927
|
+
// Create filename based on log id and timestamp
|
|
1928
|
+
const date = new Date(item.timestamp);
|
|
1929
|
+
const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
|
|
1930
|
+
const filename = `${dateStr}_${item.type}_${item.id.split('-').pop()}.${ext}`;
|
|
1931
|
+
|
|
1932
|
+
mediaFolder.file(filename, blob);
|
|
1933
|
+
mediaManifest.push({
|
|
1934
|
+
logId: item.id,
|
|
1935
|
+
filename,
|
|
1936
|
+
originalUrl: item.url,
|
|
1937
|
+
size: blob.size,
|
|
1938
|
+
type: contentType
|
|
1939
|
+
});
|
|
1940
|
+
downloadedCount++;
|
|
1941
|
+
} else {
|
|
1942
|
+
failedCount++;
|
|
1943
|
+
mediaManifest.push({
|
|
1944
|
+
logId: item.id,
|
|
1945
|
+
originalUrl: item.url,
|
|
1946
|
+
error: `HTTP ${response.status}`
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
failedCount++;
|
|
1951
|
+
mediaManifest.push({
|
|
1952
|
+
logId: item.id,
|
|
1953
|
+
originalUrl: item.url,
|
|
1954
|
+
error: err.message
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
exportBtn.textContent = `⏳ 下载媒体 ${downloadedCount + failedCount}/${mediaUrls.length}...`;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// Add media manifest
|
|
1962
|
+
zip.file('media-manifest.json', JSON.stringify({
|
|
1963
|
+
totalUrls: mediaUrls.length,
|
|
1964
|
+
downloaded: downloadedCount,
|
|
1965
|
+
failed: failedCount,
|
|
1966
|
+
files: mediaManifest
|
|
1967
|
+
}, null, 2));
|
|
1968
|
+
|
|
1969
|
+
exportBtn.textContent = '⏳ 生成 ZIP...';
|
|
1970
|
+
|
|
1971
|
+
// Generate and download ZIP
|
|
1972
|
+
const zipBlob = await zip.generateAsync({
|
|
1973
|
+
type: 'blob',
|
|
1974
|
+
compression: 'DEFLATE',
|
|
1975
|
+
compressionOptions: { level: 6 }
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
const filename = `llm-api-export-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip`;
|
|
1979
|
+
const url = URL.createObjectURL(zipBlob);
|
|
1980
|
+
const a = document.createElement('a');
|
|
1981
|
+
a.href = url;
|
|
1982
|
+
a.download = filename;
|
|
1983
|
+
document.body.appendChild(a);
|
|
1984
|
+
a.click();
|
|
1985
|
+
document.body.removeChild(a);
|
|
1986
|
+
URL.revokeObjectURL(url);
|
|
1987
|
+
|
|
1988
|
+
// Show summary
|
|
1989
|
+
const sizeInMB = (zipBlob.size / 1024 / 1024).toFixed(2);
|
|
1990
|
+
alert(`导出完成!\n\n日志数: ${state.llmapiLogs.length}\n媒体文件: ${downloadedCount} 成功, ${failedCount} 失败\n文件大小: ${sizeInMB} MB`);
|
|
1991
|
+
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
console.error('Export failed:', err);
|
|
1994
|
+
alert('导出失败: ' + err.message);
|
|
1995
|
+
} finally {
|
|
1996
|
+
exportBtn.disabled = false;
|
|
1997
|
+
exportBtn.textContent = originalText;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// ==================== Memory Logs ====================
|
|
2002
|
+
|
|
2003
|
+
/**
|
|
2004
|
+
* Render crash logs
|
|
2005
|
+
*/
|
|
2006
|
+
function renderCrashLogs() {
|
|
2007
|
+
const typeFilter = elements.filterCrashType?.value || '';
|
|
2008
|
+
|
|
2009
|
+
let filteredLogs = state.crashLogs;
|
|
2010
|
+
|
|
2011
|
+
if (typeFilter) {
|
|
2012
|
+
filteredLogs = filteredLogs.filter(l => l.type === typeFilter);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// Update count
|
|
2016
|
+
updateCrashCount();
|
|
2017
|
+
|
|
2018
|
+
if (filteredLogs.length === 0) {
|
|
2019
|
+
elements.crashLogsContainer.innerHTML = `
|
|
2020
|
+
<div class="empty-state">
|
|
2021
|
+
<span class="icon">💥</span>
|
|
2022
|
+
<p>暂无内存日志</p>
|
|
2023
|
+
<p style="font-size: 12px; opacity: 0.7;">页面启动、内存超限、错误和关闭时的快照会自动记录</p>
|
|
2024
|
+
</div>
|
|
2025
|
+
`;
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
elements.crashLogsContainer.innerHTML = '';
|
|
2030
|
+
filteredLogs.forEach(log => {
|
|
2031
|
+
const isExpanded = state.expandedCrashIds.has(log.id);
|
|
2032
|
+
const entry = createCrashEntry(log, isExpanded, (id, expanded) => {
|
|
2033
|
+
if (expanded) {
|
|
2034
|
+
state.expandedCrashIds.add(id);
|
|
2035
|
+
} else {
|
|
2036
|
+
state.expandedCrashIds.delete(id);
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
elements.crashLogsContainer.appendChild(entry);
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* Create a crash log entry element
|
|
2045
|
+
*/
|
|
2046
|
+
function createCrashEntry(log, isExpanded, onToggle) {
|
|
2047
|
+
const entry = document.createElement('div');
|
|
2048
|
+
// 使用与 Fetch 日志相同的样式
|
|
2049
|
+
entry.className = 'log-entry memory-entry' + (isExpanded ? ' expanded' : '');
|
|
2050
|
+
entry.dataset.id = log.id;
|
|
2051
|
+
|
|
2052
|
+
const time = formatTime(log.timestamp);
|
|
2053
|
+
|
|
2054
|
+
const typeLabels = {
|
|
2055
|
+
startup: '启动',
|
|
2056
|
+
periodic: '定期',
|
|
2057
|
+
error: '错误',
|
|
2058
|
+
beforeunload: '关闭',
|
|
2059
|
+
freeze: '卡死',
|
|
2060
|
+
whitescreen: '白屏',
|
|
2061
|
+
longtask: '长任务'
|
|
2062
|
+
};
|
|
2063
|
+
|
|
2064
|
+
const typeLabel = typeLabels[log.type] || log.type;
|
|
2065
|
+
const isError = log.type === 'error';
|
|
2066
|
+
const isWarning = log.type === 'freeze' || log.type === 'whitescreen' || log.type === 'longtask';
|
|
2067
|
+
|
|
2068
|
+
// 类型徽章样式类
|
|
2069
|
+
const typeClass = isError ? 'error' : (isWarning ? 'warning' : 'normal');
|
|
2070
|
+
|
|
2071
|
+
// Memory info - 简化显示
|
|
2072
|
+
let memoryBadge = '';
|
|
2073
|
+
let memoryPercent = 0;
|
|
2074
|
+
if (log.memory) {
|
|
2075
|
+
const usedMB = (log.memory.usedJSHeapSize / (1024 * 1024)).toFixed(0);
|
|
2076
|
+
memoryPercent = ((log.memory.usedJSHeapSize / log.memory.jsHeapSizeLimit) * 100);
|
|
2077
|
+
const memoryClass = memoryPercent >= 90 ? 'critical' : (memoryPercent >= 75 ? 'warning' : 'normal');
|
|
2078
|
+
memoryBadge = `<span class="log-memory-badge ${memoryClass}">${usedMB} MB</span>`;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// Page stats - 简化为一行
|
|
2082
|
+
let statsText = '';
|
|
2083
|
+
if (log.pageStats) {
|
|
2084
|
+
const stats = log.pageStats;
|
|
2085
|
+
statsText = `DOM ${stats.domNodeCount || 0} · Img ${stats.imageCount || 0}`;
|
|
2086
|
+
if (stats.plaitElementCount !== undefined) {
|
|
2087
|
+
statsText += ` · Plait ${stats.plaitElementCount}`;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// Performance info - 完整显示
|
|
2092
|
+
let perfText = '';
|
|
2093
|
+
if (log.performance) {
|
|
2094
|
+
const parts = [];
|
|
2095
|
+
if (log.performance.longTaskDuration) {
|
|
2096
|
+
parts.push(`任务时长: ${log.performance.longTaskDuration.toFixed(0)}ms`);
|
|
2097
|
+
}
|
|
2098
|
+
if (log.performance.freezeDuration) {
|
|
2099
|
+
parts.push(`卡死时长: ${(log.performance.freezeDuration / 1000).toFixed(1)}s`);
|
|
2100
|
+
}
|
|
2101
|
+
if (log.performance.fps !== undefined) {
|
|
2102
|
+
parts.push(`FPS: ${log.performance.fps}`);
|
|
2103
|
+
}
|
|
2104
|
+
if (parts.length > 0) {
|
|
2105
|
+
perfText = parts.join(' | ');
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// Error preview
|
|
2110
|
+
let errorPreview = '';
|
|
2111
|
+
if (log.error) {
|
|
2112
|
+
const shortError = (log.error.message || '').substring(0, 50);
|
|
2113
|
+
errorPreview = `<span class="log-url" style="color: var(--error-color);">${escapeHtml(shortError)}${log.error.message?.length > 50 ? '...' : ''}</span>`;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// 完整内存显示
|
|
2117
|
+
let memoryText = '';
|
|
2118
|
+
if (log.memory) {
|
|
2119
|
+
const usedMB = (log.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1);
|
|
2120
|
+
const limitMB = (log.memory.jsHeapSizeLimit / (1024 * 1024)).toFixed(1);
|
|
2121
|
+
memoryText = `${usedMB} MB / ${limitMB} MB (${memoryPercent.toFixed(1)}%)`;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
entry.innerHTML = `
|
|
2125
|
+
<div class="log-header">
|
|
2126
|
+
<span class="log-toggle"><span class="arrow">▶</span></span>
|
|
2127
|
+
<span class="log-time">${time}</span>
|
|
2128
|
+
<span class="log-type-badge ${typeClass}">${typeLabel}</span>
|
|
2129
|
+
${perfText ? `<span class="log-perf">⚡ ${perfText}</span>` : ''}
|
|
2130
|
+
${memoryText ? `<span class="log-memory-info">📊 ${memoryText}</span>` : ''}
|
|
2131
|
+
${statsText ? `<span class="log-stats-info">📄 ${statsText}</span>` : ''}
|
|
2132
|
+
${errorPreview}
|
|
2133
|
+
</div>
|
|
2134
|
+
<div class="log-details">
|
|
2135
|
+
<div class="detail-section">
|
|
2136
|
+
<h4>基本信息</h4>
|
|
2137
|
+
<pre>ID: ${log.id}
|
|
2138
|
+
时间: ${new Date(log.timestamp).toLocaleString('zh-CN')}
|
|
2139
|
+
URL: ${log.url || '-'}</pre>
|
|
2140
|
+
</div>
|
|
2141
|
+
${log.memory ? `
|
|
2142
|
+
<div class="detail-section">
|
|
2143
|
+
<h4>内存信息</h4>
|
|
2144
|
+
<pre>已用: ${(log.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)} MB
|
|
2145
|
+
总计: ${(log.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)} MB
|
|
2146
|
+
限制: ${(log.memory.jsHeapSizeLimit / (1024 * 1024)).toFixed(1)} MB
|
|
2147
|
+
使用率: ${memoryPercent.toFixed(1)}%</pre>
|
|
2148
|
+
</div>
|
|
2149
|
+
` : ''}
|
|
2150
|
+
${log.pageStats ? `
|
|
2151
|
+
<div class="detail-section">
|
|
2152
|
+
<h4>页面统计</h4>
|
|
2153
|
+
<pre>DOM节点: ${log.pageStats.domNodeCount || 0}
|
|
2154
|
+
Canvas: ${log.pageStats.canvasCount || 0}
|
|
2155
|
+
图片: ${log.pageStats.imageCount || 0}
|
|
2156
|
+
视频: ${log.pageStats.videoCount || 0}
|
|
2157
|
+
iframe: ${log.pageStats.iframeCount || 0}${log.pageStats.plaitElementCount !== undefined ? `\nPlait元素: ${log.pageStats.plaitElementCount}` : ''}</pre>
|
|
2158
|
+
</div>
|
|
2159
|
+
` : ''}
|
|
2160
|
+
${log.performance ? `
|
|
2161
|
+
<div class="detail-section">
|
|
2162
|
+
<h4>性能信息</h4>
|
|
2163
|
+
<pre>${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}` : ''}</pre>
|
|
2164
|
+
</div>
|
|
2165
|
+
` : ''}
|
|
2166
|
+
${log.error ? `
|
|
2167
|
+
<div class="detail-section">
|
|
2168
|
+
<h4>错误信息</h4>
|
|
2169
|
+
<pre style="color: var(--error-color);">${log.error.type}: ${escapeHtml(log.error.message)}</pre>
|
|
2170
|
+
${log.error.stack ? `<pre style="margin-top: 8px; font-size: 11px; opacity: 0.8;">${escapeHtml(log.error.stack)}</pre>` : ''}
|
|
2171
|
+
</div>
|
|
2172
|
+
` : ''}
|
|
2173
|
+
${log.customData ? `
|
|
2174
|
+
<div class="detail-section">
|
|
2175
|
+
<h4>自定义数据</h4>
|
|
2176
|
+
<pre>${JSON.stringify(log.customData, null, 2)}</pre>
|
|
2177
|
+
</div>
|
|
2178
|
+
${log.type === 'longtask' ? `
|
|
2179
|
+
<div class="detail-section" style="background: var(--warning-light); padding: 12px; border-radius: 6px; border-left: 3px solid var(--warning-color);">
|
|
2180
|
+
<h4 style="color: var(--warning-color);">💡 如何定位长任务来源</h4>
|
|
2181
|
+
<ol style="margin: 8px 0 0 0; padding-left: 20px; font-size: 12px; line-height: 1.8;">
|
|
2182
|
+
<li>打开 Chrome DevTools → Performance 面板</li>
|
|
2183
|
+
<li>点击录制按钮 ⏺,复现长任务操作</li>
|
|
2184
|
+
<li>停止录制,在 Main 线程中找到黄色/红色的长条(> 50ms)</li>
|
|
2185
|
+
<li>点击展开查看详细的函数调用栈</li>
|
|
2186
|
+
</ol>
|
|
2187
|
+
</div>
|
|
2188
|
+
` : ''}
|
|
2189
|
+
` : ''}
|
|
2190
|
+
</div>
|
|
2191
|
+
`;
|
|
2192
|
+
|
|
2193
|
+
// Toggle expand on header click
|
|
2194
|
+
const toggleBtn = entry.querySelector('.log-toggle');
|
|
2195
|
+
toggleBtn.addEventListener('click', (e) => {
|
|
2196
|
+
e.stopPropagation();
|
|
2197
|
+
const nowExpanded = entry.classList.toggle('expanded');
|
|
2198
|
+
onToggle(log.id, nowExpanded);
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
return entry;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
/**
|
|
2205
|
+
* Escape HTML to prevent XSS
|
|
2206
|
+
*/
|
|
2207
|
+
function escapeHtml(str) {
|
|
2208
|
+
if (!str) return '';
|
|
2209
|
+
return str
|
|
2210
|
+
.replace(/&/g, '&')
|
|
2211
|
+
.replace(/</g, '<')
|
|
2212
|
+
.replace(/>/g, '>')
|
|
2213
|
+
.replace(/"/g, '"')
|
|
2214
|
+
.replace(/'/g, ''');
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* Update crash log count indicator
|
|
2219
|
+
*/
|
|
2220
|
+
function updateCrashCount() {
|
|
2221
|
+
const errorCount = state.crashLogs.filter(l => l.type === 'error').length;
|
|
2222
|
+
|
|
2223
|
+
if (errorCount > 0) {
|
|
2224
|
+
elements.crashCountEl.innerHTML = `(<span style="color:var(--error-color)">${errorCount} errors</span>)`;
|
|
2225
|
+
} else {
|
|
2226
|
+
elements.crashCountEl.textContent = `(${state.crashLogs.length})`;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
/**
|
|
2231
|
+
* Load crash logs from SW
|
|
2232
|
+
*/
|
|
2233
|
+
function loadCrashLogs() {
|
|
2234
|
+
if (navigator.serviceWorker?.controller) {
|
|
2235
|
+
navigator.serviceWorker.controller.postMessage({
|
|
2236
|
+
type: 'SW_DEBUG_GET_CRASH_SNAPSHOTS'
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* Clear crash logs
|
|
2243
|
+
*/
|
|
2244
|
+
function handleClearCrashLogs() {
|
|
2245
|
+
if (navigator.serviceWorker?.controller) {
|
|
2246
|
+
navigator.serviceWorker.controller.postMessage({
|
|
2247
|
+
type: 'SW_DEBUG_CLEAR_CRASH_SNAPSHOTS'
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
state.crashLogs = [];
|
|
2251
|
+
renderCrashLogs();
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/**
|
|
2255
|
+
* Export crash logs as JSON
|
|
2256
|
+
*/
|
|
2257
|
+
function handleExportCrashLogs() {
|
|
2258
|
+
if (state.crashLogs.length === 0) {
|
|
2259
|
+
alert('没有可导出的内存日志');
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
const exportData = {
|
|
2264
|
+
exportTime: new Date().toISOString(),
|
|
2265
|
+
userAgent: navigator.userAgent,
|
|
2266
|
+
url: location.href,
|
|
2267
|
+
memorySnapshots: state.crashLogs,
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2270
|
+
const filename = `memory-logs-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.json`;
|
|
2271
|
+
downloadJson(exportData, filename);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// ==================== Memory Monitoring ====================
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Format bytes to human readable
|
|
2278
|
+
*/
|
|
2279
|
+
function formatBytes(bytes) {
|
|
2280
|
+
if (bytes === 0) return '0 B';
|
|
2281
|
+
const k = 1024;
|
|
2282
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
2283
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
2284
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/**
|
|
2288
|
+
* Update memory display
|
|
2289
|
+
*/
|
|
2290
|
+
function updateMemoryDisplay() {
|
|
2291
|
+
// Check for performance.memory (Chrome only)
|
|
2292
|
+
if (typeof performance !== 'undefined' && 'memory' in performance) {
|
|
2293
|
+
const mem = performance.memory;
|
|
2294
|
+
const usedMB = (mem.usedJSHeapSize / (1024 * 1024)).toFixed(1);
|
|
2295
|
+
const totalMB = (mem.totalJSHeapSize / (1024 * 1024)).toFixed(1);
|
|
2296
|
+
const limitMB = (mem.jsHeapSizeLimit / (1024 * 1024)).toFixed(0);
|
|
2297
|
+
const percent = ((mem.usedJSHeapSize / mem.jsHeapSizeLimit) * 100).toFixed(1);
|
|
2298
|
+
|
|
2299
|
+
elements.memoryUsed.textContent = `${usedMB} MB`;
|
|
2300
|
+
elements.memoryTotal.textContent = `${totalMB} MB`;
|
|
2301
|
+
elements.memoryLimit.textContent = `${limitMB} MB`;
|
|
2302
|
+
elements.memoryPercent.textContent = `${percent}%`;
|
|
2303
|
+
|
|
2304
|
+
// Warning if usage is high
|
|
2305
|
+
if (parseFloat(percent) > 70) {
|
|
2306
|
+
elements.memoryWarning.style.display = 'block';
|
|
2307
|
+
elements.memoryPercent.style.color = 'var(--error-color)';
|
|
2308
|
+
} else {
|
|
2309
|
+
elements.memoryWarning.style.display = 'none';
|
|
2310
|
+
elements.memoryPercent.style.color = '';
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
elements.memoryNotSupported.style.display = 'none';
|
|
2314
|
+
} else {
|
|
2315
|
+
elements.memoryUsed.textContent = '-';
|
|
2316
|
+
elements.memoryTotal.textContent = '-';
|
|
2317
|
+
elements.memoryLimit.textContent = '-';
|
|
2318
|
+
elements.memoryPercent.textContent = '-';
|
|
2319
|
+
elements.memoryNotSupported.style.display = 'block';
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// Update timestamp
|
|
2323
|
+
const now = new Date();
|
|
2324
|
+
elements.memoryUpdateTime.textContent = `更新: ${now.toLocaleTimeString('zh-CN', { hour12: false })}`;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* Start memory monitoring
|
|
2329
|
+
*/
|
|
2330
|
+
function startMemoryMonitoring() {
|
|
2331
|
+
// Initial update
|
|
2332
|
+
updateMemoryDisplay();
|
|
2333
|
+
|
|
2334
|
+
// Update every 2 seconds
|
|
2335
|
+
if (memoryMonitorInterval) {
|
|
2336
|
+
clearInterval(memoryMonitorInterval);
|
|
2337
|
+
}
|
|
2338
|
+
memoryMonitorInterval = setInterval(updateMemoryDisplay, 2000);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Stop memory monitoring
|
|
2343
|
+
*/
|
|
2344
|
+
function stopMemoryMonitoring() {
|
|
2345
|
+
if (memoryMonitorInterval) {
|
|
2346
|
+
clearInterval(memoryMonitorInterval);
|
|
2347
|
+
memoryMonitorInterval = null;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/**
|
|
2352
|
+
* Toggle debug mode
|
|
2353
|
+
*/
|
|
2354
|
+
function toggleDebug() {
|
|
2355
|
+
if (state.debugEnabled) {
|
|
2356
|
+
disableDebug();
|
|
2357
|
+
} else {
|
|
2358
|
+
enableDebug();
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/**
|
|
2363
|
+
* Clear all logs (Fetch, Console, PostMessage, and Memory logs)
|
|
2364
|
+
* Note: LLM API logs are NOT cleared here as they are important for cost tracking
|
|
2365
|
+
*/
|
|
2366
|
+
function handleClearLogs() {
|
|
2367
|
+
// Clear Fetch logs
|
|
2368
|
+
clearFetchLogs();
|
|
2369
|
+
state.logs = [];
|
|
2370
|
+
renderLogs();
|
|
2371
|
+
|
|
2372
|
+
// Clear Console logs
|
|
2373
|
+
clearConsoleLogs();
|
|
2374
|
+
state.consoleLogs = [];
|
|
2375
|
+
renderConsoleLogs();
|
|
2376
|
+
|
|
2377
|
+
// Clear PostMessage logs
|
|
2378
|
+
clearPostMessageLogsInSW();
|
|
2379
|
+
state.postmessageLogs = [];
|
|
2380
|
+
renderPostmessageLogs();
|
|
2381
|
+
|
|
2382
|
+
// Clear Memory logs (crash snapshots)
|
|
2383
|
+
if (navigator.serviceWorker?.controller) {
|
|
2384
|
+
navigator.serviceWorker.controller.postMessage({
|
|
2385
|
+
type: 'SW_DEBUG_CLEAR_CRASH_SNAPSHOTS'
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
state.crashLogs = [];
|
|
2389
|
+
renderCrashLogs();
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
/**
|
|
2393
|
+
* Clear console logs only
|
|
2394
|
+
*/
|
|
2395
|
+
function handleClearConsoleLogs() {
|
|
2396
|
+
clearConsoleLogs();
|
|
2397
|
+
state.consoleLogs = [];
|
|
2398
|
+
renderConsoleLogs();
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
/**
|
|
2402
|
+
* Copy filtered console logs to clipboard
|
|
2403
|
+
*/
|
|
2404
|
+
async function handleCopyConsoleLogs() {
|
|
2405
|
+
const levelFilter = elements.filterConsoleLevel?.value || '';
|
|
2406
|
+
const textFilter = (elements.filterConsoleText?.value || '').toLowerCase();
|
|
2407
|
+
|
|
2408
|
+
let filteredLogs = state.consoleLogs;
|
|
2409
|
+
|
|
2410
|
+
if (levelFilter) {
|
|
2411
|
+
filteredLogs = filteredLogs.filter(l => l.logLevel === levelFilter);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
if (textFilter) {
|
|
2415
|
+
filteredLogs = filteredLogs.filter(l =>
|
|
2416
|
+
l.logMessage?.toLowerCase().includes(textFilter) ||
|
|
2417
|
+
l.logStack?.toLowerCase().includes(textFilter)
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
if (filteredLogs.length === 0) {
|
|
2422
|
+
alert('没有可复制的日志');
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
// Format logs as text
|
|
2427
|
+
const logText = filteredLogs.map(log => {
|
|
2428
|
+
const time = new Date(log.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
|
|
2429
|
+
const level = `[${log.logLevel.toUpperCase()}]`;
|
|
2430
|
+
const message = log.logMessage || '';
|
|
2431
|
+
const stack = log.logStack ? `\n Stack: ${log.logStack}` : '';
|
|
2432
|
+
return `${time} ${level} ${message}${stack}`;
|
|
2433
|
+
}).join('\n');
|
|
2434
|
+
|
|
2435
|
+
try {
|
|
2436
|
+
await navigator.clipboard.writeText(logText);
|
|
2437
|
+
// Visual feedback
|
|
2438
|
+
const btn = elements.copyConsoleLogsBtn;
|
|
2439
|
+
const originalText = btn.textContent;
|
|
2440
|
+
btn.textContent = '✅ 已复制';
|
|
2441
|
+
setTimeout(() => {
|
|
2442
|
+
btn.textContent = originalText;
|
|
2443
|
+
}, 1500);
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
console.error('Failed to copy:', err);
|
|
2446
|
+
alert('复制失败');
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/**
|
|
2451
|
+
* Open export modal
|
|
2452
|
+
*/
|
|
2453
|
+
function openExportModal() {
|
|
2454
|
+
elements.exportModalOverlay?.classList.add('show');
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
/**
|
|
2458
|
+
* Close export modal
|
|
2459
|
+
*/
|
|
2460
|
+
function closeExportModal() {
|
|
2461
|
+
elements.exportModalOverlay?.classList.remove('show');
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* Setup export modal checkbox logic
|
|
2466
|
+
*/
|
|
2467
|
+
function setupExportModalCheckboxes() {
|
|
2468
|
+
const modal = elements.exportModalOverlay;
|
|
2469
|
+
if (!modal) return;
|
|
2470
|
+
|
|
2471
|
+
const selectAllCheckbox = elements.selectAllExport;
|
|
2472
|
+
const sectionCheckboxes = modal.querySelectorAll('[data-section]');
|
|
2473
|
+
const allItemCheckboxes = modal.querySelectorAll('input[name]');
|
|
2474
|
+
|
|
2475
|
+
// Update section checkbox state based on its items
|
|
2476
|
+
function updateSectionCheckbox(sectionName) {
|
|
2477
|
+
const sectionCheckbox = modal.querySelector(`[data-section="${sectionName}"]`);
|
|
2478
|
+
const items = modal.querySelectorAll(`input[name="${sectionName}"]`);
|
|
2479
|
+
if (!sectionCheckbox || items.length === 0) return;
|
|
2480
|
+
|
|
2481
|
+
const checkedItems = Array.from(items).filter(i => i.checked);
|
|
2482
|
+
sectionCheckbox.checked = checkedItems.length === items.length;
|
|
2483
|
+
sectionCheckbox.indeterminate = checkedItems.length > 0 && checkedItems.length < items.length;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// Update select all checkbox state
|
|
2487
|
+
function updateSelectAllCheckbox() {
|
|
2488
|
+
const allChecked = Array.from(allItemCheckboxes).every(cb => cb.checked);
|
|
2489
|
+
const someChecked = Array.from(allItemCheckboxes).some(cb => cb.checked);
|
|
2490
|
+
selectAllCheckbox.checked = allChecked;
|
|
2491
|
+
selectAllCheckbox.indeterminate = someChecked && !allChecked;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// Select all handler
|
|
2495
|
+
selectAllCheckbox?.addEventListener('change', () => {
|
|
2496
|
+
const checked = selectAllCheckbox.checked;
|
|
2497
|
+
allItemCheckboxes.forEach(cb => cb.checked = checked);
|
|
2498
|
+
sectionCheckboxes.forEach(cb => {
|
|
2499
|
+
cb.checked = checked;
|
|
2500
|
+
cb.indeterminate = false;
|
|
2501
|
+
});
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
// Section checkbox handlers
|
|
2505
|
+
sectionCheckboxes.forEach(sectionCb => {
|
|
2506
|
+
sectionCb.addEventListener('change', () => {
|
|
2507
|
+
const sectionName = sectionCb.dataset.section;
|
|
2508
|
+
const items = modal.querySelectorAll(`input[name="${sectionName}"]`);
|
|
2509
|
+
items.forEach(item => item.checked = sectionCb.checked);
|
|
2510
|
+
sectionCb.indeterminate = false;
|
|
2511
|
+
updateSelectAllCheckbox();
|
|
2512
|
+
});
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
// Item checkbox handlers
|
|
2516
|
+
allItemCheckboxes.forEach(cb => {
|
|
2517
|
+
cb.addEventListener('change', () => {
|
|
2518
|
+
updateSectionCheckbox(cb.name);
|
|
2519
|
+
updateSelectAllCheckbox();
|
|
2520
|
+
});
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* Get selected export options
|
|
2526
|
+
* @returns {object}
|
|
2527
|
+
*/
|
|
2528
|
+
function getExportOptions() {
|
|
2529
|
+
const basicTypes = Array.from(
|
|
2530
|
+
document.querySelectorAll('input[name="basic"]:checked')
|
|
2531
|
+
).map(el => el.value);
|
|
2532
|
+
|
|
2533
|
+
const fetchTypes = Array.from(
|
|
2534
|
+
document.querySelectorAll('input[name="fetch"]:checked')
|
|
2535
|
+
).map(el => el.value);
|
|
2536
|
+
|
|
2537
|
+
const consoleLevels = Array.from(
|
|
2538
|
+
document.querySelectorAll('input[name="console"]:checked')
|
|
2539
|
+
).map(el => el.value);
|
|
2540
|
+
|
|
2541
|
+
const postmessageDirections = Array.from(
|
|
2542
|
+
document.querySelectorAll('input[name="postmessage"]:checked')
|
|
2543
|
+
).map(el => el.value);
|
|
2544
|
+
|
|
2545
|
+
const memoryTypes = Array.from(
|
|
2546
|
+
document.querySelectorAll('input[name="memory"]:checked')
|
|
2547
|
+
).map(el => el.value);
|
|
2548
|
+
|
|
2549
|
+
const llmapiTypes = Array.from(
|
|
2550
|
+
document.querySelectorAll('input[name="llmapi"]:checked')
|
|
2551
|
+
).map(el => el.value);
|
|
2552
|
+
|
|
2553
|
+
return { basicTypes, fetchTypes, consoleLevels, postmessageDirections, memoryTypes, llmapiTypes };
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
/**
|
|
2557
|
+
* Get current memory info from browser
|
|
2558
|
+
*/
|
|
2559
|
+
function getCurrentMemoryInfo() {
|
|
2560
|
+
const memory = performance.memory;
|
|
2561
|
+
if (!memory) return null;
|
|
2562
|
+
|
|
2563
|
+
return {
|
|
2564
|
+
usedJSHeapSize: memory.usedJSHeapSize,
|
|
2565
|
+
totalJSHeapSize: memory.totalJSHeapSize,
|
|
2566
|
+
jsHeapSizeLimit: memory.jsHeapSizeLimit,
|
|
2567
|
+
usedMB: Math.round(memory.usedJSHeapSize / 1024 / 1024 * 10) / 10,
|
|
2568
|
+
totalMB: Math.round(memory.totalJSHeapSize / 1024 / 1024 * 10) / 10,
|
|
2569
|
+
limitMB: Math.round(memory.jsHeapSizeLimit / 1024 / 1024 * 10) / 10,
|
|
2570
|
+
usagePercent: Math.round(memory.usedJSHeapSize / memory.jsHeapSizeLimit * 1000) / 10,
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/**
|
|
2575
|
+
* Export logs to JSON file with selected options
|
|
2576
|
+
*/
|
|
2577
|
+
function exportLogs() {
|
|
2578
|
+
const options = getExportOptions();
|
|
2579
|
+
|
|
2580
|
+
// Build basic info
|
|
2581
|
+
let basicInfo = {};
|
|
2582
|
+
if (options.basicTypes.includes('swStatus') && state.swStatus) {
|
|
2583
|
+
basicInfo.swStatus = {
|
|
2584
|
+
version: state.swStatus.version,
|
|
2585
|
+
debugModeEnabled: state.swStatus.debugModeEnabled,
|
|
2586
|
+
pendingImageRequests: state.swStatus.pendingImageRequests,
|
|
2587
|
+
pendingVideoRequests: state.swStatus.pendingVideoRequests,
|
|
2588
|
+
videoBlobCacheSize: state.swStatus.videoBlobCacheSize,
|
|
2589
|
+
videoBlobCacheTotalBytes: state.swStatus.videoBlobCacheTotalBytes,
|
|
2590
|
+
completedImageRequestsSize: state.swStatus.completedImageRequestsSize,
|
|
2591
|
+
workflowHandlerInitialized: state.swStatus.workflowHandlerInitialized,
|
|
2592
|
+
debugLogsCount: state.swStatus.debugLogsCount,
|
|
2593
|
+
failedDomains: state.swStatus.failedDomains,
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
if (options.basicTypes.includes('memory')) {
|
|
2597
|
+
basicInfo.memory = getCurrentMemoryInfo();
|
|
2598
|
+
}
|
|
2599
|
+
if (options.basicTypes.includes('cache') && state.swStatus?.cacheStats) {
|
|
2600
|
+
basicInfo.cacheStats = state.swStatus.cacheStats;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// Filter fetch logs
|
|
2604
|
+
let filteredFetchLogs = [];
|
|
2605
|
+
if (options.fetchTypes.length > 0) {
|
|
2606
|
+
filteredFetchLogs = state.logs.filter(l =>
|
|
2607
|
+
options.fetchTypes.includes(l.requestType)
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// Filter console logs
|
|
2612
|
+
let filteredConsoleLogs = [];
|
|
2613
|
+
if (options.consoleLevels.length > 0) {
|
|
2614
|
+
filteredConsoleLogs = state.consoleLogs.filter(l =>
|
|
2615
|
+
options.consoleLevels.includes(l.logLevel)
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// Filter postmessage logs
|
|
2620
|
+
let filteredPostmessageLogs = [];
|
|
2621
|
+
if (options.postmessageDirections.length > 0) {
|
|
2622
|
+
filteredPostmessageLogs = state.postmessageLogs.filter(l =>
|
|
2623
|
+
options.postmessageDirections.includes(l.direction)
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// Filter memory logs
|
|
2628
|
+
let filteredMemoryLogs = [];
|
|
2629
|
+
if (options.memoryTypes.length > 0) {
|
|
2630
|
+
filteredMemoryLogs = state.crashLogs.filter(l =>
|
|
2631
|
+
options.memoryTypes.includes(l.type)
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// Filter LLM API logs
|
|
2636
|
+
let filteredLLMApiLogs = [];
|
|
2637
|
+
if (options.llmapiTypes.length > 0) {
|
|
2638
|
+
filteredLLMApiLogs = state.llmapiLogs.filter(l =>
|
|
2639
|
+
options.llmapiTypes.includes(l.taskType)
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// Check if anything selected
|
|
2644
|
+
const hasBasicInfo = Object.keys(basicInfo).length > 0;
|
|
2645
|
+
const hasLogs = filteredFetchLogs.length > 0 ||
|
|
2646
|
+
filteredConsoleLogs.length > 0 ||
|
|
2647
|
+
filteredPostmessageLogs.length > 0 ||
|
|
2648
|
+
filteredMemoryLogs.length > 0 ||
|
|
2649
|
+
filteredLLMApiLogs.length > 0;
|
|
2650
|
+
|
|
2651
|
+
if (!hasBasicInfo && !hasLogs) {
|
|
2652
|
+
alert('没有选中任何导出项,或选中的类型没有数据');
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
const exportData = {
|
|
2657
|
+
exportTime: new Date().toISOString(),
|
|
2658
|
+
userAgent: navigator.userAgent,
|
|
2659
|
+
url: location.href,
|
|
2660
|
+
exportOptions: options,
|
|
2661
|
+
// Basic info at the top level for easy access
|
|
2662
|
+
...basicInfo,
|
|
2663
|
+
summary: {
|
|
2664
|
+
hasBasicInfo,
|
|
2665
|
+
fetchLogs: filteredFetchLogs.length,
|
|
2666
|
+
consoleLogs: filteredConsoleLogs.length,
|
|
2667
|
+
postmessageLogs: filteredPostmessageLogs.length,
|
|
2668
|
+
memoryLogs: filteredMemoryLogs.length,
|
|
2669
|
+
llmapiLogs: filteredLLMApiLogs.length,
|
|
2670
|
+
},
|
|
2671
|
+
fetchLogs: filteredFetchLogs,
|
|
2672
|
+
consoleLogs: filteredConsoleLogs,
|
|
2673
|
+
postmessageLogs: filteredPostmessageLogs,
|
|
2674
|
+
memoryLogs: filteredMemoryLogs,
|
|
2675
|
+
llmapiLogs: filteredLLMApiLogs,
|
|
2676
|
+
};
|
|
2677
|
+
|
|
2678
|
+
const filename = `sw-debug-logs-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.json`;
|
|
2679
|
+
downloadJson(exportData, filename);
|
|
2680
|
+
closeExportModal();
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
/**
|
|
2684
|
+
* Switch active tab
|
|
2685
|
+
* @param {string} tabName
|
|
2686
|
+
*/
|
|
2687
|
+
function switchTab(tabName) {
|
|
2688
|
+
state.activeTab = tabName;
|
|
2689
|
+
document.querySelectorAll('.tab').forEach(t => {
|
|
2690
|
+
t.classList.toggle('active', t.dataset.tab === tabName);
|
|
2691
|
+
});
|
|
2692
|
+
document.querySelectorAll('.tab-content').forEach(c => {
|
|
2693
|
+
c.classList.toggle('active', c.id === tabName + 'Tab');
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
// Update error dots (hide dot for active tab)
|
|
2697
|
+
updateErrorDots();
|
|
2698
|
+
|
|
2699
|
+
if (tabName === 'fetch') {
|
|
2700
|
+
renderLogs();
|
|
2701
|
+
} else if (tabName === 'console') {
|
|
2702
|
+
renderConsoleLogs();
|
|
2703
|
+
} else if (tabName === 'postmessage') {
|
|
2704
|
+
renderPostmessageLogs();
|
|
2705
|
+
} else if (tabName === 'llmapi') {
|
|
2706
|
+
loadLLMApiLogs();
|
|
2707
|
+
} else if (tabName === 'crash') {
|
|
2708
|
+
loadCrashLogs();
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
/**
|
|
2713
|
+
* Handle SW status update
|
|
2714
|
+
* @param {object} data
|
|
2715
|
+
*/
|
|
2716
|
+
function handleStatusUpdate(data) {
|
|
2717
|
+
updateSwStatus(elements.swStatus, true, data.status?.version);
|
|
2718
|
+
state.debugEnabled = updateStatusPanel(data.status, elements);
|
|
2719
|
+
state.swStatus = data.status; // Store for export
|
|
2720
|
+
updateDebugButton(elements.toggleDebugBtn, state.debugEnabled);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
/**
|
|
2724
|
+
* Setup event listeners
|
|
2725
|
+
*/
|
|
2726
|
+
function setupEventListeners() {
|
|
2727
|
+
elements.toggleDebugBtn.addEventListener('click', toggleDebug);
|
|
2728
|
+
elements.exportLogsBtn.addEventListener('click', openExportModal);
|
|
2729
|
+
elements.doExportBtn?.addEventListener('click', exportLogs);
|
|
2730
|
+
elements.closeExportModalBtn?.addEventListener('click', closeExportModal);
|
|
2731
|
+
elements.cancelExportBtn?.addEventListener('click', closeExportModal);
|
|
2732
|
+
elements.clearLogsBtn.addEventListener('click', handleClearLogs);
|
|
2733
|
+
|
|
2734
|
+
// Close modal when clicking overlay
|
|
2735
|
+
elements.exportModalOverlay?.addEventListener('click', (e) => {
|
|
2736
|
+
if (e.target === elements.exportModalOverlay) {
|
|
2737
|
+
closeExportModal();
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
|
|
2741
|
+
// Setup export modal checkboxes
|
|
2742
|
+
setupExportModalCheckboxes();
|
|
2743
|
+
|
|
2744
|
+
elements.refreshStatusBtn.addEventListener('click', refreshStatus);
|
|
2745
|
+
elements.refreshCacheBtn.addEventListener('click', refreshStatus);
|
|
2746
|
+
elements.enableDebugBtn?.addEventListener('click', toggleDebug);
|
|
2747
|
+
elements.clearConsoleLogsBtn?.addEventListener('click', handleClearConsoleLogs);
|
|
2748
|
+
elements.copyConsoleLogsBtn?.addEventListener('click', handleCopyConsoleLogs);
|
|
2749
|
+
|
|
2750
|
+
elements.filterType?.addEventListener('change', renderLogs);
|
|
2751
|
+
elements.filterStatus?.addEventListener('change', renderLogs);
|
|
2752
|
+
elements.filterTimeRange?.addEventListener('change', renderLogs);
|
|
2753
|
+
elements.filterUrl?.addEventListener('input', renderLogs);
|
|
2754
|
+
|
|
2755
|
+
// 慢请求点击过滤
|
|
2756
|
+
elements.statSlowRequestsWrapper?.addEventListener('click', () => {
|
|
2757
|
+
toggleSlowRequestFilter();
|
|
2758
|
+
});
|
|
2759
|
+
elements.togglePauseBtn?.addEventListener('click', togglePause);
|
|
2760
|
+
elements.toggleSelectModeBtn?.addEventListener('click', toggleSelectMode);
|
|
2761
|
+
elements.selectAllBtn?.addEventListener('click', selectAllLogs);
|
|
2762
|
+
elements.batchBookmarkBtn?.addEventListener('click', batchBookmarkLogs);
|
|
2763
|
+
elements.batchDeleteBtn?.addEventListener('click', batchDeleteLogs);
|
|
2764
|
+
elements.filterUrlRegex?.addEventListener('change', renderLogs);
|
|
2765
|
+
elements.copyFetchLogsBtn?.addEventListener('click', handleCopyFetchLogs);
|
|
2766
|
+
elements.exportFetchCSVBtn?.addEventListener('click', exportFetchCSV);
|
|
2767
|
+
elements.showShortcutsBtn?.addEventListener('click', showShortcutsModal);
|
|
2768
|
+
elements.showBookmarksOnly?.addEventListener('change', (e) => {
|
|
2769
|
+
state.showBookmarksOnly = e.target.checked;
|
|
2770
|
+
renderLogs();
|
|
2771
|
+
});
|
|
2772
|
+
elements.closeShortcutsModalBtn?.addEventListener('click', closeShortcutsModal);
|
|
2773
|
+
elements.shortcutsModalOverlay?.addEventListener('click', (e) => {
|
|
2774
|
+
if (e.target === elements.shortcutsModalOverlay) {
|
|
2775
|
+
closeShortcutsModal();
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2778
|
+
elements.toggleThemeBtn?.addEventListener('click', toggleTheme);
|
|
2779
|
+
elements.showSettingsBtn?.addEventListener('click', showSettingsModal);
|
|
2780
|
+
elements.closeSettingsModalBtn?.addEventListener('click', closeSettingsModal);
|
|
2781
|
+
elements.saveSettingsBtn?.addEventListener('click', saveSettings);
|
|
2782
|
+
elements.settingsModalOverlay?.addEventListener('click', (e) => {
|
|
2783
|
+
if (e.target === elements.settingsModalOverlay) {
|
|
2784
|
+
closeSettingsModal();
|
|
2785
|
+
}
|
|
2786
|
+
});
|
|
2787
|
+
elements.filterConsoleLevel?.addEventListener('change', renderConsoleLogs);
|
|
2788
|
+
elements.filterConsoleText?.addEventListener('input', renderConsoleLogs);
|
|
2789
|
+
elements.autoScrollCheckbox?.addEventListener('change', (e) => {
|
|
2790
|
+
state.autoScroll = e.target.checked;
|
|
2791
|
+
});
|
|
2792
|
+
|
|
2793
|
+
// PostMessage log event listeners
|
|
2794
|
+
elements.filterMessageDirection?.addEventListener('change', renderPostmessageLogs);
|
|
2795
|
+
elements.filterMessageTypeSelect?.addEventListener('change', renderPostmessageLogs);
|
|
2796
|
+
elements.filterPmTimeRange?.addEventListener('change', renderPostmessageLogs);
|
|
2797
|
+
elements.filterMessageType?.addEventListener('input', renderPostmessageLogs);
|
|
2798
|
+
elements.clearPostmessageLogsBtn?.addEventListener('click', handleClearPostmessageLogs);
|
|
2799
|
+
elements.copyPostmessageLogsBtn?.addEventListener('click', handleCopyPostmessageLogs);
|
|
2800
|
+
|
|
2801
|
+
// LLM API log event listeners
|
|
2802
|
+
elements.filterLLMApiType?.addEventListener('change', renderLLMApiLogs);
|
|
2803
|
+
elements.filterLLMApiStatus?.addEventListener('change', renderLLMApiLogs);
|
|
2804
|
+
elements.refreshLLMApiLogsBtn?.addEventListener('click', loadLLMApiLogs);
|
|
2805
|
+
elements.copyLLMApiLogsBtn?.addEventListener('click', handleCopyLLMApiLogs);
|
|
2806
|
+
elements.exportLLMApiLogsBtn?.addEventListener('click', handleExportLLMApiLogs);
|
|
2807
|
+
elements.clearLLMApiLogsBtn?.addEventListener('click', handleClearLLMApiLogs);
|
|
2808
|
+
|
|
2809
|
+
// Crash log event listeners
|
|
2810
|
+
elements.filterCrashType?.addEventListener('change', renderCrashLogs);
|
|
2811
|
+
elements.refreshCrashLogsBtn?.addEventListener('click', loadCrashLogs);
|
|
2812
|
+
elements.copyCrashLogsBtn?.addEventListener('click', handleCopyCrashLogs);
|
|
2813
|
+
elements.clearCrashLogsBtn?.addEventListener('click', handleClearCrashLogs);
|
|
2814
|
+
elements.exportCrashLogsBtn?.addEventListener('click', handleExportCrashLogs);
|
|
2815
|
+
|
|
2816
|
+
// Tab switching
|
|
2817
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
2818
|
+
tab.addEventListener('click', () => switchTab(tab.dataset.tab));
|
|
2819
|
+
});
|
|
2820
|
+
|
|
2821
|
+
// Keyboard shortcuts
|
|
2822
|
+
document.addEventListener('keydown', (e) => {
|
|
2823
|
+
// Ignore if typing in an input
|
|
2824
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// Space - Toggle pause (when in fetch tab)
|
|
2829
|
+
if (e.code === 'Space' && state.activeTab === 'fetch') {
|
|
2830
|
+
e.preventDefault();
|
|
2831
|
+
togglePause();
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// Number keys 1-5 to switch tabs
|
|
2835
|
+
const tabMap = { '1': 'fetch', '2': 'console', '3': 'postmessage', '4': 'llmapi', '5': 'crash' };
|
|
2836
|
+
if (tabMap[e.key] && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
2837
|
+
switchTab(tabMap[e.key]);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// Escape - Close any open modals
|
|
2841
|
+
if (e.key === 'Escape') {
|
|
2842
|
+
closeExportModal();
|
|
2843
|
+
closeShortcutsModal();
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// ? - Show shortcuts help
|
|
2847
|
+
if (e.key === '?' || (e.shiftKey && e.key === '/')) {
|
|
2848
|
+
showShortcutsModal();
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
// Ctrl/Cmd + L - Clear current tab logs
|
|
2852
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
|
2853
|
+
e.preventDefault();
|
|
2854
|
+
if (state.activeTab === 'fetch') {
|
|
2855
|
+
handleClearLogs();
|
|
2856
|
+
} else if (state.activeTab === 'console') {
|
|
2857
|
+
handleClearConsoleLogs();
|
|
2858
|
+
} else if (state.activeTab === 'postmessage') {
|
|
2859
|
+
handleClearPostmessageLogs();
|
|
2860
|
+
} else if (state.activeTab === 'llmapi') {
|
|
2861
|
+
handleClearLLMApiLogs();
|
|
2862
|
+
} else if (state.activeTab === 'crash') {
|
|
2863
|
+
handleClearCrashLogs();
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
/**
|
|
2870
|
+
* Setup SW message handlers
|
|
2871
|
+
*/
|
|
2872
|
+
function setupMessageHandlers() {
|
|
2873
|
+
registerMessageHandlers({
|
|
2874
|
+
'SW_DEBUG_STATUS': handleStatusUpdate,
|
|
2875
|
+
'SW_DEBUG_ENABLED': () => {
|
|
2876
|
+
state.debugEnabled = true;
|
|
2877
|
+
updateDebugButton(elements.toggleDebugBtn, true);
|
|
2878
|
+
// Update status panel text to show "开启"
|
|
2879
|
+
if (elements.debugMode) {
|
|
2880
|
+
elements.debugMode.textContent = '开启';
|
|
2881
|
+
}
|
|
2882
|
+
renderLogs(); // Refresh to remove "enable debug" button
|
|
2883
|
+
// Refresh status after debug enabled to get latest state
|
|
2884
|
+
// This ensures cache stats and other info are up-to-date
|
|
2885
|
+
refreshStatus();
|
|
2886
|
+
},
|
|
2887
|
+
'SW_DEBUG_DISABLED': () => {
|
|
2888
|
+
state.debugEnabled = false;
|
|
2889
|
+
updateDebugButton(elements.toggleDebugBtn, false);
|
|
2890
|
+
// Update status panel text to show "关闭"
|
|
2891
|
+
if (elements.debugMode) {
|
|
2892
|
+
elements.debugMode.textContent = '关闭';
|
|
2893
|
+
}
|
|
2894
|
+
renderLogs(); // Refresh to show "enable debug" button
|
|
2895
|
+
},
|
|
2896
|
+
'SW_DEBUG_LOG': (data) => addOrUpdateLog(data.entry),
|
|
2897
|
+
'SW_DEBUG_LOGS': (data) => {
|
|
2898
|
+
state.logs = data.logs || [];
|
|
2899
|
+
renderLogs();
|
|
2900
|
+
},
|
|
2901
|
+
'SW_DEBUG_LOGS_CLEARED': () => {
|
|
2902
|
+
state.logs = [];
|
|
2903
|
+
renderLogs();
|
|
2904
|
+
},
|
|
2905
|
+
'SW_CONSOLE_LOG': (data) => addConsoleLog(data.entry),
|
|
2906
|
+
'SW_DEBUG_CONSOLE_LOGS': (data) => {
|
|
2907
|
+
state.consoleLogs = data.logs || [];
|
|
2908
|
+
renderConsoleLogs();
|
|
2909
|
+
},
|
|
2910
|
+
'SW_DEBUG_CONSOLE_LOGS_CLEARED': () => {
|
|
2911
|
+
state.consoleLogs = [];
|
|
2912
|
+
renderConsoleLogs();
|
|
2913
|
+
},
|
|
2914
|
+
'SW_POSTMESSAGE_LOG': (data) => addPostmessageLog(data.entry),
|
|
2915
|
+
'SW_DEBUG_POSTMESSAGE_LOGS': (data) => {
|
|
2916
|
+
state.postmessageLogs = data.logs || [];
|
|
2917
|
+
updateMessageTypeOptions();
|
|
2918
|
+
renderPostmessageLogs();
|
|
2919
|
+
},
|
|
2920
|
+
'SW_DEBUG_POSTMESSAGE_LOGS_CLEARED': () => {
|
|
2921
|
+
state.postmessageLogs = [];
|
|
2922
|
+
renderPostmessageLogs();
|
|
2923
|
+
},
|
|
2924
|
+
'SW_DEBUG_CRASH_SNAPSHOTS': (data) => {
|
|
2925
|
+
state.crashLogs = data.snapshots || [];
|
|
2926
|
+
renderCrashLogs();
|
|
2927
|
+
},
|
|
2928
|
+
'SW_DEBUG_NEW_CRASH_SNAPSHOT': (data) => {
|
|
2929
|
+
// 实时接收新的内存快照
|
|
2930
|
+
if (data.snapshot) {
|
|
2931
|
+
// 添加到列表开头
|
|
2932
|
+
state.crashLogs.unshift(data.snapshot);
|
|
2933
|
+
// 限制数量
|
|
2934
|
+
if (state.crashLogs.length > 100) {
|
|
2935
|
+
state.crashLogs.pop();
|
|
2936
|
+
}
|
|
2937
|
+
renderCrashLogs();
|
|
2938
|
+
}
|
|
2939
|
+
},
|
|
2940
|
+
'SW_DEBUG_CRASH_SNAPSHOTS_CLEARED': () => {
|
|
2941
|
+
state.crashLogs = [];
|
|
2942
|
+
renderCrashLogs();
|
|
2943
|
+
},
|
|
2944
|
+
'SW_DEBUG_LLM_API_LOGS': (data) => {
|
|
2945
|
+
state.llmapiLogs = data.logs || [];
|
|
2946
|
+
renderLLMApiLogs();
|
|
2947
|
+
},
|
|
2948
|
+
'SW_DEBUG_LLM_API_LOG': (data) => {
|
|
2949
|
+
// 实时接收新的 LLM API 日志
|
|
2950
|
+
if (data.log) {
|
|
2951
|
+
// 检查是否是更新现有日志
|
|
2952
|
+
const existingIndex = state.llmapiLogs.findIndex(l => l.id === data.log.id);
|
|
2953
|
+
if (existingIndex >= 0) {
|
|
2954
|
+
state.llmapiLogs[existingIndex] = data.log;
|
|
2955
|
+
} else {
|
|
2956
|
+
state.llmapiLogs.unshift(data.log);
|
|
2957
|
+
}
|
|
2958
|
+
// 限制数量
|
|
2959
|
+
if (state.llmapiLogs.length > 200) {
|
|
2960
|
+
state.llmapiLogs.pop();
|
|
2961
|
+
}
|
|
2962
|
+
renderLLMApiLogs();
|
|
2963
|
+
}
|
|
2964
|
+
},
|
|
2965
|
+
'SW_DEBUG_LLM_API_LOGS_CLEARED': () => {
|
|
2966
|
+
state.llmapiLogs = [];
|
|
2967
|
+
renderLLMApiLogs();
|
|
2968
|
+
},
|
|
2969
|
+
'SW_DEBUG_EXPORT_DATA': () => {
|
|
2970
|
+
// Handle export data from SW if needed
|
|
2971
|
+
},
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2974
|
+
onControllerChange(() => {
|
|
2975
|
+
updateSwStatus(elements.swStatus, true);
|
|
2976
|
+
refreshStatus();
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/**
|
|
2981
|
+
* Initialize the application
|
|
2982
|
+
*/
|
|
2983
|
+
async function init() {
|
|
2984
|
+
cacheElements();
|
|
2985
|
+
|
|
2986
|
+
// Check SW availability
|
|
2987
|
+
if (!('serviceWorker' in navigator)) {
|
|
2988
|
+
alert('此浏览器不支持 Service Worker');
|
|
2989
|
+
updateSwStatus(elements.swStatus, false);
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
const swReady = await checkSwReady();
|
|
2994
|
+
|
|
2995
|
+
if (!swReady) {
|
|
2996
|
+
alert('Service Worker 未注册或未激活\n\n请先访问主应用,然后刷新此页面');
|
|
2997
|
+
updateSwStatus(elements.swStatus, false);
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
console.log('[SW Debug] SW ready, controller:', !!navigator.serviceWorker.controller);
|
|
3002
|
+
|
|
3003
|
+
updateSwStatus(elements.swStatus, true);
|
|
3004
|
+
|
|
3005
|
+
// Load saved bookmarks, theme, and settings
|
|
3006
|
+
loadBookmarks();
|
|
3007
|
+
loadTheme();
|
|
3008
|
+
loadSettings();
|
|
3009
|
+
|
|
3010
|
+
// Register PostMessage logging callback
|
|
3011
|
+
setPostMessageLogCallback(addPostmessageLog);
|
|
3012
|
+
|
|
3013
|
+
setupMessageHandlers();
|
|
3014
|
+
setupEventListeners();
|
|
3015
|
+
|
|
3016
|
+
// Auto-enable debug mode first when entering debug page
|
|
3017
|
+
// The SW_DEBUG_ENABLED handler will then call refreshStatus() to get latest state
|
|
3018
|
+
// This ensures proper state synchronization and avoids race conditions
|
|
3019
|
+
console.log('[SW Debug] Auto-enabling debug mode');
|
|
3020
|
+
enableDebug();
|
|
3021
|
+
|
|
3022
|
+
// Load console logs from IndexedDB (independent of debug mode status)
|
|
3023
|
+
loadConsoleLogs();
|
|
3024
|
+
// Load PostMessage logs from SW
|
|
3025
|
+
loadPostMessageLogs();
|
|
3026
|
+
// Load crash logs
|
|
3027
|
+
loadCrashLogs();
|
|
3028
|
+
renderLogs();
|
|
3029
|
+
|
|
3030
|
+
// Start memory monitoring
|
|
3031
|
+
startMemoryMonitoring();
|
|
3032
|
+
|
|
3033
|
+
// Heartbeat mechanism to keep debug mode alive
|
|
3034
|
+
// This allows SW to detect when debug page is truly closed (no heartbeat for 15s)
|
|
3035
|
+
// vs just refreshed (new page immediately sends heartbeat)
|
|
3036
|
+
const HEARTBEAT_INTERVAL = 5000; // 5 seconds
|
|
3037
|
+
|
|
3038
|
+
function sendHeartbeat() {
|
|
3039
|
+
if (navigator.serviceWorker?.controller) {
|
|
3040
|
+
navigator.serviceWorker.controller.postMessage({ type: 'SW_DEBUG_HEARTBEAT' });
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// Send initial heartbeat
|
|
3045
|
+
sendHeartbeat();
|
|
3046
|
+
|
|
3047
|
+
// Start heartbeat interval
|
|
3048
|
+
const heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);
|
|
3049
|
+
|
|
3050
|
+
// When page becomes visible again, immediately send heartbeat
|
|
3051
|
+
// This handles browser throttling of background tabs
|
|
3052
|
+
document.addEventListener('visibilitychange', () => {
|
|
3053
|
+
if (document.visibilityState === 'visible') {
|
|
3054
|
+
sendHeartbeat();
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
|
|
3058
|
+
// Clean up on page unload (stop heartbeat timer and memory monitoring)
|
|
3059
|
+
window.addEventListener('beforeunload', () => {
|
|
3060
|
+
clearInterval(heartbeatTimer);
|
|
3061
|
+
stopMemoryMonitoring();
|
|
3062
|
+
// Don't send disable message here - let SW detect timeout instead
|
|
3063
|
+
// This allows refresh to work without disabling debug mode
|
|
3064
|
+
});
|
|
3065
|
+
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// Start the app
|
|
3069
|
+
init();
|