@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.
- package/.codeartsdoer/.codebaseignore +0 -0
- package/.codeartsdoer/AGENTS.md +12 -0
- package/.github/workflows/build.yml +146 -0
- package/README.en.md +264 -0
- package/README.md +276 -0
- package/RELEASE_NOTES.md +16 -0
- package/USAGE.md +490 -0
- package/color-preview-r3.html +414 -0
- package/color-preview.html +414 -0
- package/dev-app-update.yml +3 -0
- package/electron-builder.yml +36 -0
- package/electron.vite.config.ts +40 -0
- package/package.json +53 -0
- package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
- package/resources/doloffer-logo.png +0 -0
- package/resources/entitlements.mac.plist +12 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/src/main/ai/ai-analyzer.ts +517 -0
- package/src/main/ai/crypto-script-extractor.ts +206 -0
- package/src/main/ai/data-assembler.ts +205 -0
- package/src/main/ai/llm-router.ts +1120 -0
- package/src/main/ai/prompt-builder.ts +349 -0
- package/src/main/ai/scene-detector.ts +302 -0
- package/src/main/capture/capture-engine.ts +130 -0
- package/src/main/capture/interaction-recorder.ts +171 -0
- package/src/main/capture/js-injector.ts +57 -0
- package/src/main/capture/replay-engine.ts +256 -0
- package/src/main/capture/storage-collector.ts +76 -0
- package/src/main/cdp/cdp-manager.ts +233 -0
- package/src/main/db/database.ts +41 -0
- package/src/main/db/migrations.ts +235 -0
- package/src/main/db/repositories.ts +574 -0
- package/src/main/fingerprint/http-spoofing.ts +48 -0
- package/src/main/fingerprint/presets.ts +173 -0
- package/src/main/fingerprint/profile-generator.ts +115 -0
- package/src/main/fingerprint/profile-store.ts +52 -0
- package/src/main/index.ts +260 -0
- package/src/main/ipc.ts +856 -0
- package/src/main/logger.ts +42 -0
- package/src/main/mcp/mcp-config.ts +66 -0
- package/src/main/mcp/mcp-manager.ts +155 -0
- package/src/main/mcp/mcp-server.ts +1038 -0
- package/src/main/prompt-templates.ts +170 -0
- package/src/main/proxy/ca-manager.ts +204 -0
- package/src/main/proxy/cert-download-page.ts +171 -0
- package/src/main/proxy/cert-installer.ts +242 -0
- package/src/main/proxy/mitm-proxy-config.ts +37 -0
- package/src/main/proxy/mitm-proxy-server.ts +1085 -0
- package/src/main/proxy/system-proxy.ts +248 -0
- package/src/main/session/session-manager.ts +724 -0
- package/src/main/tab-manager.ts +582 -0
- package/src/main/updater.ts +111 -0
- package/src/main/window.ts +235 -0
- package/src/preload/hook-script.ts +270 -0
- package/src/preload/index.ts +211 -0
- package/src/preload/interaction-hook.ts +286 -0
- package/src/preload/stealth-script.ts +302 -0
- package/src/preload/target-preload.ts +15 -0
- package/src/renderer/App.tsx +656 -0
- package/src/renderer/components/AiLogDetail.tsx +173 -0
- package/src/renderer/components/AiLogList.tsx +101 -0
- package/src/renderer/components/AiLogView.module.css +364 -0
- package/src/renderer/components/AiLogView.tsx +86 -0
- package/src/renderer/components/AnalyzeBar.module.css +79 -0
- package/src/renderer/components/AnalyzeBar.tsx +104 -0
- package/src/renderer/components/BrowserPanel.module.css +67 -0
- package/src/renderer/components/BrowserPanel.tsx +90 -0
- package/src/renderer/components/ControlBar.module.css +47 -0
- package/src/renderer/components/ControlBar.tsx +205 -0
- package/src/renderer/components/HookLog.tsx +132 -0
- package/src/renderer/components/InteractionLog.tsx +183 -0
- package/src/renderer/components/MCPServerModal.tsx +427 -0
- package/src/renderer/components/PromptTemplateModal.tsx +254 -0
- package/src/renderer/components/ReportView.module.css +413 -0
- package/src/renderer/components/ReportView.tsx +429 -0
- package/src/renderer/components/RequestDetail.module.css +191 -0
- package/src/renderer/components/RequestDetail.tsx +202 -0
- package/src/renderer/components/RequestLog.module.css +69 -0
- package/src/renderer/components/RequestLog.tsx +208 -0
- package/src/renderer/components/SessionList.module.css +245 -0
- package/src/renderer/components/SessionList.tsx +247 -0
- package/src/renderer/components/SettingsModal.tsx +100 -0
- package/src/renderer/components/StatusBar.module.css +44 -0
- package/src/renderer/components/StatusBar.tsx +102 -0
- package/src/renderer/components/StorageView.module.css +41 -0
- package/src/renderer/components/StorageView.tsx +178 -0
- package/src/renderer/components/TabBar.module.css +88 -0
- package/src/renderer/components/TabBar.tsx +70 -0
- package/src/renderer/components/Titlebar.module.css +254 -0
- package/src/renderer/components/Titlebar.tsx +169 -0
- package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
- package/src/renderer/components/settings/GeneralSection.tsx +164 -0
- package/src/renderer/components/settings/LLMSection.tsx +148 -0
- package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
- package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
- package/src/renderer/components/settings/ProxySection.tsx +110 -0
- package/src/renderer/css-modules.d.ts +4 -0
- package/src/renderer/hooks/useCapture.ts +383 -0
- package/src/renderer/hooks/useConfirm.tsx +91 -0
- package/src/renderer/hooks/useSession.ts +136 -0
- package/src/renderer/hooks/useTabs.ts +103 -0
- package/src/renderer/i18n/en.ts +167 -0
- package/src/renderer/i18n/index.ts +47 -0
- package/src/renderer/i18n/zh.ts +170 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +15 -0
- package/src/renderer/styles/global.css +144 -0
- package/src/renderer/styles/themes/ayu-dark.css +59 -0
- package/src/renderer/styles/themes/catppuccin.css +59 -0
- package/src/renderer/styles/themes/discord.css +59 -0
- package/src/renderer/styles/themes/dracula.css +59 -0
- package/src/renderer/styles/themes/github-dark.css +59 -0
- package/src/renderer/styles/themes/gruvbox.css +59 -0
- package/src/renderer/styles/themes/index.css +11 -0
- package/src/renderer/styles/themes/light.css +59 -0
- package/src/renderer/styles/themes/nord.css +59 -0
- package/src/renderer/styles/themes/one-dark.css +59 -0
- package/src/renderer/styles/themes/tokyo-night.css +59 -0
- package/src/renderer/styles/tokens.css +137 -0
- package/src/renderer/theme.ts +31 -0
- package/src/renderer/ui/Badge.module.css +38 -0
- package/src/renderer/ui/Badge.tsx +36 -0
- package/src/renderer/ui/Button.module.css +142 -0
- package/src/renderer/ui/Button.tsx +46 -0
- package/src/renderer/ui/Collapse.module.css +49 -0
- package/src/renderer/ui/Collapse.tsx +57 -0
- package/src/renderer/ui/CopyableBlock.module.css +56 -0
- package/src/renderer/ui/CopyableBlock.tsx +42 -0
- package/src/renderer/ui/Empty.module.css +19 -0
- package/src/renderer/ui/Empty.tsx +34 -0
- package/src/renderer/ui/Icons.tsx +346 -0
- package/src/renderer/ui/Input.module.css +103 -0
- package/src/renderer/ui/Input.tsx +94 -0
- package/src/renderer/ui/InputNumber.module.css +68 -0
- package/src/renderer/ui/InputNumber.tsx +104 -0
- package/src/renderer/ui/Modal.module.css +83 -0
- package/src/renderer/ui/Modal.tsx +67 -0
- package/src/renderer/ui/Popconfirm.module.css +73 -0
- package/src/renderer/ui/Popconfirm.tsx +74 -0
- package/src/renderer/ui/Progress.module.css +35 -0
- package/src/renderer/ui/Progress.tsx +30 -0
- package/src/renderer/ui/Select.module.css +91 -0
- package/src/renderer/ui/Select.tsx +100 -0
- package/src/renderer/ui/Spinner.module.css +44 -0
- package/src/renderer/ui/Spinner.tsx +27 -0
- package/src/renderer/ui/Switch.module.css +39 -0
- package/src/renderer/ui/Switch.tsx +43 -0
- package/src/renderer/ui/Tabs.module.css +76 -0
- package/src/renderer/ui/Tabs.tsx +53 -0
- package/src/renderer/ui/Tag.module.css +66 -0
- package/src/renderer/ui/Tag.tsx +47 -0
- package/src/renderer/ui/Timeline.module.css +42 -0
- package/src/renderer/ui/Timeline.tsx +29 -0
- package/src/renderer/ui/Toast.module.css +99 -0
- package/src/renderer/ui/Toast.tsx +90 -0
- package/src/renderer/ui/Tooltip.module.css +26 -0
- package/src/renderer/ui/Tooltip.tsx +23 -0
- package/src/renderer/ui/VirtualTable.module.css +230 -0
- package/src/renderer/ui/VirtualTable.tsx +416 -0
- package/src/renderer/ui/index.ts +55 -0
- package/src/shared/types.ts +695 -0
- package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
- package/tests/main/ai/llm-router.test.ts +1537 -0
- package/tests/main/ai/prompt-builder.test.ts +178 -0
- package/tests/main/ai/scene-detector.test.ts +212 -0
- package/tests/main/db/migrations.test.ts +134 -0
- package/tests/main/release-workflow.test.ts +59 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/tsconfig.web.json +24 -0
- 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
|