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.
Files changed (237) hide show
  1. package/.DS_Store +0 -0
  2. package/README.md +47 -0
  3. package/_headers +84 -0
  4. package/_redirects +2 -0
  5. package/assets/ChatMessagesArea-CkUX81uB.js +251 -0
  6. package/assets/ChatMessagesArea-Di0Z80Zh.css +1 -0
  7. package/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  8. package/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  9. package/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  10. package/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  11. package/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  12. package/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  13. package/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  14. package/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  15. package/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  16. package/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  17. package/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  18. package/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  19. package/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  20. package/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  21. package/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  22. package/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  23. package/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  24. package/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  25. package/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  26. package/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  27. package/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  28. package/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  29. package/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  30. package/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  31. package/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  32. package/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  33. package/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  34. package/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  35. package/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  36. package/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  37. package/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  38. package/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  39. package/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  40. package/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  41. package/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  42. package/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  43. package/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  44. package/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  45. package/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  46. package/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  47. package/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  48. package/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  49. package/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  50. package/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  51. package/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  52. package/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  53. package/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  54. package/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  55. package/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  56. package/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  57. package/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  58. package/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  59. package/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  60. package/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  61. package/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  62. package/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  63. package/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  64. package/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  65. package/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  66. package/assets/Tableau10-B-NsZVaP.js +1 -0
  67. package/assets/Tableau10-Dnlau_Wv.js +1 -0
  68. package/assets/ToolboxDrawer-By1XMh8B.js +87 -0
  69. package/assets/ToolboxDrawer-fPqvDLQE.css +1 -0
  70. package/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  71. package/assets/ai-analyze-Db-iXol6.js +1 -0
  72. package/assets/arc-BZXVqUcI.js +1 -0
  73. package/assets/arc-ajYHRRnk.js +1 -0
  74. package/assets/array-B5oSNiGi.js +1 -0
  75. package/assets/array-BKyUJesY.js +1 -0
  76. package/assets/batch-image-generation-Baqb01Lm.js +6 -0
  77. package/assets/batch-image-generation-CbLMWmjk.css +1 -0
  78. package/assets/blockDiagram-38ab4fdb-BT3H_WVv.js +118 -0
  79. package/assets/blockDiagram-38ab4fdb-u0xYP3Lt.js +118 -0
  80. package/assets/c4Diagram-3d4e48cf-CBvM6zjM.js +10 -0
  81. package/assets/c4Diagram-3d4e48cf-WOIEVidH.js +10 -0
  82. package/assets/channel-BP25wTsw.js +1 -0
  83. package/assets/channel-HzrLNFUg.js +1 -0
  84. package/assets/classDiagram-70f12bd4-BMutcvFi.js +2 -0
  85. package/assets/classDiagram-70f12bd4-Cl9U1r5F.js +2 -0
  86. package/assets/classDiagram-v2-f2320105-C0agtbR4.js +2 -0
  87. package/assets/classDiagram-v2-f2320105-tCBzATK6.js +2 -0
  88. package/assets/clone-B69pF7Y_.js +1 -0
  89. package/assets/clone-oX7o-l4R.js +1 -0
  90. package/assets/createText-2e5e7dd3-CZ9_fscE.js +5 -0
  91. package/assets/createText-2e5e7dd3-idrqgJjU.js +7 -0
  92. package/assets/edges-e0da2a9e-C-RyePMV.js +4 -0
  93. package/assets/edges-e0da2a9e-DJXAjJSL.js +4 -0
  94. package/assets/erDiagram-9861fffd-DWJR_3zL.js +51 -0
  95. package/assets/erDiagram-9861fffd-x2Kcy95-.js +51 -0
  96. package/assets/flowDb-956e92f1-BgKjOIdz.js +10 -0
  97. package/assets/flowDb-956e92f1-CF6y18Tn.js +10 -0
  98. package/assets/flowDiagram-66a62f08-BPPw0wPU.js +4 -0
  99. package/assets/flowDiagram-66a62f08-CSAllSFf.js +4 -0
  100. package/assets/flowDiagram-v2-96b9c2cf-B-UGyXRi.js +1 -0
  101. package/assets/flowDiagram-v2-96b9c2cf-Cm596kxZ.js +1 -0
  102. package/assets/flowchart-elk-definition-4a651766-9XSRJbsr.js +139 -0
  103. package/assets/flowchart-elk-definition-4a651766-DWFN9DN3.js +139 -0
  104. package/assets/ganttDiagram-c361ad54-D9tbz9tQ.js +257 -0
  105. package/assets/ganttDiagram-c361ad54-ot5pUYpT.js +257 -0
  106. package/assets/gitGraphDiagram-72cf32ee-BFV3Mt8C.js +70 -0
  107. package/assets/gitGraphDiagram-72cf32ee-C6qFzgGh.js +70 -0
  108. package/assets/graph-BxwlF7JS.js +1 -0
  109. package/assets/graph-D-2Ldvxg.js +1 -0
  110. package/assets/grid-image-cM9AmYC8.js +1 -0
  111. package/assets/has-CgdIPiQG.js +1 -0
  112. package/assets/hasIn-4iY02rGN.js +1 -0
  113. package/assets/index-3862675e-CVZnpwDN.js +1 -0
  114. package/assets/index-3862675e-DqdI9cab.js +1 -0
  115. package/assets/index-B2dvADz8.css +1 -0
  116. package/assets/index-BicRPzXC.js +1 -0
  117. package/assets/index-Bs7-jmv6.css +1 -0
  118. package/assets/index-BwSGXyRr.js +99 -0
  119. package/assets/index-C1XdOOAn.css +1 -0
  120. package/assets/index-C4AKKbpQ.css +1 -0
  121. package/assets/index-CkpXFt8n.js +1 -0
  122. package/assets/index-CrxF9gFe.js +42 -0
  123. package/assets/index-DBWqXBIQ.js +93 -0
  124. package/assets/index-DI_5V2-m.js +3 -0
  125. package/assets/index-DWUAFoZG.js +2064 -0
  126. package/assets/index-Dn0YtZ2R.js +3 -0
  127. package/assets/index-e05Rs4M6.js +12 -0
  128. package/assets/index.dom-C3-224fz.js +1 -0
  129. package/assets/infoDiagram-f8f76790-CnrpwoOt.js +7 -0
  130. package/assets/infoDiagram-f8f76790-FKC1Sy9Y.js +7 -0
  131. package/assets/init-A0kIFD9x.js +1 -0
  132. package/assets/init-Gi6I4Gst.js +1 -0
  133. package/assets/inspiration-board-B_-BBBHt.js +1 -0
  134. package/assets/isEmpty-Dj2GV0v-.js +1 -0
  135. package/assets/journeyDiagram-49397b02-B7fP21sU.js +139 -0
  136. package/assets/journeyDiagram-49397b02-Dp3X9XWq.js +139 -0
  137. package/assets/katex-BbEIqZs1.js +261 -0
  138. package/assets/katex-Cu_Erd72.js +261 -0
  139. package/assets/layout-BD3yCK_X.js +1 -0
  140. package/assets/layout-DHHYqX7p.js +1 -0
  141. package/assets/line-B3bNrkzn.js +1 -0
  142. package/assets/line-B86HLuqu.js +1 -0
  143. package/assets/linear-DU2Ciymb.js +1 -0
  144. package/assets/linear-wCAlMhOS.js +1 -0
  145. package/assets/mermaid.core-DfVvnpgz.js +91 -0
  146. package/assets/mindmap-definition-fc14e90a-D1sxE3xG.js +425 -0
  147. package/assets/mindmap-definition-fc14e90a-YuSOJC7P.js +425 -0
  148. package/assets/ordinal-BRr1uYdk.js +1 -0
  149. package/assets/ordinal-Cboi1Yqb.js +1 -0
  150. package/assets/path-CY0bYimO.js +1 -0
  151. package/assets/path-CbwjOpE9.js +1 -0
  152. package/assets/photo-wall-splitter-BVU2e0aS.js +1 -0
  153. package/assets/pick-Cvlwra4g.js +1 -0
  154. package/assets/pieDiagram-8a3498a8-B6mJUqro.js +35 -0
  155. package/assets/pieDiagram-8a3498a8-B91bWgo_.js +35 -0
  156. package/assets/quadrantDiagram-120e2f19-BxS8fQEz.js +7 -0
  157. package/assets/quadrantDiagram-120e2f19-DwudONqx.js +7 -0
  158. package/assets/requirementDiagram-deff3bca-DygaMIoy.js +52 -0
  159. package/assets/requirementDiagram-deff3bca-v9xlgfS8.js +52 -0
  160. package/assets/sankeyDiagram-04a897e0-BV23dp4l.js +8 -0
  161. package/assets/sankeyDiagram-04a897e0-BXCiXiyw.js +8 -0
  162. package/assets/sequenceDiagram-704730f1-CObRpNi4.js +122 -0
  163. package/assets/sequenceDiagram-704730f1-Ck69A6wI.js +122 -0
  164. package/assets/settings-dialog-BlCO49C4.js +1 -0
  165. package/assets/settings-dialog-QUxXj54T.css +1 -0
  166. package/assets/stateDiagram-587899a1-J_G6I0oo.js +1 -0
  167. package/assets/stateDiagram-587899a1-z-tKclr3.js +1 -0
  168. package/assets/stateDiagram-v2-d93cdb3a-DsThtOzP.js +1 -0
  169. package/assets/stateDiagram-v2-d93cdb3a-XIvq5t8a.js +1 -0
  170. package/assets/styles-6aaf32cf-1fjuNMUk.js +207 -0
  171. package/assets/styles-6aaf32cf-DT2rVNfQ.js +207 -0
  172. package/assets/styles-9a916d00-fLeUSina.js +160 -0
  173. package/assets/styles-9a916d00-q64Umkis.js +160 -0
  174. package/assets/styles-c10674c1-BWlxVc3Q.js +116 -0
  175. package/assets/styles-c10674c1-CtYpjMYU.js +116 -0
  176. package/assets/svgDrawCommon-08f97a94-C_DhKfny.js +1 -0
  177. package/assets/svgDrawCommon-08f97a94-DSBqmUv2.js +1 -0
  178. package/assets/timeline-definition-85554ec2-AKpzwLPN.js +61 -0
  179. package/assets/timeline-definition-85554ec2-dTkYwoLF.js +61 -0
  180. package/assets/ttd-dialog-CxiaIUuJ.js +47 -0
  181. package/assets/ttd-dialog-DCapefb6.css +1 -0
  182. package/assets/upload-4sxUU7q_.js +1 -0
  183. package/assets/video-recovery-service-BckHbSyK.js +1 -0
  184. package/assets/web-vitals-DcvjKPr-.js +1 -0
  185. package/assets/winbox.bundle.min-CoRPjCs5.js +1 -0
  186. package/assets/xlsx-CkFp8p6R.js +105 -0
  187. package/assets/xychartDiagram-e933f94c-DCmvL0ag.js +7 -0
  188. package/assets/xychartDiagram-e933f94c-aqOiXp_u.js +7 -0
  189. package/batch-image.html +1616 -0
  190. package/favicon.ico +0 -0
  191. package/icons/README.md +55 -0
  192. package/icons/aitu10.png +0 -0
  193. package/icons/android-chrome-192x192.png +0 -0
  194. package/icons/android-chrome-512x512.png +0 -0
  195. package/icons/apple-touch-icon.png +0 -0
  196. package/icons/favicon-16x16.png +0 -0
  197. package/icons/favicon-16x16.svg +539 -0
  198. package/icons/favicon-32x32.png +0 -0
  199. package/icons/favicon-32x32.svg +539 -0
  200. package/icons/favicon-new.svg +539 -0
  201. package/icons/favicon-new.svg.png +0 -0
  202. package/icons/icon-192x192.svg +539 -0
  203. package/icons/icon-512x512.svg +539 -0
  204. package/icons/icon-96x96.png +0 -0
  205. package/icons/icon-96x96.svg +539 -0
  206. package/iframe-test.html +340 -0
  207. package/index.html +105 -0
  208. package/init.json +6 -0
  209. package/logo/cardid.jpg +0 -0
  210. package/logo/group-qr.png +0 -0
  211. package/logo/logo_drawnix_h.svg +539 -0
  212. package/logo/logo_drawnix_h_dark.svg +539 -0
  213. package/logo/logo_drawnix_new.svg +539 -0
  214. package/manifest.json +52 -0
  215. package/package.json +31 -0
  216. package/product_showcase/aitu-01.png +0 -0
  217. package/product_showcase/aitu-02.png +0 -0
  218. package/product_showcase/aitu-03.png +0 -0
  219. package/product_showcase/aitu-04.png +0 -0
  220. package/product_showcase/aitu-05.png +0 -0
  221. package/product_showcase/aitu-06.png +0 -0
  222. package/product_showcase/case-1.png +0 -0
  223. package/product_showcase/case-2.png +0 -0
  224. package/robots.txt +13 -0
  225. package/sitemap.xml +29 -0
  226. package/sw-debug/app.js +3069 -0
  227. package/sw-debug/console-entry.js +80 -0
  228. package/sw-debug/log-entry.js +452 -0
  229. package/sw-debug/log-panel.js +309 -0
  230. package/sw-debug/postmessage-entry.js +117 -0
  231. package/sw-debug/status-panel.js +125 -0
  232. package/sw-debug/styles.css +2103 -0
  233. package/sw-debug/sw-communication.js +208 -0
  234. package/sw-debug/utils.js +112 -0
  235. package/sw-debug.html +685 -0
  236. package/sw.js +58 -0
  237. package/version.json +10 -0
@@ -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, '&amp;')
2211
+ .replace(/</g, '&lt;')
2212
+ .replace(/>/g, '&gt;')
2213
+ .replace(/"/g, '&quot;')
2214
+ .replace(/'/g, '&#039;');
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();