@mseep/anything-analyzer 3.6.50

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 (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,202 @@
1
+ import React, { useMemo, useState } from 'react'
2
+ import { Tag, Empty } from '../ui'
3
+ import type { CapturedRequest, JsHookRecord } from '@shared/types'
4
+ import styles from './RequestDetail.module.css'
5
+
6
+ interface RequestDetailProps {
7
+ request: CapturedRequest | null
8
+ hooks: JsHookRecord[]
9
+ }
10
+
11
+ function safeParse(json: string | null): Record<string, string> | null {
12
+ if (!json) return null
13
+ try { return JSON.parse(json) } catch { return null }
14
+ }
15
+
16
+ function formatContent(content: string | null, contentType?: string | null): string {
17
+ if (!content) return '(empty)'
18
+ const isJson = contentType?.includes('application/json') || content.trimStart().startsWith('{') || content.trimStart().startsWith('[')
19
+ if (isJson) {
20
+ try { return JSON.stringify(JSON.parse(content), null, 2) } catch { return content }
21
+ }
22
+ return content
23
+ }
24
+
25
+ function extractPath(url: string): string {
26
+ try {
27
+ const parsed = new URL(url)
28
+ return parsed.pathname + parsed.search
29
+ } catch {
30
+ return url
31
+ }
32
+ }
33
+
34
+ const METHOD_COLORS: Record<string, string> = {
35
+ GET: 'var(--color-success)',
36
+ POST: 'var(--color-info)',
37
+ PUT: 'var(--color-orange)',
38
+ DELETE: 'var(--color-error)',
39
+ PATCH: 'var(--color-info)',
40
+ }
41
+
42
+ const HOOK_TYPE_COLORS: Record<string, 'info' | 'success' | 'purple' | 'orange'> = {
43
+ fetch: 'info',
44
+ xhr: 'success',
45
+ crypto: 'purple',
46
+ crypto_lib: 'purple',
47
+ cookie_set: 'orange',
48
+ }
49
+
50
+ // Sensitive header value highlighting
51
+ const HIGHLIGHT_HEADERS: Record<string, string> = {
52
+ authorization: 'var(--color-warning)',
53
+ 'x-ss-token': 'var(--color-purple)',
54
+ 'x-csrf-token': 'var(--color-warning)',
55
+ cookie: 'var(--color-orange)',
56
+ 'set-cookie': 'var(--color-orange)',
57
+ }
58
+
59
+ type DetailTab = 'headers' | 'body' | 'response' | 'hooks'
60
+
61
+ // KV row component for headers
62
+ const KVRow: React.FC<{ k: string; v: string }> = ({ k, v }) => {
63
+ const highlight = HIGHLIGHT_HEADERS[k.toLowerCase()]
64
+ return (
65
+ <div className={styles.kvRow}>
66
+ <div className={styles.kvKey}>{k}</div>
67
+ <div className={styles.kvVal} style={highlight ? { color: highlight } : undefined}>{v}</div>
68
+ </div>
69
+ )
70
+ }
71
+
72
+ // Headers tab — KV pairs layout
73
+ const HeadersTab: React.FC<{ request: CapturedRequest }> = ({ request }) => {
74
+ const reqHeaders = safeParse(request.request_headers)
75
+ const resHeaders = safeParse(request.response_headers)
76
+ return (
77
+ <div className={styles.kvSection}>
78
+ <div className={styles.sectionLabel}>REQUEST HEADERS</div>
79
+ {reqHeaders ? (
80
+ Object.entries(reqHeaders).map(([k, v]) => <KVRow key={k} k={k} v={String(v)} />)
81
+ ) : (
82
+ <div className={styles.emptyHint}>(none)</div>
83
+ )}
84
+ <div className={styles.sectionLabel} style={{ marginTop: 14 }}>RESPONSE HEADERS</div>
85
+ {resHeaders ? (
86
+ Object.entries(resHeaders).map(([k, v]) => <KVRow key={k} k={k} v={String(v)} />)
87
+ ) : (
88
+ <div className={styles.emptyHint}>(none)</div>
89
+ )}
90
+ </div>
91
+ )
92
+ }
93
+
94
+ // Body tab
95
+ const BodyTab: React.FC<{ request: CapturedRequest }> = ({ request }) => (
96
+ <div className={styles.kvSection}>
97
+ <div className={styles.sectionLabel}>REQUEST BODY</div>
98
+ <pre className={styles.codeBlock}>{formatContent(request.request_body, request.content_type)}</pre>
99
+ </div>
100
+ )
101
+
102
+ // Response tab
103
+ const ResponseTab: React.FC<{ request: CapturedRequest }> = ({ request }) => (
104
+ <div className={styles.kvSection}>
105
+ <div className={styles.metaGrid}>
106
+ <div className={styles.metaLabel}>Status</div>
107
+ <div className={styles.metaValue}>{request.status_code ?? '--'}</div>
108
+ <div className={styles.metaLabel}>Content-Type</div>
109
+ <div className={styles.metaValue}>{request.content_type ?? '--'}</div>
110
+ <div className={styles.metaLabel}>Duration</div>
111
+ <div className={styles.metaValue}>{request.duration_ms != null ? `${request.duration_ms} ms` : '--'}</div>
112
+ </div>
113
+ <div className={styles.sectionLabel}>RESPONSE BODY</div>
114
+ <pre className={styles.codeBlock}>{formatContent(request.response_body, request.content_type)}</pre>
115
+ </div>
116
+ )
117
+
118
+ // Hooks tab
119
+ const HooksTab: React.FC<{ hooks: JsHookRecord[] }> = ({ hooks }) => {
120
+ if (hooks.length === 0) return <Empty description="No related hooks found" />
121
+ return (
122
+ <div className={styles.hookList}>
123
+ {hooks.map((hook) => (
124
+ <div key={hook.id} className={styles.hookItem}>
125
+ <div className={styles.hookHeader}>
126
+ <Tag color={HOOK_TYPE_COLORS[hook.hook_type] || 'default'}>{hook.hook_type}</Tag>
127
+ <code className={styles.hookFn}>{hook.function_name}</code>
128
+ <span className={styles.hookTime}>{new Date(hook.timestamp).toLocaleTimeString()}</span>
129
+ </div>
130
+ <div className={styles.hookArgs}>{hook.arguments}</div>
131
+ </div>
132
+ ))}
133
+ </div>
134
+ )
135
+ }
136
+
137
+ const TAB_LIST: { key: DetailTab; label: string }[] = [
138
+ { key: 'headers', label: 'Headers' },
139
+ { key: 'body', label: 'Body' },
140
+ { key: 'response', label: 'Response' },
141
+ { key: 'hooks', label: 'Hooks' },
142
+ ]
143
+
144
+ const RequestDetail: React.FC<RequestDetailProps> = ({ request, hooks }) => {
145
+ const [activeKey, setActiveKey] = useState<DetailTab>('headers')
146
+
147
+ const relatedHooks = useMemo(() => {
148
+ if (!request) return []
149
+ return hooks.filter((h) => {
150
+ if (h.related_request_id === request.id) return true
151
+ const timeDiff = request.timestamp - h.timestamp
152
+ return timeDiff >= 0 && timeDiff <= 500
153
+ })
154
+ }, [request, hooks])
155
+
156
+ if (!request) {
157
+ return (
158
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', minHeight: 200 }}>
159
+ <Empty description="Select a request to view details" />
160
+ </div>
161
+ )
162
+ }
163
+
164
+ const methodColor = METHOD_COLORS[request.method.toUpperCase()] || 'var(--text-muted)'
165
+
166
+ return (
167
+ <div className={styles.container}>
168
+ {/* Header: METHOD STATUS · TIME */}
169
+ <div className={styles.detailHeader}>
170
+ <div className={styles.detailTitle}>
171
+ <span style={{ color: methodColor, fontWeight: 600 }}>{request.method.toUpperCase()}</span>
172
+ {' '}
173
+ {request.status_code ?? '--'} · {request.duration_ms != null ? `${request.duration_ms}ms` : '--'}
174
+ </div>
175
+ <div className={styles.detailUrl}>{request.url}</div>
176
+ </div>
177
+
178
+ {/* Simple text tabs */}
179
+ <div className={styles.detailTabs}>
180
+ {TAB_LIST.map(tab => (
181
+ <button
182
+ key={tab.key}
183
+ className={`${styles.detailTab} ${activeKey === tab.key ? styles.detailTabActive : ''}`}
184
+ onClick={() => setActiveKey(tab.key)}
185
+ >
186
+ {tab.label}{tab.key === 'hooks' ? ` (${relatedHooks.length})` : ''}
187
+ </button>
188
+ ))}
189
+ </div>
190
+
191
+ {/* Tab content */}
192
+ <div className={styles.detailBody}>
193
+ {activeKey === 'headers' && <HeadersTab request={request} />}
194
+ {activeKey === 'body' && <BodyTab request={request} />}
195
+ {activeKey === 'response' && <ResponseTab request={request} />}
196
+ {activeKey === 'hooks' && <HooksTab hooks={relatedHooks} />}
197
+ </div>
198
+ </div>
199
+ )
200
+ }
201
+
202
+ export default RequestDetail
@@ -0,0 +1,69 @@
1
+ .container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ flex: 1;
5
+ min-height: 0;
6
+ overflow: hidden;
7
+ }
8
+
9
+ /* Search toolbar */
10
+ .toolbar {
11
+ height: 34px;
12
+ background: var(--color-frame);
13
+ border-bottom: 1px solid var(--color-border);
14
+ display: flex;
15
+ align-items: center;
16
+ padding: 0 12px;
17
+ gap: 6px;
18
+ flex-shrink: 0;
19
+ }
20
+
21
+ /* Search box */
22
+ .searchBox {
23
+ flex: 1;
24
+ height: 22px;
25
+ background: var(--color-surface);
26
+ border: 1px solid var(--color-border-subtle);
27
+ border-radius: var(--radius-badge);
28
+ display: flex;
29
+ align-items: center;
30
+ padding: 0 8px;
31
+ gap: 4px;
32
+ }
33
+
34
+ .searchIcon {
35
+ font-size: 10px;
36
+ color: var(--text-disabled);
37
+ flex-shrink: 0;
38
+ }
39
+
40
+ .searchInput {
41
+ flex: 1;
42
+ background: none;
43
+ border: none;
44
+ outline: none;
45
+ font-size: var(--font-size-3xs);
46
+ color: var(--text-secondary);
47
+ font-family: var(--font-sans);
48
+ }
49
+
50
+ .searchInput::placeholder {
51
+ color: var(--text-disabled);
52
+ }
53
+
54
+ /* Source badges */
55
+ .srcCdp {
56
+ font-size: 8px;
57
+ padding: 1px 4px;
58
+ border-radius: 3px;
59
+ background: var(--color-orange-bg);
60
+ color: var(--color-orange);
61
+ }
62
+
63
+ .srcProxy {
64
+ font-size: 8px;
65
+ padding: 1px 4px;
66
+ border-radius: 3px;
67
+ background: var(--color-purple-bg);
68
+ color: var(--color-purple);
69
+ }
@@ -0,0 +1,208 @@
1
+ import React, { useMemo, useCallback, useState } from 'react'
2
+ import { VirtualTable } from '../ui'
3
+ import type { VTColumn, VTRowSelection } from '../ui'
4
+ import type { CapturedRequest } from '@shared/types'
5
+ import styles from './RequestLog.module.css'
6
+
7
+ interface RequestLogProps {
8
+ requests: CapturedRequest[]
9
+ selectedId: string | null
10
+ onSelect: (request: CapturedRequest) => void
11
+ selectedSeqs: number[]
12
+ onSelectedSeqsChange: (seqs: number[]) => void
13
+ }
14
+
15
+ // Color mapping for HTTP methods
16
+ const METHOD_COLORS: Record<string, string> = {
17
+ GET: 'var(--color-success)',
18
+ POST: 'var(--color-info)',
19
+ PUT: 'var(--color-orange)',
20
+ DELETE: 'var(--color-error)',
21
+ PATCH: 'var(--color-info)',
22
+ HEAD: 'var(--text-muted)',
23
+ OPTIONS: 'var(--text-muted)',
24
+ }
25
+
26
+ // Color for status codes
27
+ function getStatusColor(code: number | null): string {
28
+ if (code === null) return 'var(--text-muted)'
29
+ if (code >= 200 && code < 300) return 'var(--color-success)'
30
+ if (code >= 300 && code < 400) return 'var(--color-warning)'
31
+ if (code >= 400 && code < 500) return 'var(--color-error)'
32
+ if (code >= 500) return 'var(--color-error)'
33
+ return 'var(--text-muted)'
34
+ }
35
+
36
+ function extractPath(url: string): string {
37
+ try {
38
+ const parsed = new URL(url)
39
+ return parsed.pathname + parsed.search
40
+ } catch {
41
+ return url
42
+ }
43
+ }
44
+
45
+ function extractHost(url: string): string {
46
+ try {
47
+ return new URL(url).host
48
+ } catch {
49
+ return url
50
+ }
51
+ }
52
+
53
+ const RequestLog: React.FC<RequestLogProps> = ({ requests, selectedId, onSelect, selectedSeqs, onSelectedSeqsChange }) => {
54
+ const [searchText, setSearchText] = useState('')
55
+
56
+ // Pre-filter by search text only (method filter now handled by VirtualTable column filter)
57
+ const filteredRequests = useMemo(() => {
58
+ if (!searchText.trim()) return requests
59
+ const q = searchText.trim().toLowerCase()
60
+ return requests.filter(r => r.url.toLowerCase().includes(q))
61
+ }, [requests, searchText])
62
+
63
+ // Collect unique methods for column filter
64
+ const methodFilters = useMemo(() => {
65
+ const methods = new Set(requests.map(r => r.method.toUpperCase()))
66
+ return Array.from(methods).sort().map(m => ({ text: m, value: m }))
67
+ }, [requests])
68
+
69
+ // Collect unique domains for column filter
70
+ const domainFilters = useMemo(() => {
71
+ const domains = new Set(requests.map(r => extractHost(r.url)))
72
+ return Array.from(domains).sort().map(d => ({ text: d, value: d }))
73
+ }, [requests])
74
+
75
+ // Collect unique sources for column filter
76
+ const sourceFilters = useMemo(() => [
77
+ { text: 'CDP', value: 'cdp' },
78
+ { text: 'Proxy', value: 'proxy' },
79
+ ], [])
80
+
81
+ const columns: VTColumn<CapturedRequest>[] = useMemo(() => [
82
+ {
83
+ key: 'sequence',
84
+ title: '#',
85
+ dataIndex: 'sequence',
86
+ width: 50,
87
+ render: (val) => <span style={{ color: 'var(--text-muted)', fontVariantNumeric: 'tabular-nums' }}>{val as number}</span>,
88
+ sorter: (a, b) => a.sequence - b.sequence,
89
+ },
90
+ {
91
+ key: 'method',
92
+ title: 'Method',
93
+ dataIndex: 'method',
94
+ width: 100,
95
+ filters: methodFilters,
96
+ onFilter: (value, record) => record.method.toUpperCase() === value,
97
+ render: (val) => {
98
+ const m = (val as string).toUpperCase()
99
+ return <span style={{ color: METHOD_COLORS[m] || 'var(--text-muted)', fontWeight: 600 }}>{m}</span>
100
+ },
101
+ },
102
+ {
103
+ key: 'domain',
104
+ title: 'Domain',
105
+ dataIndex: 'url',
106
+ width: 180,
107
+ filters: domainFilters,
108
+ filterSearch: true,
109
+ onFilter: (value, record) => extractHost(record.url) === value,
110
+ render: (_val, record) => (
111
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={extractHost(record.url)}>
112
+ {extractHost(record.url)}
113
+ </span>
114
+ ),
115
+ },
116
+ {
117
+ key: 'url',
118
+ title: 'Path',
119
+ dataIndex: 'url',
120
+ render: (_val, record) => (
121
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={record.url}>
122
+ {extractPath(record.url)}
123
+ </span>
124
+ ),
125
+ },
126
+ {
127
+ key: 'status_code',
128
+ title: 'Status',
129
+ dataIndex: 'status_code',
130
+ width: 70,
131
+ render: (val) => {
132
+ const code = val as number | null
133
+ return <span style={{ color: getStatusColor(code), fontWeight: 500 }}>{code ?? '--'}</span>
134
+ },
135
+ sorter: (a, b) => (a.status_code ?? 0) - (b.status_code ?? 0),
136
+ },
137
+ {
138
+ key: 'duration_ms',
139
+ title: 'Time',
140
+ dataIndex: 'duration_ms',
141
+ width: 80,
142
+ render: (val) => {
143
+ const ms = val as number | null
144
+ return <span style={{ color: 'var(--text-muted)' }}>{ms !== null ? `${ms}ms` : '--'}</span>
145
+ },
146
+ sorter: (a, b) => (a.duration_ms ?? 0) - (b.duration_ms ?? 0),
147
+ },
148
+ {
149
+ key: 'source',
150
+ title: 'Source',
151
+ dataIndex: 'source',
152
+ width: 90,
153
+ filters: sourceFilters,
154
+ onFilter: (value, record) => (record.source || 'cdp') === value,
155
+ render: (val) => {
156
+ const src = (val as string) || 'cdp'
157
+ const isProxy = src === 'proxy'
158
+ return (
159
+ <span className={isProxy ? styles.srcProxy : styles.srcCdp}>
160
+ {isProxy ? 'Proxy' : 'CDP'}
161
+ </span>
162
+ )
163
+ },
164
+ },
165
+ ], [methodFilters, domainFilters, sourceFilters])
166
+
167
+ const handleRow = useCallback((record: CapturedRequest) => ({
168
+ onClick: () => onSelect(record),
169
+ className: record.id === selectedId ? 'vtRowHighlight' : '',
170
+ }), [selectedId, onSelect])
171
+
172
+ const rowSelection: VTRowSelection<CapturedRequest> = useMemo(() => ({
173
+ selectedKeys: selectedSeqs,
174
+ onChange: (_keys, rows) => {
175
+ onSelectedSeqsChange(rows.map(r => r.sequence))
176
+ },
177
+ }), [selectedSeqs, onSelectedSeqsChange])
178
+
179
+ return (
180
+ <div className={styles.container}>
181
+ {/* Search toolbar */}
182
+ <div className={styles.toolbar}>
183
+ <div className={styles.searchBox}>
184
+ <span className={styles.searchIcon}>🔍</span>
185
+ <input
186
+ className={styles.searchInput}
187
+ value={searchText}
188
+ onChange={e => setSearchText(e.target.value)}
189
+ placeholder="搜索 URL..."
190
+ />
191
+ </div>
192
+ </div>
193
+
194
+ {/* Request list with column headers and filters */}
195
+ <VirtualTable<CapturedRequest>
196
+ columns={columns}
197
+ data={filteredRequests}
198
+ rowKey="sequence"
199
+ rowHeight={32}
200
+ rowSelection={rowSelection}
201
+ onRow={handleRow}
202
+ emptyText="No requests captured yet"
203
+ />
204
+ </div>
205
+ )
206
+ }
207
+
208
+ export default RequestLog