@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,574 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
import type {
|
|
3
|
+
Session,
|
|
4
|
+
CapturedRequest,
|
|
5
|
+
JsHookRecord,
|
|
6
|
+
StorageSnapshot,
|
|
7
|
+
AnalysisReport,
|
|
8
|
+
FingerprintProfile,
|
|
9
|
+
AiRequestLog,
|
|
10
|
+
InteractionEvent,
|
|
11
|
+
InteractionType,
|
|
12
|
+
} from '@shared/types'
|
|
13
|
+
|
|
14
|
+
// ============================================================
|
|
15
|
+
// Sessions Repository
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
export class SessionsRepo {
|
|
19
|
+
private stmts: {
|
|
20
|
+
insert: Database.Statement
|
|
21
|
+
findById: Database.Statement
|
|
22
|
+
findAll: Database.Statement
|
|
23
|
+
updateStatus: Database.Statement
|
|
24
|
+
delete: Database.Statement
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor(private db: Database.Database) {
|
|
28
|
+
this.stmts = {
|
|
29
|
+
insert: db.prepare(
|
|
30
|
+
`INSERT INTO sessions (id, name, target_url, status, created_at, stopped_at)
|
|
31
|
+
VALUES (@id, @name, @target_url, @status, @created_at, @stopped_at)`
|
|
32
|
+
),
|
|
33
|
+
findById: db.prepare('SELECT * FROM sessions WHERE id = ?'),
|
|
34
|
+
findAll: db.prepare('SELECT * FROM sessions ORDER BY created_at DESC'),
|
|
35
|
+
updateStatus: db.prepare(
|
|
36
|
+
'UPDATE sessions SET status = @status, stopped_at = @stopped_at WHERE id = @id'
|
|
37
|
+
),
|
|
38
|
+
delete: db.prepare('DELETE FROM sessions WHERE id = ?')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
insert(session: Session): void {
|
|
43
|
+
this.stmts.insert.run(session)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
findById(id: string): Session | undefined {
|
|
47
|
+
return this.stmts.findById.get(id) as Session | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
findAll(): Session[] {
|
|
51
|
+
return this.stmts.findAll.all() as Session[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
updateStatus(id: string, status: string, stoppedAt: number | null = null): void {
|
|
55
|
+
this.stmts.updateStatus.run({ id, status, stopped_at: stoppedAt })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
delete(id: string): void {
|
|
59
|
+
this.stmts.delete.run(id)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Requests Repository
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
export class RequestsRepo {
|
|
68
|
+
private stmts: {
|
|
69
|
+
insert: Database.Statement
|
|
70
|
+
updateResponse: Database.Statement
|
|
71
|
+
findBySession: Database.Statement
|
|
72
|
+
findById: Database.Statement
|
|
73
|
+
getNextSequence: Database.Statement
|
|
74
|
+
deleteBySession: Database.Statement
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
constructor(private db: Database.Database) {
|
|
78
|
+
this.stmts = {
|
|
79
|
+
insert: db.prepare(
|
|
80
|
+
`INSERT INTO requests (id, session_id, sequence, timestamp, method, url, request_headers, request_body, content_type, initiator, source)
|
|
81
|
+
VALUES (@id, @session_id, @sequence, @timestamp, @method, @url, @request_headers, @request_body, @content_type, @initiator, @source)`
|
|
82
|
+
),
|
|
83
|
+
updateResponse: db.prepare(
|
|
84
|
+
`UPDATE requests SET status_code = @status_code, response_headers = @response_headers,
|
|
85
|
+
response_body = @response_body, content_type = @content_type, duration_ms = @duration_ms,
|
|
86
|
+
is_streaming = @is_streaming, is_websocket = @is_websocket
|
|
87
|
+
WHERE id = @id`
|
|
88
|
+
),
|
|
89
|
+
findBySession: db.prepare(
|
|
90
|
+
'SELECT * FROM requests WHERE session_id = ? ORDER BY sequence ASC'
|
|
91
|
+
),
|
|
92
|
+
findById: db.prepare('SELECT * FROM requests WHERE id = ?'),
|
|
93
|
+
getNextSequence: db.prepare(
|
|
94
|
+
'SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM requests WHERE session_id = ?'
|
|
95
|
+
),
|
|
96
|
+
deleteBySession: db.prepare('DELETE FROM requests WHERE session_id = ?')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
insert(data: Partial<CapturedRequest> & { source?: string }): void {
|
|
101
|
+
this.stmts.insert.run({ ...data, source: data.source || 'cdp' })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
updateResponse(data: {
|
|
105
|
+
id: string
|
|
106
|
+
status_code: number
|
|
107
|
+
response_headers: string
|
|
108
|
+
response_body: string | null
|
|
109
|
+
content_type: string | null
|
|
110
|
+
duration_ms: number
|
|
111
|
+
is_streaming: number // 0 or 1
|
|
112
|
+
is_websocket: number // 0 or 1
|
|
113
|
+
}): void {
|
|
114
|
+
this.stmts.updateResponse.run(data)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
findBySession(sessionId: string): CapturedRequest[] {
|
|
118
|
+
return this.stmts.findBySession.all(sessionId) as CapturedRequest[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
findById(id: string): CapturedRequest | undefined {
|
|
122
|
+
return this.stmts.findById.get(id) as CapturedRequest | undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getNextSequence(sessionId: string): number {
|
|
126
|
+
const row = this.stmts.getNextSequence.get(sessionId) as { next_seq: number }
|
|
127
|
+
return row.next_seq
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
deleteBySession(sessionId: string): void {
|
|
131
|
+
this.stmts.deleteBySession.run(sessionId)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Find requests with dynamic filtering conditions.
|
|
136
|
+
*/
|
|
137
|
+
findBySessionFiltered(sessionId: string, filters: {
|
|
138
|
+
method?: string
|
|
139
|
+
domain?: string
|
|
140
|
+
statusCode?: number
|
|
141
|
+
statusRange?: string
|
|
142
|
+
contentType?: string
|
|
143
|
+
urlPattern?: string
|
|
144
|
+
limit?: number
|
|
145
|
+
}): CapturedRequest[] {
|
|
146
|
+
const conditions: string[] = ['session_id = ?']
|
|
147
|
+
const params: unknown[] = [sessionId]
|
|
148
|
+
|
|
149
|
+
if (filters.method) {
|
|
150
|
+
conditions.push('method = ?')
|
|
151
|
+
params.push(filters.method.toUpperCase())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (filters.domain) {
|
|
155
|
+
conditions.push('url LIKE ?')
|
|
156
|
+
params.push(`%${filters.domain}%`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (filters.statusCode != null) {
|
|
160
|
+
conditions.push('status_code = ?')
|
|
161
|
+
params.push(filters.statusCode)
|
|
162
|
+
} else if (filters.statusRange) {
|
|
163
|
+
const prefix = filters.statusRange.charAt(0)
|
|
164
|
+
if (/^[1-5]$/.test(prefix)) {
|
|
165
|
+
conditions.push('status_code >= ? AND status_code < ?')
|
|
166
|
+
params.push(Number(prefix) * 100, (Number(prefix) + 1) * 100)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (filters.contentType) {
|
|
171
|
+
conditions.push('content_type LIKE ?')
|
|
172
|
+
params.push(`%${filters.contentType}%`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (filters.urlPattern) {
|
|
176
|
+
conditions.push('url LIKE ?')
|
|
177
|
+
params.push(`%${filters.urlPattern}%`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const limit = filters.limit && filters.limit > 0 ? filters.limit : 50
|
|
181
|
+
const sql = `SELECT * FROM requests WHERE ${conditions.join(' AND ')} ORDER BY sequence ASC LIMIT ?`
|
|
182
|
+
params.push(limit)
|
|
183
|
+
|
|
184
|
+
return this.db.prepare(sql).all(...params) as CapturedRequest[]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================
|
|
189
|
+
// JS Hooks Repository
|
|
190
|
+
// ============================================================
|
|
191
|
+
|
|
192
|
+
export class JsHooksRepo {
|
|
193
|
+
private stmts: {
|
|
194
|
+
insert: Database.Statement
|
|
195
|
+
findBySession: Database.Statement
|
|
196
|
+
deleteBySession: Database.Statement
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
constructor(private db: Database.Database) {
|
|
200
|
+
this.stmts = {
|
|
201
|
+
insert: db.prepare(
|
|
202
|
+
`INSERT INTO js_hooks (session_id, timestamp, hook_type, function_name, arguments, result, call_stack, related_request_id)
|
|
203
|
+
VALUES (@session_id, @timestamp, @hook_type, @function_name, @arguments, @result, @call_stack, @related_request_id)`
|
|
204
|
+
),
|
|
205
|
+
findBySession: db.prepare(
|
|
206
|
+
'SELECT * FROM js_hooks WHERE session_id = ? ORDER BY timestamp ASC'
|
|
207
|
+
),
|
|
208
|
+
deleteBySession: db.prepare('DELETE FROM js_hooks WHERE session_id = ?')
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
insert(record: Omit<JsHookRecord, 'id'>): void {
|
|
213
|
+
this.stmts.insert.run(record)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
findBySession(sessionId: string): JsHookRecord[] {
|
|
217
|
+
return this.stmts.findBySession.all(sessionId) as JsHookRecord[]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
deleteBySession(sessionId: string): void {
|
|
221
|
+
this.stmts.deleteBySession.run(sessionId)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================================
|
|
226
|
+
// Storage Snapshots Repository
|
|
227
|
+
// ============================================================
|
|
228
|
+
|
|
229
|
+
export class StorageSnapshotsRepo {
|
|
230
|
+
private stmts: {
|
|
231
|
+
insert: Database.Statement
|
|
232
|
+
findBySession: Database.Statement
|
|
233
|
+
findLatest: Database.Statement
|
|
234
|
+
deleteBySession: Database.Statement
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
constructor(private db: Database.Database) {
|
|
238
|
+
this.stmts = {
|
|
239
|
+
insert: db.prepare(
|
|
240
|
+
`INSERT INTO storage_snapshots (session_id, timestamp, domain, storage_type, data)
|
|
241
|
+
VALUES (@session_id, @timestamp, @domain, @storage_type, @data)`
|
|
242
|
+
),
|
|
243
|
+
findBySession: db.prepare(
|
|
244
|
+
'SELECT * FROM storage_snapshots WHERE session_id = ? ORDER BY timestamp ASC'
|
|
245
|
+
),
|
|
246
|
+
findLatest: db.prepare(
|
|
247
|
+
`SELECT * FROM storage_snapshots
|
|
248
|
+
WHERE session_id = ? AND storage_type = ?
|
|
249
|
+
ORDER BY timestamp DESC LIMIT 1`
|
|
250
|
+
),
|
|
251
|
+
deleteBySession: db.prepare('DELETE FROM storage_snapshots WHERE session_id = ?')
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
insert(snapshot: Omit<StorageSnapshot, 'id'>): void {
|
|
256
|
+
this.stmts.insert.run(snapshot)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
findBySession(sessionId: string): StorageSnapshot[] {
|
|
260
|
+
return this.stmts.findBySession.all(sessionId) as StorageSnapshot[]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
findLatest(sessionId: string, storageType: string): StorageSnapshot | undefined {
|
|
264
|
+
return this.stmts.findLatest.get(sessionId, storageType) as StorageSnapshot | undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
deleteBySession(sessionId: string): void {
|
|
268
|
+
this.stmts.deleteBySession.run(sessionId)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ============================================================
|
|
273
|
+
// Analysis Reports Repository
|
|
274
|
+
// ============================================================
|
|
275
|
+
|
|
276
|
+
export class AnalysisReportsRepo {
|
|
277
|
+
private stmts: {
|
|
278
|
+
insert: Database.Statement
|
|
279
|
+
findBySession: Database.Statement
|
|
280
|
+
findById: Database.Statement
|
|
281
|
+
deleteBySession: Database.Statement
|
|
282
|
+
deleteById: Database.Statement
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
constructor(private db: Database.Database) {
|
|
286
|
+
this.stmts = {
|
|
287
|
+
insert: db.prepare(
|
|
288
|
+
`INSERT INTO analysis_reports (id, session_id, created_at, llm_provider, llm_model, prompt_tokens, completion_tokens, report_content, filter_prompt_tokens, filter_completion_tokens)
|
|
289
|
+
VALUES (@id, @session_id, @created_at, @llm_provider, @llm_model, @prompt_tokens, @completion_tokens, @report_content, @filter_prompt_tokens, @filter_completion_tokens)`
|
|
290
|
+
),
|
|
291
|
+
findBySession: db.prepare(
|
|
292
|
+
'SELECT * FROM analysis_reports WHERE session_id = ? ORDER BY created_at DESC'
|
|
293
|
+
),
|
|
294
|
+
findById: db.prepare('SELECT * FROM analysis_reports WHERE id = ?'),
|
|
295
|
+
deleteBySession: db.prepare('DELETE FROM analysis_reports WHERE session_id = ?'),
|
|
296
|
+
deleteById: db.prepare('DELETE FROM analysis_reports WHERE id = ?')
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
insert(report: AnalysisReport): void {
|
|
301
|
+
this.stmts.insert.run(report)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
findBySession(sessionId: string): AnalysisReport[] {
|
|
305
|
+
return this.stmts.findBySession.all(sessionId) as AnalysisReport[]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
findById(id: string): AnalysisReport | undefined {
|
|
309
|
+
return this.stmts.findById.get(id) as AnalysisReport | undefined
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
deleteBySession(sessionId: string): void {
|
|
313
|
+
this.stmts.deleteBySession.run(sessionId)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
deleteById(id: string): void {
|
|
317
|
+
this.stmts.deleteById.run(id)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============================================================
|
|
322
|
+
// Fingerprint Profiles Repository
|
|
323
|
+
// ============================================================
|
|
324
|
+
|
|
325
|
+
export class FingerprintProfilesRepo {
|
|
326
|
+
private stmts: {
|
|
327
|
+
upsert: Database.Statement
|
|
328
|
+
findBySessionId: Database.Statement
|
|
329
|
+
delete: Database.Statement
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
constructor(private db: Database.Database) {
|
|
333
|
+
this.stmts = {
|
|
334
|
+
upsert: db.prepare(
|
|
335
|
+
`INSERT OR REPLACE INTO fingerprint_profiles (session_id, profile_json)
|
|
336
|
+
VALUES (@session_id, @profile_json)`
|
|
337
|
+
),
|
|
338
|
+
findBySessionId: db.prepare(
|
|
339
|
+
'SELECT profile_json FROM fingerprint_profiles WHERE session_id = ?'
|
|
340
|
+
),
|
|
341
|
+
delete: db.prepare('DELETE FROM fingerprint_profiles WHERE session_id = ?')
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
upsert(sessionId: string, profile: FingerprintProfile): void {
|
|
346
|
+
this.stmts.upsert.run({
|
|
347
|
+
session_id: sessionId,
|
|
348
|
+
profile_json: JSON.stringify(profile),
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
findBySessionId(sessionId: string): FingerprintProfile | null {
|
|
353
|
+
const row = this.stmts.findBySessionId.get(sessionId) as { profile_json: string } | undefined
|
|
354
|
+
if (!row) return null
|
|
355
|
+
return JSON.parse(row.profile_json)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
delete(sessionId: string): void {
|
|
359
|
+
this.stmts.delete.run(sessionId)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ============================================================
|
|
364
|
+
// Chat Messages Repository
|
|
365
|
+
// ============================================================
|
|
366
|
+
|
|
367
|
+
export class ChatMessagesRepo {
|
|
368
|
+
private stmts: {
|
|
369
|
+
insert: Database.Statement
|
|
370
|
+
findByReport: Database.Statement
|
|
371
|
+
deleteByReport: Database.Statement
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
constructor(private db: Database.Database) {
|
|
375
|
+
this.stmts = {
|
|
376
|
+
insert: db.prepare(
|
|
377
|
+
`INSERT INTO chat_messages (report_id, role, content, created_at)
|
|
378
|
+
VALUES (@report_id, @role, @content, @created_at)`
|
|
379
|
+
),
|
|
380
|
+
findByReport: db.prepare(
|
|
381
|
+
'SELECT role, content FROM chat_messages WHERE report_id = ? ORDER BY id ASC'
|
|
382
|
+
),
|
|
383
|
+
deleteByReport: db.prepare('DELETE FROM chat_messages WHERE report_id = ?')
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
append(reportId: string, role: string, content: string): void {
|
|
388
|
+
this.stmts.insert.run({
|
|
389
|
+
report_id: reportId,
|
|
390
|
+
role,
|
|
391
|
+
content,
|
|
392
|
+
created_at: Date.now(),
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
insertMany(reportId: string, messages: Array<{ role: string; content: string }>): void {
|
|
397
|
+
const now = Date.now()
|
|
398
|
+
const insertMany = this.db.transaction((msgs: Array<{ role: string; content: string }>) => {
|
|
399
|
+
for (const msg of msgs) {
|
|
400
|
+
this.stmts.insert.run({
|
|
401
|
+
report_id: reportId,
|
|
402
|
+
role: msg.role,
|
|
403
|
+
content: msg.content,
|
|
404
|
+
created_at: now,
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
insertMany(messages)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
findByReport(reportId: string): Array<{ role: string; content: string }> {
|
|
412
|
+
return this.stmts.findByReport.all(reportId) as Array<{ role: string; content: string }>
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
deleteByReport(reportId: string): void {
|
|
416
|
+
this.stmts.deleteByReport.run(reportId)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ============================================================
|
|
421
|
+
// AI Request Log Repository
|
|
422
|
+
// ============================================================
|
|
423
|
+
|
|
424
|
+
/** Columns returned in list queries (excludes large body fields) */
|
|
425
|
+
const AI_LOG_LIST_COLUMNS = `
|
|
426
|
+
id, session_id, report_id, type, provider, model,
|
|
427
|
+
request_url, request_method, status_code,
|
|
428
|
+
prompt_tokens, completion_tokens, duration_ms, error, created_at
|
|
429
|
+
`.trim();
|
|
430
|
+
|
|
431
|
+
export class AiRequestLogRepo {
|
|
432
|
+
private stmts: {
|
|
433
|
+
insert: Database.Statement;
|
|
434
|
+
findBySession: Database.Statement;
|
|
435
|
+
findAll: Database.Statement;
|
|
436
|
+
findById: Database.Statement;
|
|
437
|
+
deleteBySession: Database.Statement;
|
|
438
|
+
updateTokens: Database.Statement;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
constructor(private db: Database.Database) {
|
|
442
|
+
this.stmts = {
|
|
443
|
+
insert: db.prepare(
|
|
444
|
+
`INSERT INTO ai_request_logs
|
|
445
|
+
(session_id, report_id, type, provider, model,
|
|
446
|
+
request_url, request_method, request_headers, request_body,
|
|
447
|
+
status_code, response_headers, response_body,
|
|
448
|
+
prompt_tokens, completion_tokens, duration_ms, error, created_at)
|
|
449
|
+
VALUES
|
|
450
|
+
(@session_id, @report_id, @type, @provider, @model,
|
|
451
|
+
@request_url, @request_method, @request_headers, @request_body,
|
|
452
|
+
@status_code, @response_headers, @response_body,
|
|
453
|
+
@prompt_tokens, @completion_tokens, @duration_ms, @error, @created_at)`
|
|
454
|
+
),
|
|
455
|
+
findBySession: db.prepare(
|
|
456
|
+
`SELECT ${AI_LOG_LIST_COLUMNS} FROM ai_request_logs
|
|
457
|
+
WHERE session_id = ? ORDER BY created_at DESC`
|
|
458
|
+
),
|
|
459
|
+
findAll: db.prepare(
|
|
460
|
+
`SELECT ${AI_LOG_LIST_COLUMNS} FROM ai_request_logs
|
|
461
|
+
ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
462
|
+
),
|
|
463
|
+
findById: db.prepare(
|
|
464
|
+
'SELECT * FROM ai_request_logs WHERE id = ?'
|
|
465
|
+
),
|
|
466
|
+
deleteBySession: db.prepare(
|
|
467
|
+
'DELETE FROM ai_request_logs WHERE session_id = ?'
|
|
468
|
+
),
|
|
469
|
+
updateTokens: db.prepare(
|
|
470
|
+
`UPDATE ai_request_logs SET prompt_tokens = ?, completion_tokens = ?
|
|
471
|
+
WHERE id = (SELECT MAX(id) FROM ai_request_logs WHERE session_id = ? AND type = ?)`
|
|
472
|
+
),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
insert(log: Omit<AiRequestLog, 'id'>): void {
|
|
477
|
+
this.stmts.insert.run(log);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
findBySession(sessionId: string): AiRequestLog[] {
|
|
481
|
+
return this.stmts.findBySession.all(sessionId) as AiRequestLog[];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
findAll(limit: number, offset: number): AiRequestLog[] {
|
|
485
|
+
return this.stmts.findAll.all(limit, offset) as AiRequestLog[];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
findById(id: number): AiRequestLog | null {
|
|
489
|
+
return (this.stmts.findById.get(id) as AiRequestLog) ?? null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
deleteBySession(sessionId: string): void {
|
|
493
|
+
this.stmts.deleteBySession.run(sessionId);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
updateLatestTokens(sessionId: string, type: string, promptTokens: number, completionTokens: number): void {
|
|
497
|
+
this.stmts.updateTokens.run(promptTokens, completionTokens, sessionId, type);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ============================================================
|
|
502
|
+
// Interaction Events Repository
|
|
503
|
+
// ============================================================
|
|
504
|
+
|
|
505
|
+
export class InteractionEventsRepo {
|
|
506
|
+
private stmts: {
|
|
507
|
+
insert: Database.Statement;
|
|
508
|
+
getNextSequence: Database.Statement;
|
|
509
|
+
findBySession: Database.Statement;
|
|
510
|
+
findBySessionAndType: Database.Statement;
|
|
511
|
+
findById: Database.Statement;
|
|
512
|
+
deleteBySession: Database.Statement;
|
|
513
|
+
count: Database.Statement;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
constructor(private db: Database.Database) {
|
|
517
|
+
this.stmts = {
|
|
518
|
+
insert: db.prepare(
|
|
519
|
+
`INSERT INTO interaction_events
|
|
520
|
+
(session_id, sequence, type, timestamp, x, y, viewport_x, viewport_y,
|
|
521
|
+
selector, xpath, tag_name, element_text, attributes, bounding_rect,
|
|
522
|
+
input_value, key, scroll_x, scroll_y, scroll_dx, scroll_dy,
|
|
523
|
+
url, page_title, path, created_at)
|
|
524
|
+
VALUES
|
|
525
|
+
(@session_id, @sequence, @type, @timestamp, @x, @y, @viewport_x, @viewport_y,
|
|
526
|
+
@selector, @xpath, @tag_name, @element_text, @attributes, @bounding_rect,
|
|
527
|
+
@input_value, @key, @scroll_x, @scroll_y, @scroll_dx, @scroll_dy,
|
|
528
|
+
@url, @page_title, @path, @created_at)`
|
|
529
|
+
),
|
|
530
|
+
getNextSequence: db.prepare(
|
|
531
|
+
'SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM interaction_events WHERE session_id = ?'
|
|
532
|
+
),
|
|
533
|
+
findBySession: db.prepare(
|
|
534
|
+
'SELECT * FROM interaction_events WHERE session_id = ? ORDER BY sequence ASC LIMIT ?'
|
|
535
|
+
),
|
|
536
|
+
findBySessionAndType: db.prepare(
|
|
537
|
+
'SELECT * FROM interaction_events WHERE session_id = ? AND type = ? ORDER BY sequence ASC'
|
|
538
|
+
),
|
|
539
|
+
findById: db.prepare('SELECT * FROM interaction_events WHERE id = ?'),
|
|
540
|
+
deleteBySession: db.prepare('DELETE FROM interaction_events WHERE session_id = ?'),
|
|
541
|
+
count: db.prepare('SELECT COUNT(*) AS cnt FROM interaction_events WHERE session_id = ?'),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
insert(event: Omit<InteractionEvent, 'id'>): void {
|
|
546
|
+
this.stmts.insert.run(event);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
getNextSequence(sessionId: string): number {
|
|
550
|
+
const row = this.stmts.getNextSequence.get(sessionId) as { next_seq: number };
|
|
551
|
+
return row.next_seq;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
findBySession(sessionId: string, limit: number = 1000): InteractionEvent[] {
|
|
555
|
+
return this.stmts.findBySession.all(sessionId, limit) as InteractionEvent[];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
findBySessionAndType(sessionId: string, type: InteractionType): InteractionEvent[] {
|
|
559
|
+
return this.stmts.findBySessionAndType.all(sessionId, type) as InteractionEvent[];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
findById(id: number): InteractionEvent | null {
|
|
563
|
+
return (this.stmts.findById.get(id) as InteractionEvent) ?? null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
deleteBySession(sessionId: string): void {
|
|
567
|
+
this.stmts.deleteBySession.run(sessionId);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
count(sessionId: string): number {
|
|
571
|
+
const row = this.stmts.count.get(sessionId) as { cnt: number };
|
|
572
|
+
return row.cnt;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Session as ElectronSession } from 'electron';
|
|
2
|
+
import type { FingerprintProfile } from '@shared/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Apply HTTP-level fingerprint spoofing to an Electron session.
|
|
6
|
+
* - Sets the User-Agent globally
|
|
7
|
+
* - Intercepts outgoing requests to rewrite Client Hints and Accept-Language headers
|
|
8
|
+
*/
|
|
9
|
+
export function applyHttpSpoofing(
|
|
10
|
+
electronSession: ElectronSession,
|
|
11
|
+
profile: FingerprintProfile,
|
|
12
|
+
): void {
|
|
13
|
+
// Set global User-Agent
|
|
14
|
+
electronSession.setUserAgent(profile.userAgent);
|
|
15
|
+
|
|
16
|
+
// Build Client Hints values from profile
|
|
17
|
+
const majorVersion = profile.userAgent.match(/Chrome\/(\d+)/)?.[1] ?? '131';
|
|
18
|
+
const brandList = `"Chromium";v="${majorVersion}", "Google Chrome";v="${majorVersion}", "Not-A.Brand";v="8"`;
|
|
19
|
+
const platformMap: Record<string, string> = {
|
|
20
|
+
'Win32': '"Windows"',
|
|
21
|
+
'MacIntel': '"macOS"',
|
|
22
|
+
'Linux x86_64': '"Linux"',
|
|
23
|
+
};
|
|
24
|
+
const secPlatform = platformMap[profile.platform] ?? '"Windows"';
|
|
25
|
+
const acceptLanguage = profile.languages
|
|
26
|
+
.map((lang, i) => i === 0 ? lang : `${lang};q=${(1 - i * 0.1).toFixed(1)}`)
|
|
27
|
+
.join(',');
|
|
28
|
+
|
|
29
|
+
// Intercept and rewrite headers
|
|
30
|
+
electronSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
|
31
|
+
const headers = { ...details.requestHeaders };
|
|
32
|
+
|
|
33
|
+
headers['Accept-Language'] = acceptLanguage;
|
|
34
|
+
headers['Sec-CH-UA'] = brandList;
|
|
35
|
+
headers['Sec-CH-UA-Platform'] = secPlatform;
|
|
36
|
+
headers['Sec-CH-UA-Mobile'] = '?0';
|
|
37
|
+
|
|
38
|
+
callback({ requestHeaders: headers });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Remove HTTP spoofing from an Electron session.
|
|
44
|
+
* Restores default behavior by passing through all headers unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export function removeHttpSpoofing(electronSession: ElectronSession): void {
|
|
47
|
+
electronSession.webRequest.onBeforeSendHeaders(null);
|
|
48
|
+
}
|