@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,130 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
import type { WebContents } from 'electron'
|
|
3
|
+
import type { CapturedRequest, JsHookRecord, StorageSnapshot } from '@shared/types'
|
|
4
|
+
import type { RequestsRepo, JsHooksRepo, StorageSnapshotsRepo } from '../db/repositories'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CaptureEngine — Aggregates data from CDP, JS hooks, and storage collectors,
|
|
8
|
+
* writes structured data to SQLite, and emits IPC events to the renderer.
|
|
9
|
+
*/
|
|
10
|
+
export class CaptureEngine extends EventEmitter {
|
|
11
|
+
private sessionId: string | null = null
|
|
12
|
+
private rendererWebContents: WebContents | null = null
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private requestsRepo: RequestsRepo,
|
|
16
|
+
private jsHooksRepo: JsHooksRepo,
|
|
17
|
+
private storageSnapshotsRepo: StorageSnapshotsRepo
|
|
18
|
+
) {
|
|
19
|
+
super()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start(sessionId: string, rendererWebContents: WebContents): void {
|
|
23
|
+
this.sessionId = sessionId
|
|
24
|
+
this.rendererWebContents = rendererWebContents
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
stop(): void {
|
|
28
|
+
this.sessionId = null
|
|
29
|
+
this.rendererWebContents = null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
handleResponseCaptured(data: {
|
|
33
|
+
requestId: string; method: string; url: string;
|
|
34
|
+
requestHeaders: string; requestBody: string | null;
|
|
35
|
+
statusCode: number; responseHeaders: string;
|
|
36
|
+
responseBody: string | null; contentType: string | null;
|
|
37
|
+
initiator: string | null; durationMs: number | null;
|
|
38
|
+
isOptions: boolean; isStatic: boolean; isStreaming: boolean; isWebSocket: boolean; truncated: boolean; timestamp: number;
|
|
39
|
+
source?: 'cdp' | 'proxy'
|
|
40
|
+
}): void {
|
|
41
|
+
if (!this.sessionId) return
|
|
42
|
+
|
|
43
|
+
let sequence: number
|
|
44
|
+
// Guard quit race: DB may already be closing during app shutdown.
|
|
45
|
+
try {
|
|
46
|
+
sequence = this.requestsRepo.getNextSequence(this.sessionId)
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn('[CaptureEngine] getNextSequence failed:', (err as Error).message)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Generate a unique ID per record to avoid UNIQUE constraint conflicts.
|
|
53
|
+
// The original requestId from CDP/proxy may repeat across sessions or retries.
|
|
54
|
+
const uniqueId = `${this.sessionId}-${sequence}`
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
this.requestsRepo.insert({
|
|
58
|
+
id: uniqueId, session_id: this.sessionId, sequence,
|
|
59
|
+
timestamp: data.timestamp, method: data.method, url: data.url,
|
|
60
|
+
request_headers: data.requestHeaders, request_body: data.requestBody,
|
|
61
|
+
content_type: data.contentType, initiator: data.initiator,
|
|
62
|
+
source: data.source || 'cdp'
|
|
63
|
+
})
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.warn('[CaptureEngine] Insert failed:', (err as Error).message)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
this.requestsRepo.updateResponse({
|
|
70
|
+
id: uniqueId, status_code: data.statusCode,
|
|
71
|
+
response_headers: data.responseHeaders,
|
|
72
|
+
response_body: data.responseBody,
|
|
73
|
+
content_type: data.contentType, duration_ms: data.durationMs || 0,
|
|
74
|
+
is_streaming: data.isStreaming ? 1 : 0,
|
|
75
|
+
is_websocket: data.isWebSocket ? 1 : 0
|
|
76
|
+
})
|
|
77
|
+
} catch { /* ignore */ }
|
|
78
|
+
|
|
79
|
+
const captured: CapturedRequest = {
|
|
80
|
+
id: uniqueId, session_id: this.sessionId, sequence,
|
|
81
|
+
timestamp: data.timestamp, method: data.method, url: data.url,
|
|
82
|
+
request_headers: data.requestHeaders, request_body: data.requestBody,
|
|
83
|
+
status_code: data.statusCode, response_headers: data.responseHeaders,
|
|
84
|
+
response_body: data.responseBody, content_type: data.contentType,
|
|
85
|
+
initiator: data.initiator, duration_ms: data.durationMs,
|
|
86
|
+
is_streaming: data.isStreaming, is_websocket: data.isWebSocket,
|
|
87
|
+
source: data.source || 'cdp'
|
|
88
|
+
}
|
|
89
|
+
this.sendToRenderer('capture:request', captured)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
handleHookCaptured(data: {
|
|
93
|
+
hookType: string; functionName: string; arguments: string;
|
|
94
|
+
result: string | null; callStack: string | null; timestamp: number
|
|
95
|
+
}): void {
|
|
96
|
+
if (!this.sessionId) return
|
|
97
|
+
|
|
98
|
+
const record: Omit<JsHookRecord, 'id'> = {
|
|
99
|
+
session_id: this.sessionId, timestamp: data.timestamp,
|
|
100
|
+
hook_type: data.hookType as JsHookRecord['hook_type'],
|
|
101
|
+
function_name: data.functionName, arguments: data.arguments,
|
|
102
|
+
result: data.result, call_stack: data.callStack, related_request_id: null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try { this.jsHooksRepo.insert(record) } catch { /* ignore */ }
|
|
106
|
+
this.sendToRenderer('capture:hook', record)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
handleStorageCollected(data: {
|
|
110
|
+
domain: string; storageType: string; data: string; timestamp: number
|
|
111
|
+
}): void {
|
|
112
|
+
if (!this.sessionId) return
|
|
113
|
+
|
|
114
|
+
const snapshot: Omit<StorageSnapshot, 'id'> = {
|
|
115
|
+
session_id: this.sessionId, timestamp: data.timestamp,
|
|
116
|
+
domain: data.domain,
|
|
117
|
+
storage_type: data.storageType as StorageSnapshot['storage_type'],
|
|
118
|
+
data: data.data
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try { this.storageSnapshotsRepo.insert(snapshot) } catch { /* ignore */ }
|
|
122
|
+
this.sendToRenderer('capture:storage', snapshot)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private sendToRenderer(channel: string, data: unknown): void {
|
|
126
|
+
if (this.rendererWebContents && !this.rendererWebContents.isDestroyed()) {
|
|
127
|
+
this.rendererWebContents.send(channel, data)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
import type { WebContents } from 'electron'
|
|
3
|
+
import type { InteractionEventsRepo } from '../db/repositories'
|
|
4
|
+
import type { InteractionEvent, RawInteractionData } from '@shared/types'
|
|
5
|
+
import { readFileSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* InteractionRecorder — Records user interactions (clicks, inputs, scrolls, mouse movements)
|
|
10
|
+
* from the target browser and persists them to SQLite.
|
|
11
|
+
*/
|
|
12
|
+
export class InteractionRecorder extends EventEmitter {
|
|
13
|
+
private sessionId: string | null = null
|
|
14
|
+
private rendererWebContents: WebContents | null = null
|
|
15
|
+
private recording = false
|
|
16
|
+
private scriptContent: string | null = null
|
|
17
|
+
private injectedWebContents: Set<WebContents> = new Set()
|
|
18
|
+
|
|
19
|
+
constructor(private repo: InteractionEventsRepo) {
|
|
20
|
+
super()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Start recording interactions (injection is handled separately via injectIntoWebContents) */
|
|
24
|
+
start(sessionId: string, rendererWebContents: WebContents): void {
|
|
25
|
+
this.sessionId = sessionId
|
|
26
|
+
this.rendererWebContents = rendererWebContents
|
|
27
|
+
this.recording = true
|
|
28
|
+
this.loadScript()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inject interaction-hook script into a WebContents (for multi-tab support).
|
|
33
|
+
* Called by SessionManager when tabs are attached to the capture pipeline.
|
|
34
|
+
*/
|
|
35
|
+
injectIntoWebContents(webContents: WebContents): void {
|
|
36
|
+
this.loadScript()
|
|
37
|
+
if (!this.scriptContent) return
|
|
38
|
+
if (webContents.isDestroyed()) return
|
|
39
|
+
webContents.executeJavaScript(this.scriptContent, true).catch(() => { /* page not ready or destroyed */ })
|
|
40
|
+
|
|
41
|
+
this.injectedWebContents.add(webContents)
|
|
42
|
+
webContents.once('destroyed', () => {
|
|
43
|
+
this.injectedWebContents.delete(webContents)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Set recording state
|
|
47
|
+
if (this.recording) {
|
|
48
|
+
webContents.executeJavaScript(
|
|
49
|
+
`window.postMessage({type:'ar-interaction-control',recording:true},'*')`,
|
|
50
|
+
true
|
|
51
|
+
).catch(() => {})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Re-inject on navigation for this tab
|
|
55
|
+
const handler = () => {
|
|
56
|
+
if (!this.recording || webContents.isDestroyed()) return
|
|
57
|
+
webContents.executeJavaScript(this.scriptContent!, true).catch(() => {})
|
|
58
|
+
webContents.executeJavaScript(
|
|
59
|
+
`window.postMessage({type:'ar-interaction-control',recording:true},'*')`,
|
|
60
|
+
true
|
|
61
|
+
).catch(() => {})
|
|
62
|
+
}
|
|
63
|
+
webContents.on('did-navigate', handler)
|
|
64
|
+
webContents.on('did-navigate-in-page', handler)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Pause recording (keeps session active) */
|
|
68
|
+
pause(): void {
|
|
69
|
+
this.recording = false
|
|
70
|
+
this.setRecordingState(false)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resume recording */
|
|
74
|
+
resume(): void {
|
|
75
|
+
this.recording = true
|
|
76
|
+
this.setRecordingState(true)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Stop recording and clean up */
|
|
80
|
+
stop(): void {
|
|
81
|
+
this.recording = false
|
|
82
|
+
this.sessionId = null
|
|
83
|
+
this.rendererWebContents = null
|
|
84
|
+
this.injectedWebContents.clear()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Process interaction data from page injection script */
|
|
88
|
+
handleInteraction(data: RawInteractionData): void {
|
|
89
|
+
if (!this.recording || !this.sessionId) return
|
|
90
|
+
|
|
91
|
+
let sequence: number
|
|
92
|
+
try {
|
|
93
|
+
sequence = this.repo.getNextSequence(this.sessionId)
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.warn('[InteractionRecorder] getNextSequence failed:', (err as Error).message)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const event: Omit<InteractionEvent, 'id'> = {
|
|
100
|
+
session_id: this.sessionId,
|
|
101
|
+
sequence,
|
|
102
|
+
type: data.type,
|
|
103
|
+
timestamp: data.timestamp,
|
|
104
|
+
x: data.x ?? null,
|
|
105
|
+
y: data.y ?? null,
|
|
106
|
+
viewport_x: data.viewportX ?? null,
|
|
107
|
+
viewport_y: data.viewportY ?? null,
|
|
108
|
+
selector: data.selector ?? null,
|
|
109
|
+
xpath: data.xpath ?? null,
|
|
110
|
+
tag_name: data.tagName ?? null,
|
|
111
|
+
element_text: data.elementText ?? null,
|
|
112
|
+
attributes: data.attributes ? JSON.stringify(data.attributes) : null,
|
|
113
|
+
bounding_rect: data.boundingRect ? JSON.stringify(data.boundingRect) : null,
|
|
114
|
+
input_value: data.inputValue ?? null,
|
|
115
|
+
key: data.key ?? null,
|
|
116
|
+
scroll_x: data.scrollX ?? null,
|
|
117
|
+
scroll_y: data.scrollY ?? null,
|
|
118
|
+
scroll_dx: data.scrollDX ?? null,
|
|
119
|
+
scroll_dy: data.scrollDY ?? null,
|
|
120
|
+
url: data.url,
|
|
121
|
+
page_title: data.pageTitle ?? null,
|
|
122
|
+
path: data.path ? JSON.stringify(data.path) : null,
|
|
123
|
+
created_at: Date.now(),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
this.repo.insert(event)
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.warn('[InteractionRecorder] Insert failed:', (err as Error).message)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Notify renderer (lightweight event, no heavy data)
|
|
134
|
+
if (this.rendererWebContents && !this.rendererWebContents.isDestroyed()) {
|
|
135
|
+
this.rendererWebContents.send('interaction:recorded', {
|
|
136
|
+
type: data.type,
|
|
137
|
+
sequence,
|
|
138
|
+
timestamp: data.timestamp,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
this.emit('interaction', event)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
isRecording(): boolean {
|
|
145
|
+
return this.recording
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getSessionId(): string | null {
|
|
149
|
+
return this.sessionId
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private loadScript(): void {
|
|
153
|
+
if (this.scriptContent) return
|
|
154
|
+
try {
|
|
155
|
+
const scriptPath = join(__dirname, '../preload/interaction-hook.js')
|
|
156
|
+
this.scriptContent = readFileSync(scriptPath, 'utf-8')
|
|
157
|
+
} catch {
|
|
158
|
+
this.scriptContent = `console.log('[AnythingAnalyzer] Interaction hook script not found')`
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private setRecordingState(recording: boolean): void {
|
|
163
|
+
for (const wc of this.injectedWebContents) {
|
|
164
|
+
if (wc.isDestroyed()) continue
|
|
165
|
+
wc.executeJavaScript(
|
|
166
|
+
`window.postMessage({type:'ar-interaction-control',recording:${recording}},'*')`,
|
|
167
|
+
true
|
|
168
|
+
).catch(() => {})
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import type { WebContents } from "electron";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* JsInjector — Manages hook script injection into a target browser WebContents.
|
|
8
|
+
* Only handles script injection and re-injection on navigation.
|
|
9
|
+
* IPC hook data listening is handled externally (SessionManager).
|
|
10
|
+
*/
|
|
11
|
+
export class JsInjector extends EventEmitter {
|
|
12
|
+
private webContents: WebContents | null = null;
|
|
13
|
+
private hookScriptContent: string | null = null;
|
|
14
|
+
private navigationHandler: (() => void) | null = null;
|
|
15
|
+
|
|
16
|
+
start(webContents: WebContents): void {
|
|
17
|
+
this.webContents = webContents;
|
|
18
|
+
this.loadHookScript();
|
|
19
|
+
this.injectHooks();
|
|
20
|
+
|
|
21
|
+
this.navigationHandler = () => {
|
|
22
|
+
this.injectHooks();
|
|
23
|
+
};
|
|
24
|
+
webContents.on("did-navigate", this.navigationHandler);
|
|
25
|
+
webContents.on("did-navigate-in-page", this.navigationHandler);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
stop(): void {
|
|
29
|
+
if (this.webContents && this.navigationHandler) {
|
|
30
|
+
this.webContents.removeListener("did-navigate", this.navigationHandler);
|
|
31
|
+
this.webContents.removeListener(
|
|
32
|
+
"did-navigate-in-page",
|
|
33
|
+
this.navigationHandler,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
this.webContents = null;
|
|
37
|
+
this.navigationHandler = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private loadHookScript(): void {
|
|
41
|
+
if (this.hookScriptContent) return;
|
|
42
|
+
try {
|
|
43
|
+
const scriptPath = join(__dirname, "../preload/hook-script.js");
|
|
44
|
+
this.hookScriptContent = readFileSync(scriptPath, "utf-8");
|
|
45
|
+
} catch {
|
|
46
|
+
this.hookScriptContent = `console.log('[AnythingAnalyzer] Hook script not found')`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private injectHooks(): void {
|
|
51
|
+
if (!this.webContents || !this.hookScriptContent) return;
|
|
52
|
+
if (this.webContents.isDestroyed()) return;
|
|
53
|
+
this.webContents.executeJavaScript(this.hookScriptContent, true).catch(() => {
|
|
54
|
+
/* not ready or destroyed */
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { WebContents } from 'electron'
|
|
2
|
+
import type { InteractionEvent } from '@shared/types'
|
|
3
|
+
|
|
4
|
+
interface ReplayOptions {
|
|
5
|
+
speed: number // Playback speed multiplier (1.0 = original)
|
|
6
|
+
skipMoves: boolean // Whether to skip hover/move events
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ReplayEngine — Replays recorded interaction events via CDP Input domain.
|
|
11
|
+
*/
|
|
12
|
+
export class ReplayEngine {
|
|
13
|
+
private aborted = false
|
|
14
|
+
|
|
15
|
+
/** Send a CDP command, checking for destroyed WebContents first. */
|
|
16
|
+
private async cdp(
|
|
17
|
+
wc: WebContents,
|
|
18
|
+
method: string,
|
|
19
|
+
params: Record<string, unknown> = {}
|
|
20
|
+
): Promise<Record<string, unknown>> {
|
|
21
|
+
if (wc.isDestroyed()) throw new Error('WebContents destroyed during replay')
|
|
22
|
+
return wc.debugger.sendCommand(method, params) as Promise<Record<string, unknown>>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Safely load a URL, checking for destroyed WebContents first. */
|
|
26
|
+
private async safeLoadURL(wc: WebContents, url: string): Promise<void> {
|
|
27
|
+
if (wc.isDestroyed()) throw new Error('WebContents destroyed during replay')
|
|
28
|
+
await wc.loadURL(url)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async replay(
|
|
32
|
+
webContents: WebContents,
|
|
33
|
+
events: InteractionEvent[],
|
|
34
|
+
options: ReplayOptions = { speed: 1, skipMoves: false }
|
|
35
|
+
): Promise<{ success: boolean; stepsCompleted: number; error?: string }> {
|
|
36
|
+
this.aborted = false
|
|
37
|
+
let completed = 0
|
|
38
|
+
|
|
39
|
+
if (webContents.isDestroyed()) {
|
|
40
|
+
return { success: false, stepsCompleted: 0, error: 'WebContents destroyed' }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!webContents.debugger.isAttached()) {
|
|
44
|
+
try {
|
|
45
|
+
webContents.debugger.attach('1.3')
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { success: false, stepsCompleted: 0, error: `Failed to attach debugger: ${(err as Error).message}` }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
for (let i = 0; i < events.length; i++) {
|
|
53
|
+
if (this.aborted || webContents.isDestroyed()) break
|
|
54
|
+
const event = events[i]
|
|
55
|
+
|
|
56
|
+
if (options.skipMoves && event.type === 'hover') {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await this.executeStep(webContents, event)
|
|
61
|
+
completed++
|
|
62
|
+
|
|
63
|
+
// Wait between events based on original timing
|
|
64
|
+
const nextEvent = events[i + 1]
|
|
65
|
+
if (nextEvent) {
|
|
66
|
+
const delay = (nextEvent.timestamp - event.timestamp) / options.speed
|
|
67
|
+
await this.wait(Math.min(Math.max(delay, 10), 3000))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return { success: false, stepsCompleted: completed, error: (err as Error).message }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { success: !this.aborted, stepsCompleted: completed }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
abort(): void {
|
|
78
|
+
this.aborted = true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Execute a single browser action (for MCP execute_browser_action tool) */
|
|
82
|
+
async executeAction(
|
|
83
|
+
webContents: WebContents,
|
|
84
|
+
action: { type: string; selector?: string; text?: string; url?: string; x?: number; y?: number; scrollDelta?: number }
|
|
85
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
86
|
+
if (webContents.isDestroyed()) {
|
|
87
|
+
return { success: false, error: 'WebContents destroyed' }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!webContents.debugger.isAttached()) {
|
|
91
|
+
try {
|
|
92
|
+
webContents.debugger.attach('1.3')
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return { success: false, error: `Failed to attach debugger: ${(err as Error).message}` }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
switch (action.type) {
|
|
100
|
+
case 'click': {
|
|
101
|
+
if (action.selector) {
|
|
102
|
+
const coords = await this.resolveElementCenter(webContents, action.selector)
|
|
103
|
+
if (!coords) return { success: false, error: `Element not found: ${action.selector}` }
|
|
104
|
+
await this.clickAt(webContents, coords.x, coords.y)
|
|
105
|
+
} else if (action.x != null && action.y != null) {
|
|
106
|
+
await this.clickAt(webContents, action.x, action.y)
|
|
107
|
+
} else {
|
|
108
|
+
return { success: false, error: 'click requires selector or x/y coordinates' }
|
|
109
|
+
}
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
case 'type': {
|
|
113
|
+
if (!action.text) return { success: false, error: 'type requires text' }
|
|
114
|
+
if (action.selector) {
|
|
115
|
+
await this.cdp(webContents, 'Runtime.evaluate', {
|
|
116
|
+
expression: `document.querySelector(${JSON.stringify(action.selector)})?.focus()`
|
|
117
|
+
})
|
|
118
|
+
await this.wait(50)
|
|
119
|
+
}
|
|
120
|
+
for (const char of action.text) {
|
|
121
|
+
await this.cdp(webContents, 'Input.dispatchKeyEvent', {
|
|
122
|
+
type: 'char', text: char
|
|
123
|
+
})
|
|
124
|
+
await this.wait(20)
|
|
125
|
+
}
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
case 'scroll': {
|
|
129
|
+
const x = action.x ?? 400
|
|
130
|
+
const y = action.y ?? 300
|
|
131
|
+
const delta = action.scrollDelta ?? 200
|
|
132
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
133
|
+
type: 'mouseWheel', x, y, deltaX: 0, deltaY: delta
|
|
134
|
+
})
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
case 'navigate': {
|
|
138
|
+
if (!action.url) return { success: false, error: 'navigate requires url' }
|
|
139
|
+
await this.safeLoadURL(webContents, action.url)
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
return { success: false, error: `Unknown action type: ${action.type}` }
|
|
144
|
+
}
|
|
145
|
+
return { success: true }
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return { success: false, error: (err as Error).message }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async executeStep(webContents: WebContents, event: InteractionEvent): Promise<void> {
|
|
152
|
+
switch (event.type) {
|
|
153
|
+
case 'click':
|
|
154
|
+
case 'dblclick': {
|
|
155
|
+
const x = event.viewport_x ?? event.x ?? 0
|
|
156
|
+
const y = event.viewport_y ?? event.y ?? 0
|
|
157
|
+
const clickCount = event.type === 'dblclick' ? 2 : 1
|
|
158
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
159
|
+
type: 'mouseMoved', x, y
|
|
160
|
+
})
|
|
161
|
+
await this.wait(30)
|
|
162
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
163
|
+
type: 'mousePressed', x, y, button: 'left', clickCount
|
|
164
|
+
})
|
|
165
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
166
|
+
type: 'mouseReleased', x, y, button: 'left', clickCount
|
|
167
|
+
})
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
case 'input': {
|
|
171
|
+
if (event.selector && event.input_value != null) {
|
|
172
|
+
await this.cdp(webContents, 'Runtime.evaluate', {
|
|
173
|
+
expression: `document.querySelector(${JSON.stringify(event.selector)})?.focus()`
|
|
174
|
+
})
|
|
175
|
+
await this.wait(50)
|
|
176
|
+
await this.cdp(webContents, 'Runtime.evaluate', {
|
|
177
|
+
expression: `{
|
|
178
|
+
const el = document.querySelector(${JSON.stringify(event.selector)});
|
|
179
|
+
if (el) { el.value = ''; el.dispatchEvent(new Event('input', {bubbles:true})); }
|
|
180
|
+
}`
|
|
181
|
+
})
|
|
182
|
+
for (const char of event.input_value) {
|
|
183
|
+
await this.cdp(webContents, 'Input.dispatchKeyEvent', {
|
|
184
|
+
type: 'char', text: char
|
|
185
|
+
})
|
|
186
|
+
await this.wait(15)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
case 'scroll': {
|
|
192
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
193
|
+
type: 'mouseWheel',
|
|
194
|
+
x: event.viewport_x ?? 400,
|
|
195
|
+
y: event.viewport_y ?? 300,
|
|
196
|
+
deltaX: event.scroll_dx ?? 0,
|
|
197
|
+
deltaY: event.scroll_dy ?? 0
|
|
198
|
+
})
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
case 'navigate': {
|
|
202
|
+
if (event.url) {
|
|
203
|
+
await this.safeLoadURL(webContents, event.url)
|
|
204
|
+
await this.wait(500)
|
|
205
|
+
}
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
case 'hover': {
|
|
209
|
+
if (event.path) {
|
|
210
|
+
const points = JSON.parse(event.path) as Array<{ x: number; y: number; t: number }>
|
|
211
|
+
for (const point of points) {
|
|
212
|
+
if (this.aborted) break
|
|
213
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
214
|
+
type: 'mouseMoved', x: point.x, y: point.y
|
|
215
|
+
})
|
|
216
|
+
await this.wait(20)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async clickAt(webContents: WebContents, x: number, y: number): Promise<void> {
|
|
225
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
226
|
+
type: 'mouseMoved', x, y
|
|
227
|
+
})
|
|
228
|
+
await this.wait(30)
|
|
229
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
230
|
+
type: 'mousePressed', x, y, button: 'left', clickCount: 1
|
|
231
|
+
})
|
|
232
|
+
await this.cdp(webContents, 'Input.dispatchMouseEvent', {
|
|
233
|
+
type: 'mouseReleased', x, y, button: 'left', clickCount: 1
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async resolveElementCenter(
|
|
238
|
+
webContents: WebContents,
|
|
239
|
+
selector: string
|
|
240
|
+
): Promise<{ x: number; y: number } | null> {
|
|
241
|
+
const result = await this.cdp(webContents, 'Runtime.evaluate', {
|
|
242
|
+
expression: `(function() {
|
|
243
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
244
|
+
if (!el) return null;
|
|
245
|
+
const r = el.getBoundingClientRect();
|
|
246
|
+
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
|
247
|
+
})()`,
|
|
248
|
+
returnByValue: true
|
|
249
|
+
}) as { result?: { value?: { x: number; y: number } | null } }
|
|
250
|
+
return result?.result?.value ?? null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private wait(ms: number): Promise<void> {
|
|
254
|
+
return new Promise(resolve => setTimeout(resolve, Math.max(ms, 5)))
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
import type { WebContents } from 'electron'
|
|
3
|
+
|
|
4
|
+
const COLLECTION_INTERVAL = 5000 // 5 seconds
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* StorageCollector — Periodically collects Cookie, localStorage, and sessionStorage
|
|
8
|
+
* snapshots from the target browser via CDP commands.
|
|
9
|
+
*/
|
|
10
|
+
export class StorageCollector extends EventEmitter {
|
|
11
|
+
private webContents: WebContents | null = null
|
|
12
|
+
private sessionId: string | null = null
|
|
13
|
+
private timer: ReturnType<typeof setInterval> | null = null
|
|
14
|
+
private collecting = false
|
|
15
|
+
|
|
16
|
+
start(sessionId: string, webContents: WebContents): void {
|
|
17
|
+
this.sessionId = sessionId
|
|
18
|
+
this.webContents = webContents
|
|
19
|
+
this.collectAll()
|
|
20
|
+
this.timer = setInterval(() => { this.collectAll() }, COLLECTION_INTERVAL)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
stop(): void {
|
|
24
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null }
|
|
25
|
+
if (this.webContents && !this.webContents.isDestroyed()) this.collectAll()
|
|
26
|
+
this.webContents = null
|
|
27
|
+
this.sessionId = null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
triggerCollection(): void { this.collectAll() }
|
|
31
|
+
|
|
32
|
+
private async collectAll(): Promise<void> {
|
|
33
|
+
if (this.collecting || !this.webContents || !this.sessionId) return
|
|
34
|
+
if (this.webContents.isDestroyed()) return
|
|
35
|
+
this.collecting = true
|
|
36
|
+
const domain = this.getCurrentDomain()
|
|
37
|
+
const timestamp = Date.now()
|
|
38
|
+
try {
|
|
39
|
+
await Promise.allSettled([
|
|
40
|
+
this.collectCookies(domain, timestamp),
|
|
41
|
+
this.collectLocalStorage(domain, timestamp),
|
|
42
|
+
this.collectSessionStorage(domain, timestamp)
|
|
43
|
+
])
|
|
44
|
+
} finally { this.collecting = false }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async collectCookies(domain: string, timestamp: number): Promise<void> {
|
|
48
|
+
if (!this.webContents || this.webContents.isDestroyed()) return
|
|
49
|
+
try {
|
|
50
|
+
const currentUrl = this.webContents.getURL()
|
|
51
|
+
const result = await this.webContents.debugger.sendCommand('Network.getCookies', { urls: [currentUrl] }) as { cookies: Array<Record<string, unknown>> }
|
|
52
|
+
this.emit('storage-collected', { domain, storageType: 'cookie', data: JSON.stringify(result.cookies || []), timestamp })
|
|
53
|
+
} catch (err) { console.warn('[StorageCollector] collectCookies failed:', (err as Error).message) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async collectLocalStorage(domain: string, timestamp: number): Promise<void> {
|
|
57
|
+
if (!this.webContents || this.webContents.isDestroyed()) return
|
|
58
|
+
try {
|
|
59
|
+
const result = await this.webContents.debugger.sendCommand('Runtime.evaluate', { expression: 'JSON.stringify(localStorage)', returnByValue: true }) as { result: { value?: string } }
|
|
60
|
+
this.emit('storage-collected', { domain, storageType: 'localStorage', data: result.result?.value || '{}', timestamp })
|
|
61
|
+
} catch (err) { console.warn('[StorageCollector] collectLocalStorage failed:', (err as Error).message) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async collectSessionStorage(domain: string, timestamp: number): Promise<void> {
|
|
65
|
+
if (!this.webContents || this.webContents.isDestroyed()) return
|
|
66
|
+
try {
|
|
67
|
+
const result = await this.webContents.debugger.sendCommand('Runtime.evaluate', { expression: 'JSON.stringify(sessionStorage)', returnByValue: true }) as { result: { value?: string } }
|
|
68
|
+
this.emit('storage-collected', { domain, storageType: 'sessionStorage', data: result.result?.value || '{}', timestamp })
|
|
69
|
+
} catch (err) { console.warn('[StorageCollector] collectSessionStorage failed:', (err as Error).message) }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private getCurrentDomain(): string {
|
|
73
|
+
if (!this.webContents || this.webContents.isDestroyed()) return 'unknown'
|
|
74
|
+
try { return new URL(this.webContents.getURL()).hostname } catch { return 'unknown' }
|
|
75
|
+
}
|
|
76
|
+
}
|