@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,233 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
import type { WebContents } from 'electron'
|
|
3
|
+
|
|
4
|
+
const MAX_BODY_SIZE = 1024 * 1024 // 1MB
|
|
5
|
+
|
|
6
|
+
// Binary content types that should not have their body stored
|
|
7
|
+
const BINARY_CONTENT_TYPES = [
|
|
8
|
+
'image/', 'font/', 'audio/', 'video/',
|
|
9
|
+
'application/octet-stream', 'application/pdf', 'application/zip'
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
interface RequestInfo {
|
|
13
|
+
method: string
|
|
14
|
+
url: string
|
|
15
|
+
headers: Record<string, string>
|
|
16
|
+
postData: string | null
|
|
17
|
+
timestamp: number
|
|
18
|
+
initiator: unknown
|
|
19
|
+
isOptions: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* CdpManager — Chrome DevTools Protocol manager for network interception.
|
|
24
|
+
* Attaches to a WebContents debugger and intercepts all Fetch/XHR requests.
|
|
25
|
+
*/
|
|
26
|
+
export class CdpManager extends EventEmitter {
|
|
27
|
+
private webContents: WebContents | null = null
|
|
28
|
+
private pendingRequests = new Map<string, RequestInfo>()
|
|
29
|
+
private running = false
|
|
30
|
+
private messageHandler: ((event: Electron.Event, method: string, params: Record<string, unknown>) => void) | null = null
|
|
31
|
+
private detachedHandler: (() => void) | null = null
|
|
32
|
+
|
|
33
|
+
async start(webContents: WebContents): Promise<void> {
|
|
34
|
+
this.webContents = webContents
|
|
35
|
+
|
|
36
|
+
if (webContents.isDestroyed()) {
|
|
37
|
+
throw new Error('Cannot start CDP on destroyed WebContents')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Detach if already attached (e.g. leftover from a previous session)
|
|
41
|
+
if (webContents.debugger.isAttached()) {
|
|
42
|
+
try { webContents.debugger.detach() } catch { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
webContents.debugger.attach('1.3')
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new Error(`Failed to attach CDP debugger: ${(err as Error).message}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.messageHandler = (_event, method, params) => {
|
|
52
|
+
this.handleCdpMessage(method, params)
|
|
53
|
+
}
|
|
54
|
+
this.detachedHandler = () => {
|
|
55
|
+
this.running = false
|
|
56
|
+
this.emit('detached')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
webContents.debugger.on('message', this.messageHandler)
|
|
60
|
+
webContents.debugger.on('detach', this.detachedHandler)
|
|
61
|
+
|
|
62
|
+
await Promise.all([
|
|
63
|
+
this.send('Fetch.enable', {
|
|
64
|
+
patterns: [
|
|
65
|
+
{ urlPattern: '*', requestStage: 'Request' },
|
|
66
|
+
{ urlPattern: '*', requestStage: 'Response' }
|
|
67
|
+
]
|
|
68
|
+
}),
|
|
69
|
+
this.send('Network.enable', {}),
|
|
70
|
+
this.send('Page.enable', {})
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
this.running = true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async stop(): Promise<void> {
|
|
77
|
+
if (!this.running || !this.webContents) return
|
|
78
|
+
this.running = false
|
|
79
|
+
if (this.webContents.isDestroyed()) return
|
|
80
|
+
try {
|
|
81
|
+
await this.send('Fetch.disable', {})
|
|
82
|
+
} catch { /* ignore */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
detach(): void {
|
|
86
|
+
if (!this.webContents) return
|
|
87
|
+
if (!this.webContents.isDestroyed()) {
|
|
88
|
+
if (this.messageHandler) {
|
|
89
|
+
this.webContents.debugger.removeListener('message', this.messageHandler)
|
|
90
|
+
}
|
|
91
|
+
if (this.detachedHandler) {
|
|
92
|
+
this.webContents.debugger.removeListener('detach', this.detachedHandler)
|
|
93
|
+
}
|
|
94
|
+
try { this.webContents.debugger.detach() } catch { /* already detached */ }
|
|
95
|
+
}
|
|
96
|
+
this.pendingRequests.clear()
|
|
97
|
+
this.webContents = null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Send a raw CDP command. Exposed for advanced use cases (e.g. stealth injection).
|
|
102
|
+
*/
|
|
103
|
+
async sendCommand(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
104
|
+
if (!this.webContents || this.webContents.isDestroyed()) throw new Error('No WebContents attached')
|
|
105
|
+
return this.webContents.debugger.sendCommand(method, params) as Promise<Record<string, unknown>>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async send(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
109
|
+
return this.sendCommand(method, params)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private handleCdpMessage(method: string, params: Record<string, unknown>): void {
|
|
113
|
+
switch (method) {
|
|
114
|
+
case 'Fetch.requestPaused':
|
|
115
|
+
this.handleRequestPaused(params)
|
|
116
|
+
break
|
|
117
|
+
case 'Network.webSocketFrameSent':
|
|
118
|
+
this.emit('websocket-frame', { direction: 'sent', ...params })
|
|
119
|
+
break
|
|
120
|
+
case 'Network.webSocketFrameReceived':
|
|
121
|
+
this.emit('websocket-frame', { direction: 'received', ...params })
|
|
122
|
+
break
|
|
123
|
+
case 'Network.webSocketCreated':
|
|
124
|
+
this.emit('websocket-created', params)
|
|
125
|
+
break
|
|
126
|
+
case 'Network.webSocketClosed':
|
|
127
|
+
this.emit('websocket-closed', params)
|
|
128
|
+
break
|
|
129
|
+
case 'Page.frameNavigated':
|
|
130
|
+
this.emit('frame-navigated', params)
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async handleRequestPaused(params: Record<string, unknown>): Promise<void> {
|
|
136
|
+
const requestId = params.requestId as string
|
|
137
|
+
const responseStatusCode = params.responseStatusCode as number | undefined
|
|
138
|
+
|
|
139
|
+
if (responseStatusCode === undefined) {
|
|
140
|
+
await this.handleRequestStage(requestId, params)
|
|
141
|
+
} else {
|
|
142
|
+
await this.handleResponseStage(requestId, params)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async handleRequestStage(requestId: string, params: Record<string, unknown>): Promise<void> {
|
|
147
|
+
const request = params.request as Record<string, unknown>
|
|
148
|
+
const method = (request.method as string) || 'GET'
|
|
149
|
+
const url = (request.url as string) || ''
|
|
150
|
+
const headers = (request.headers as Record<string, string>) || {}
|
|
151
|
+
const postData = (request.postData as string) || null
|
|
152
|
+
const isOptions = method.toUpperCase() === 'OPTIONS'
|
|
153
|
+
|
|
154
|
+
const info: RequestInfo = {
|
|
155
|
+
method, url, headers, postData,
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
initiator: params.initiator || null,
|
|
158
|
+
isOptions
|
|
159
|
+
}
|
|
160
|
+
this.pendingRequests.set(requestId, info)
|
|
161
|
+
|
|
162
|
+
this.emit('request-captured', {
|
|
163
|
+
requestId, method, url,
|
|
164
|
+
headers: JSON.stringify(headers),
|
|
165
|
+
body: postData,
|
|
166
|
+
timestamp: info.timestamp,
|
|
167
|
+
initiator: params.initiator ? JSON.stringify(params.initiator) : null,
|
|
168
|
+
isOptions
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
try { await this.send('Fetch.continueRequest', { requestId }) } catch { /* cancelled */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async handleResponseStage(requestId: string, params: Record<string, unknown>): Promise<void> {
|
|
175
|
+
const requestInfo = this.pendingRequests.get(requestId)
|
|
176
|
+
const statusCode = params.responseStatusCode as number
|
|
177
|
+
const responseHeaders = (params.responseHeaders as Array<{ name: string; value: string }>) || []
|
|
178
|
+
|
|
179
|
+
const headersObj: Record<string, string> = {}
|
|
180
|
+
for (const h of responseHeaders) {
|
|
181
|
+
headersObj[h.name.toLowerCase()] = h.value
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const contentType = headersObj['content-type'] || null
|
|
185
|
+
const isBinary = contentType ? BINARY_CONTENT_TYPES.some(t => contentType.includes(t)) : false
|
|
186
|
+
|
|
187
|
+
let responseBody: string | null = null
|
|
188
|
+
let truncated = false
|
|
189
|
+
|
|
190
|
+
if (!isBinary) {
|
|
191
|
+
try {
|
|
192
|
+
const bodyResult = await this.send('Fetch.getResponseBody', { requestId })
|
|
193
|
+
const body = bodyResult.body as string
|
|
194
|
+
const base64Encoded = bodyResult.base64Encoded as boolean
|
|
195
|
+
responseBody = base64Encoded ? Buffer.from(body, 'base64').toString('utf-8') : body
|
|
196
|
+
|
|
197
|
+
if (responseBody && responseBody.length > MAX_BODY_SIZE) {
|
|
198
|
+
responseBody = responseBody.substring(0, MAX_BODY_SIZE) + '\n[TRUNCATED]'
|
|
199
|
+
truncated = true
|
|
200
|
+
}
|
|
201
|
+
} catch { responseBody = null }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const durationMs = requestInfo ? Date.now() - requestInfo.timestamp : null
|
|
205
|
+
const STATIC_EXTENSIONS = /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|ico|map)(\?|$)/i
|
|
206
|
+
|
|
207
|
+
const isStreaming = contentType ? contentType.includes('text/event-stream') : false
|
|
208
|
+
const isWebSocket = requestInfo
|
|
209
|
+
? Object.entries(requestInfo.headers).some(([key, value]) =>
|
|
210
|
+
key.toLowerCase() === 'upgrade' && value.toLowerCase() === 'websocket')
|
|
211
|
+
: false
|
|
212
|
+
|
|
213
|
+
this.emit('response-captured', {
|
|
214
|
+
requestId,
|
|
215
|
+
method: requestInfo?.method || 'UNKNOWN',
|
|
216
|
+
url: requestInfo?.url || '',
|
|
217
|
+
requestHeaders: requestInfo ? JSON.stringify(requestInfo.headers) : '{}',
|
|
218
|
+
requestBody: requestInfo?.postData || null,
|
|
219
|
+
statusCode, responseHeaders: JSON.stringify(headersObj),
|
|
220
|
+
responseBody, contentType,
|
|
221
|
+
initiator: requestInfo?.initiator ? JSON.stringify(requestInfo.initiator) : null,
|
|
222
|
+
durationMs,
|
|
223
|
+
isOptions: requestInfo?.isOptions || false,
|
|
224
|
+
isStatic: requestInfo ? STATIC_EXTENSIONS.test(requestInfo.url) : false,
|
|
225
|
+
isStreaming,
|
|
226
|
+
isWebSocket,
|
|
227
|
+
truncated, timestamp: requestInfo?.timestamp || Date.now()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
this.pendingRequests.delete(requestId)
|
|
231
|
+
try { await this.send('Fetch.continueResponse', { requestId }) } catch { /* cancelled */ }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Database from 'better-sqlite3'
|
|
2
|
+
import { app } from 'electron'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { existsSync, mkdirSync } from 'fs'
|
|
5
|
+
|
|
6
|
+
let db: Database.Database | null = null
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get or initialize the SQLite database connection.
|
|
10
|
+
* Database file is stored in the app's user data directory.
|
|
11
|
+
*/
|
|
12
|
+
export function getDatabase(): Database.Database {
|
|
13
|
+
if (db) return db
|
|
14
|
+
|
|
15
|
+
const userDataPath = app.getPath('userData')
|
|
16
|
+
const dbDir = join(userDataPath, 'data')
|
|
17
|
+
|
|
18
|
+
if (!existsSync(dbDir)) {
|
|
19
|
+
mkdirSync(dbDir, { recursive: true })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const dbPath = join(dbDir, 'anything-register.db')
|
|
23
|
+
|
|
24
|
+
db = new Database(dbPath)
|
|
25
|
+
|
|
26
|
+
// Enable WAL mode for better concurrent read performance
|
|
27
|
+
db.pragma('journal_mode = WAL')
|
|
28
|
+
db.pragma('foreign_keys = ON')
|
|
29
|
+
|
|
30
|
+
return db
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Close the database connection (called on app quit).
|
|
35
|
+
*/
|
|
36
|
+
export function closeDatabase(): void {
|
|
37
|
+
if (db) {
|
|
38
|
+
db.close()
|
|
39
|
+
db = null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration 005: Add streaming and WebSocket flags to requests table
|
|
5
|
+
* Safe to call multiple times (handles duplicate column errors)
|
|
6
|
+
*/
|
|
7
|
+
export function migrateAddStreamingAndWebSocketFlags(db: Database.Database): void {
|
|
8
|
+
const migrations = [
|
|
9
|
+
`ALTER TABLE requests ADD COLUMN is_streaming INTEGER DEFAULT 0`,
|
|
10
|
+
`ALTER TABLE requests ADD COLUMN is_websocket INTEGER DEFAULT 0`,
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
for (const migration of migrations) {
|
|
14
|
+
try {
|
|
15
|
+
db.exec(migration)
|
|
16
|
+
} catch (err) {
|
|
17
|
+
// Ignore error if column already exists
|
|
18
|
+
if (!String(err).includes('duplicate column name')) {
|
|
19
|
+
throw err
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run all database migrations — create tables and indexes.
|
|
27
|
+
* Safe to call multiple times (uses IF NOT EXISTS).
|
|
28
|
+
*/
|
|
29
|
+
export function runMigrations(db: Database.Database): void {
|
|
30
|
+
db.exec(`
|
|
31
|
+
-- Sessions table
|
|
32
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
name TEXT,
|
|
35
|
+
target_url TEXT,
|
|
36
|
+
status TEXT NOT NULL DEFAULT 'stopped',
|
|
37
|
+
created_at INTEGER NOT NULL,
|
|
38
|
+
stopped_at INTEGER
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
-- HTTP request records
|
|
42
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
45
|
+
sequence INTEGER NOT NULL,
|
|
46
|
+
timestamp INTEGER NOT NULL,
|
|
47
|
+
method TEXT NOT NULL,
|
|
48
|
+
url TEXT NOT NULL,
|
|
49
|
+
request_headers TEXT,
|
|
50
|
+
request_body TEXT,
|
|
51
|
+
status_code INTEGER,
|
|
52
|
+
response_headers TEXT,
|
|
53
|
+
response_body TEXT,
|
|
54
|
+
content_type TEXT,
|
|
55
|
+
initiator TEXT,
|
|
56
|
+
duration_ms INTEGER
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
-- JS Hook capture records
|
|
60
|
+
CREATE TABLE IF NOT EXISTS js_hooks (
|
|
61
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
63
|
+
timestamp INTEGER NOT NULL,
|
|
64
|
+
hook_type TEXT NOT NULL,
|
|
65
|
+
function_name TEXT NOT NULL,
|
|
66
|
+
arguments TEXT,
|
|
67
|
+
result TEXT,
|
|
68
|
+
call_stack TEXT,
|
|
69
|
+
related_request_id TEXT
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- Storage snapshots
|
|
73
|
+
CREATE TABLE IF NOT EXISTS storage_snapshots (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
76
|
+
timestamp INTEGER NOT NULL,
|
|
77
|
+
domain TEXT NOT NULL,
|
|
78
|
+
storage_type TEXT NOT NULL,
|
|
79
|
+
data TEXT
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
-- AI analysis reports
|
|
83
|
+
CREATE TABLE IF NOT EXISTS analysis_reports (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
86
|
+
created_at INTEGER NOT NULL,
|
|
87
|
+
llm_provider TEXT NOT NULL,
|
|
88
|
+
llm_model TEXT NOT NULL,
|
|
89
|
+
prompt_tokens INTEGER,
|
|
90
|
+
completion_tokens INTEGER,
|
|
91
|
+
report_content TEXT
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
-- Fingerprint profiles (one per session)
|
|
95
|
+
CREATE TABLE IF NOT EXISTS fingerprint_profiles (
|
|
96
|
+
session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
|
|
97
|
+
profile_json TEXT NOT NULL
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
-- Indexes
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id, sequence);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_requests_url ON requests(url);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_js_hooks_session ON js_hooks(session_id, timestamp);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_storage_session ON storage_snapshots(session_id, timestamp);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_reports_session ON analysis_reports(session_id);
|
|
106
|
+
`)
|
|
107
|
+
|
|
108
|
+
// Run additional migrations
|
|
109
|
+
migrateAddStreamingAndWebSocketFlags(db)
|
|
110
|
+
migrateAddFilterTokenColumns(db)
|
|
111
|
+
migrateAddSourceColumn(db)
|
|
112
|
+
migrateAddChatMessagesTable(db)
|
|
113
|
+
migrateAddAiRequestLogsTable(db)
|
|
114
|
+
migrateAddInteractionEventsTable(db)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Migration 008: Add chat_messages table for persisting follow-up Q&A per report
|
|
119
|
+
* Safe to call multiple times (uses IF NOT EXISTS).
|
|
120
|
+
*/
|
|
121
|
+
export function migrateAddChatMessagesTable(db: Database.Database): void {
|
|
122
|
+
db.exec(`
|
|
123
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
report_id TEXT NOT NULL REFERENCES analysis_reports(id) ON DELETE CASCADE,
|
|
126
|
+
role TEXT NOT NULL,
|
|
127
|
+
content TEXT NOT NULL,
|
|
128
|
+
created_at INTEGER NOT NULL
|
|
129
|
+
);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_report ON chat_messages(report_id, id);
|
|
131
|
+
`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Migration 007: Add source column to requests table for MITM proxy support
|
|
136
|
+
* Safe to call multiple times (handles duplicate column errors)
|
|
137
|
+
*/
|
|
138
|
+
export function migrateAddSourceColumn(db: Database.Database): void {
|
|
139
|
+
try {
|
|
140
|
+
db.exec(`ALTER TABLE requests ADD COLUMN source TEXT DEFAULT 'cdp'`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (!String(err).includes("duplicate column name")) throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Migration 006: Add Phase 1 filter token columns to analysis_reports
|
|
148
|
+
* Safe to call multiple times (handles duplicate column errors)
|
|
149
|
+
*/
|
|
150
|
+
export function migrateAddFilterTokenColumns(db: Database.Database): void {
|
|
151
|
+
const migrations = [
|
|
152
|
+
`ALTER TABLE analysis_reports ADD COLUMN filter_prompt_tokens INTEGER`,
|
|
153
|
+
`ALTER TABLE analysis_reports ADD COLUMN filter_completion_tokens INTEGER`,
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
for (const migration of migrations) {
|
|
157
|
+
try {
|
|
158
|
+
db.exec(migration)
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (!String(err).includes('duplicate column name')) {
|
|
161
|
+
throw err
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Migration 009: Add ai_request_logs table for recording LLM HTTP requests
|
|
169
|
+
* Safe to call multiple times (uses IF NOT EXISTS).
|
|
170
|
+
*/
|
|
171
|
+
export function migrateAddAiRequestLogsTable(db: Database.Database): void {
|
|
172
|
+
db.exec(`
|
|
173
|
+
CREATE TABLE IF NOT EXISTS ai_request_logs (
|
|
174
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
175
|
+
session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
|
|
176
|
+
report_id TEXT REFERENCES analysis_reports(id) ON DELETE SET NULL,
|
|
177
|
+
type TEXT NOT NULL,
|
|
178
|
+
provider TEXT NOT NULL,
|
|
179
|
+
model TEXT NOT NULL,
|
|
180
|
+
request_url TEXT NOT NULL,
|
|
181
|
+
request_method TEXT NOT NULL DEFAULT 'POST',
|
|
182
|
+
request_headers TEXT NOT NULL,
|
|
183
|
+
request_body TEXT NOT NULL,
|
|
184
|
+
status_code INTEGER,
|
|
185
|
+
response_headers TEXT,
|
|
186
|
+
response_body TEXT,
|
|
187
|
+
prompt_tokens INTEGER DEFAULT 0,
|
|
188
|
+
completion_tokens INTEGER DEFAULT 0,
|
|
189
|
+
duration_ms INTEGER,
|
|
190
|
+
error TEXT,
|
|
191
|
+
created_at INTEGER NOT NULL
|
|
192
|
+
);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_ai_logs_session ON ai_request_logs(session_id);
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_ai_logs_created ON ai_request_logs(created_at);
|
|
195
|
+
`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Migration 010: Add interaction_events table for recording user interactions
|
|
200
|
+
* Safe to call multiple times (uses IF NOT EXISTS).
|
|
201
|
+
*/
|
|
202
|
+
export function migrateAddInteractionEventsTable(db: Database.Database): void {
|
|
203
|
+
db.exec(`
|
|
204
|
+
CREATE TABLE IF NOT EXISTS interaction_events (
|
|
205
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
206
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
207
|
+
sequence INTEGER NOT NULL,
|
|
208
|
+
type TEXT NOT NULL,
|
|
209
|
+
timestamp INTEGER NOT NULL,
|
|
210
|
+
x REAL,
|
|
211
|
+
y REAL,
|
|
212
|
+
viewport_x REAL,
|
|
213
|
+
viewport_y REAL,
|
|
214
|
+
selector TEXT,
|
|
215
|
+
xpath TEXT,
|
|
216
|
+
tag_name TEXT,
|
|
217
|
+
element_text TEXT,
|
|
218
|
+
attributes TEXT,
|
|
219
|
+
bounding_rect TEXT,
|
|
220
|
+
input_value TEXT,
|
|
221
|
+
key TEXT,
|
|
222
|
+
scroll_x REAL,
|
|
223
|
+
scroll_y REAL,
|
|
224
|
+
scroll_dx REAL,
|
|
225
|
+
scroll_dy REAL,
|
|
226
|
+
url TEXT NOT NULL,
|
|
227
|
+
page_title TEXT,
|
|
228
|
+
path TEXT,
|
|
229
|
+
created_at INTEGER NOT NULL
|
|
230
|
+
);
|
|
231
|
+
CREATE INDEX IF NOT EXISTS idx_interactions_session ON interaction_events(session_id);
|
|
232
|
+
CREATE INDEX IF NOT EXISTS idx_interactions_session_seq ON interaction_events(session_id, sequence);
|
|
233
|
+
CREATE INDEX IF NOT EXISTS idx_interactions_type ON interaction_events(session_id, type);
|
|
234
|
+
`)
|
|
235
|
+
}
|