@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,724 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { ipcMain, session as electronSession } from "electron";
|
|
3
|
+
import type { WebContents, Session as ElectronSession } from "electron";
|
|
4
|
+
import type { Session, ProxyConfig, RawInteractionData } from "@shared/types";
|
|
5
|
+
import type { SessionsRepo } from "../db/repositories";
|
|
6
|
+
import type { TabManager } from "../tab-manager";
|
|
7
|
+
import { CdpManager } from "../cdp/cdp-manager";
|
|
8
|
+
import { CaptureEngine } from "../capture/capture-engine";
|
|
9
|
+
import { JsInjector } from "../capture/js-injector";
|
|
10
|
+
import { StorageCollector } from "../capture/storage-collector";
|
|
11
|
+
import { InteractionRecorder } from "../capture/interaction-recorder";
|
|
12
|
+
import type { InteractionEventsRepo } from "../db/repositories";
|
|
13
|
+
import type { ProfileStore } from '../fingerprint/profile-store';
|
|
14
|
+
import { buildStealthScript } from '../../preload/stealth-script';
|
|
15
|
+
import { applyHttpSpoofing, removeHttpSpoofing } from '../fingerprint/http-spoofing';
|
|
16
|
+
|
|
17
|
+
/** Per-tab capture bundle: CDP + JS hooks + storage + stealth cleanup */
|
|
18
|
+
interface TabCaptureBundle {
|
|
19
|
+
cdp: CdpManager;
|
|
20
|
+
injector: JsInjector;
|
|
21
|
+
storage: StorageCollector;
|
|
22
|
+
stealthCleanup?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SessionManager — Manages the lifecycle of capture sessions.
|
|
27
|
+
* Coordinates per-tab CDP, JS injection, storage collection, and capture engine.
|
|
28
|
+
* Also provides standalone stealth (fingerprint) mode independent of capture.
|
|
29
|
+
*/
|
|
30
|
+
export class SessionManager {
|
|
31
|
+
private currentSessionId: string | null = null;
|
|
32
|
+
private tabManager: TabManager | null = null;
|
|
33
|
+
private tabCaptures = new Map<string, TabCaptureBundle>();
|
|
34
|
+
|
|
35
|
+
/** Cached Electron partition sessions keyed by app session ID */
|
|
36
|
+
private electronSessions = new Map<string, ElectronSession>();
|
|
37
|
+
/** The app session ID currently driving the browser partition */
|
|
38
|
+
private activePartitionSessionId: string | null = null;
|
|
39
|
+
|
|
40
|
+
/** Global hook IPC handler (registered once per session) */
|
|
41
|
+
private hookIpcHandler:
|
|
42
|
+
| ((event: Electron.IpcMainEvent, data: unknown) => void)
|
|
43
|
+
| null = null;
|
|
44
|
+
/** Interaction recording IPC handler */
|
|
45
|
+
private interactionIpcHandler:
|
|
46
|
+
| ((event: Electron.IpcMainEvent, data: unknown) => void)
|
|
47
|
+
| null = null;
|
|
48
|
+
/** Per-session interaction recorder instance */
|
|
49
|
+
private interactionRecorder: InteractionRecorder | null = null;
|
|
50
|
+
/** TabManager event listeners */
|
|
51
|
+
private tabCreatedHandler:
|
|
52
|
+
| ((tabInfo: { id: string; url: string; title: string }) => void)
|
|
53
|
+
| null = null;
|
|
54
|
+
private tabClosedHandler: ((data: { tabId: string }) => void) | null = null;
|
|
55
|
+
|
|
56
|
+
/** Standalone stealth mode — event-based fingerprint injection (no CDP) */
|
|
57
|
+
private stealthSessionId: string | null = null;
|
|
58
|
+
private stealthTabManager: TabManager | null = null;
|
|
59
|
+
private stealthCleanups = new Map<string, () => void>();
|
|
60
|
+
private stealthTabCreatedHandler:
|
|
61
|
+
| ((tabInfo: { id: string; url: string; title: string }) => void)
|
|
62
|
+
| null = null;
|
|
63
|
+
private stealthTabClosedHandler: ((data: { tabId: string }) => void) | null = null;
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
private sessionsRepo: SessionsRepo,
|
|
67
|
+
private captureEngine: CaptureEngine,
|
|
68
|
+
private profileStore?: ProfileStore,
|
|
69
|
+
private interactionEventsRepo?: InteractionEventsRepo,
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
// =============================================
|
|
73
|
+
// Partition Session Management
|
|
74
|
+
// =============================================
|
|
75
|
+
|
|
76
|
+
/** Get or create an isolated Electron session for the given app session. */
|
|
77
|
+
private getElectronSession(sessionId: string): ElectronSession {
|
|
78
|
+
if (!this.electronSessions.has(sessionId)) {
|
|
79
|
+
this.electronSessions.set(
|
|
80
|
+
sessionId,
|
|
81
|
+
electronSession.fromPartition(`persist:session-${sessionId}`),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return this.electronSessions.get(sessionId)!;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Return the Electron session for the currently active app session (capture or stealth). */
|
|
88
|
+
getActiveElectronSession(): ElectronSession | null {
|
|
89
|
+
const activeId = this.currentSessionId ?? this.stealthSessionId;
|
|
90
|
+
if (!activeId) return null;
|
|
91
|
+
return this.getElectronSession(activeId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Apply proxy config to an Electron session. */
|
|
95
|
+
private async applyProxyToSession(
|
|
96
|
+
elSession: ElectronSession,
|
|
97
|
+
config: ProxyConfig | null,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
if (!config || config.type === "none") {
|
|
100
|
+
await elSession.setProxy({ mode: "direct" });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Chromium proxyRules do NOT support inline credentials (user:pass@host)
|
|
105
|
+
// — that causes ERR_NO_SUPPORTED_PROXIES. Use plain host:port instead.
|
|
106
|
+
// Proxy auth is handled via app.on('login') in index.ts.
|
|
107
|
+
await elSession.setProxy({
|
|
108
|
+
proxyRules: `${config.type}://${config.host}:${config.port}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Switch the browser environment to a specific session's partition.
|
|
114
|
+
* Uses TabManager's session group to hide/restore tabs instead of destroying them.
|
|
115
|
+
*/
|
|
116
|
+
private async switchBrowserToSession(
|
|
117
|
+
sessionId: string,
|
|
118
|
+
tabManager: TabManager,
|
|
119
|
+
proxyConfig?: ProxyConfig | null,
|
|
120
|
+
): Promise<boolean> {
|
|
121
|
+
if (this.activePartitionSessionId === sessionId) return false;
|
|
122
|
+
|
|
123
|
+
const elSession = this.getElectronSession(sessionId);
|
|
124
|
+
|
|
125
|
+
// Apply upstream proxy to the new partition (before tabs open)
|
|
126
|
+
if (proxyConfig !== undefined) {
|
|
127
|
+
await this.applyProxyToSession(elSession, proxyConfig ?? null);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const createdNew = tabManager.switchSessionGroup(sessionId, elSession);
|
|
131
|
+
this.activePartitionSessionId = sessionId;
|
|
132
|
+
return createdNew;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a new session record.
|
|
137
|
+
*/
|
|
138
|
+
createSession(name: string, targetUrl: string): Session {
|
|
139
|
+
const session: Session = {
|
|
140
|
+
id: uuidv4(),
|
|
141
|
+
name,
|
|
142
|
+
target_url: targetUrl,
|
|
143
|
+
status: "stopped",
|
|
144
|
+
created_at: Date.now(),
|
|
145
|
+
stopped_at: null,
|
|
146
|
+
};
|
|
147
|
+
this.sessionsRepo.insert(session);
|
|
148
|
+
// Auto-generate fingerprint profile for the new session
|
|
149
|
+
if (this.profileStore) {
|
|
150
|
+
this.profileStore.getOrCreate(session.id);
|
|
151
|
+
}
|
|
152
|
+
return session;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Start capturing on a session. Attaches capture pipelines to all existing tabs
|
|
157
|
+
* and auto-attaches to new tabs created during the session.
|
|
158
|
+
*/
|
|
159
|
+
async startCapture(
|
|
160
|
+
sessionId: string,
|
|
161
|
+
tabManager: TabManager,
|
|
162
|
+
rendererWebContents: WebContents,
|
|
163
|
+
proxyConfig?: ProxyConfig | null,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
const session = this.sessionsRepo.findById(sessionId);
|
|
166
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
167
|
+
|
|
168
|
+
// Stop any running capture first
|
|
169
|
+
if (this.currentSessionId) {
|
|
170
|
+
await this.stopCapture(this.currentSessionId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Suspend standalone stealth listeners — full capture pipeline includes stealth injection
|
|
174
|
+
if (this.stealthSessionId) {
|
|
175
|
+
this.suspendStealthListeners();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Switch browser to this session's isolated partition
|
|
179
|
+
await this.switchBrowserToSession(sessionId, tabManager, proxyConfig);
|
|
180
|
+
|
|
181
|
+
this.currentSessionId = sessionId;
|
|
182
|
+
this.tabManager = tabManager;
|
|
183
|
+
|
|
184
|
+
// Start capture engine
|
|
185
|
+
this.captureEngine.start(sessionId, rendererWebContents);
|
|
186
|
+
|
|
187
|
+
// Apply fingerprint HTTP spoofing to the session's partition
|
|
188
|
+
if (this.profileStore) {
|
|
189
|
+
const profile = this.profileStore.getOrCreate(sessionId);
|
|
190
|
+
applyHttpSpoofing(this.getElectronSession(sessionId), profile);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Register global hook IPC listener (once for all tabs)
|
|
194
|
+
this.hookIpcHandler = (_event, data) => {
|
|
195
|
+
const hookData = data as {
|
|
196
|
+
type: string;
|
|
197
|
+
hookType: string;
|
|
198
|
+
functionName: string;
|
|
199
|
+
arguments: string;
|
|
200
|
+
result: string | null;
|
|
201
|
+
callStack: string | null;
|
|
202
|
+
timestamp: number;
|
|
203
|
+
};
|
|
204
|
+
if (hookData.type === "ar-hook") {
|
|
205
|
+
this.captureEngine.handleHookCaptured({
|
|
206
|
+
hookType: hookData.hookType,
|
|
207
|
+
functionName: hookData.functionName,
|
|
208
|
+
arguments: hookData.arguments,
|
|
209
|
+
result: hookData.result,
|
|
210
|
+
callStack: hookData.callStack,
|
|
211
|
+
timestamp: hookData.timestamp,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
ipcMain.on("capture:hook-data", this.hookIpcHandler);
|
|
216
|
+
|
|
217
|
+
// Register interaction recording IPC handler
|
|
218
|
+
if (this.interactionEventsRepo) {
|
|
219
|
+
this.interactionRecorder = new InteractionRecorder(this.interactionEventsRepo);
|
|
220
|
+
this.interactionIpcHandler = (_event, data) => {
|
|
221
|
+
const msg = data as { type: string } & Record<string, unknown>;
|
|
222
|
+
if (msg.type === 'ar-interaction') {
|
|
223
|
+
this.interactionRecorder?.handleInteraction({
|
|
224
|
+
type: msg.interactionType as RawInteractionData['type'],
|
|
225
|
+
timestamp: msg.timestamp as number,
|
|
226
|
+
x: msg.x as number | undefined,
|
|
227
|
+
y: msg.y as number | undefined,
|
|
228
|
+
viewportX: msg.viewportX as number | undefined,
|
|
229
|
+
viewportY: msg.viewportY as number | undefined,
|
|
230
|
+
selector: msg.selector as string | undefined,
|
|
231
|
+
xpath: msg.xpath as string | undefined,
|
|
232
|
+
tagName: msg.tagName as string | undefined,
|
|
233
|
+
elementText: msg.elementText as string | undefined,
|
|
234
|
+
attributes: msg.attributes as Record<string, string> | undefined,
|
|
235
|
+
boundingRect: msg.boundingRect as RawInteractionData['boundingRect'],
|
|
236
|
+
inputValue: msg.inputValue as string | undefined,
|
|
237
|
+
key: msg.key as string | undefined,
|
|
238
|
+
scrollX: msg.scrollX as number | undefined,
|
|
239
|
+
scrollY: msg.scrollY as number | undefined,
|
|
240
|
+
scrollDX: msg.scrollDX as number | undefined,
|
|
241
|
+
scrollDY: msg.scrollDY as number | undefined,
|
|
242
|
+
url: msg.url as string,
|
|
243
|
+
pageTitle: msg.pageTitle as string | undefined,
|
|
244
|
+
path: msg.path as RawInteractionData['path'],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
ipcMain.on("capture:hook-data", this.interactionIpcHandler);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Start interaction recorder (before tab attachment so injectIntoWebContents works)
|
|
252
|
+
if (this.interactionRecorder) {
|
|
253
|
+
this.interactionRecorder.start(sessionId, rendererWebContents);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Attach capture pipelines to all existing tabs
|
|
257
|
+
for (const tab of tabManager.getAllTabs()) {
|
|
258
|
+
if (!tab.view.webContents.isDestroyed()) {
|
|
259
|
+
await this.attachCaptureToTab(tab.id, tab.view.webContents);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Auto-attach to new tabs
|
|
264
|
+
this.tabCreatedHandler = async (tabInfo) => {
|
|
265
|
+
const tab = tabManager.getAllTabs().find((t) => t.id === tabInfo.id);
|
|
266
|
+
if (tab && !tab.view.webContents.isDestroyed()) {
|
|
267
|
+
await this.attachCaptureToTab(tab.id, tab.view.webContents);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
this.tabClosedHandler = (data) => {
|
|
271
|
+
this.detachCaptureFromTab(data.tabId);
|
|
272
|
+
};
|
|
273
|
+
tabManager.on("tab-created", this.tabCreatedHandler);
|
|
274
|
+
tabManager.on("tab-closed", this.tabClosedHandler);
|
|
275
|
+
|
|
276
|
+
// Update session status
|
|
277
|
+
this.sessionsRepo.updateStatus(sessionId, "running");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Attach CDP, JS injector, and storage collector to a single tab.
|
|
282
|
+
* If CDP attachment fails (e.g. blank page, debugger conflict), the tab is
|
|
283
|
+
* silently skipped — proxy-based capture still works without CDP.
|
|
284
|
+
*/
|
|
285
|
+
private async attachCaptureToTab(
|
|
286
|
+
tabId: string,
|
|
287
|
+
webContents: WebContents,
|
|
288
|
+
): Promise<void> {
|
|
289
|
+
if (this.tabCaptures.has(tabId)) return;
|
|
290
|
+
if (webContents.isDestroyed()) return;
|
|
291
|
+
|
|
292
|
+
const cdp = new CdpManager();
|
|
293
|
+
const injector = new JsInjector();
|
|
294
|
+
const storage = new StorageCollector();
|
|
295
|
+
|
|
296
|
+
// Start CDP manager — non-fatal if it fails
|
|
297
|
+
try {
|
|
298
|
+
await cdp.start(webContents);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.warn(`[SessionManager] CDP attach failed for tab ${tabId}, skipping browser capture:`, (err as Error).message);
|
|
301
|
+
cdp.detach();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
cdp.on("response-captured", (data) => {
|
|
306
|
+
this.captureEngine.handleResponseCaptured(data);
|
|
307
|
+
});
|
|
308
|
+
cdp.on("frame-navigated", () => {
|
|
309
|
+
storage.triggerCollection();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Start JS injector (injection only, no IPC listener)
|
|
313
|
+
injector.start(webContents);
|
|
314
|
+
|
|
315
|
+
// Inject interaction recording script into this tab
|
|
316
|
+
if (this.interactionRecorder) {
|
|
317
|
+
this.interactionRecorder.injectIntoWebContents(webContents);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Inject stealth script via CDP — runs BEFORE any page JS (critical for WAF challenges)
|
|
321
|
+
let stealthCleanup: (() => void) | undefined;
|
|
322
|
+
if (this.profileStore) {
|
|
323
|
+
const profile = this.profileStore.getOrCreate(this.currentSessionId!);
|
|
324
|
+
const stealthJs = buildStealthScript(JSON.stringify(profile));
|
|
325
|
+
|
|
326
|
+
// Use Page.addScriptToEvaluateOnNewDocument for early injection
|
|
327
|
+
// This ensures stealth runs before any page JavaScript, including WAF challenge scripts
|
|
328
|
+
try {
|
|
329
|
+
await cdp.sendCommand('Page.addScriptToEvaluateOnNewDocument', { source: stealthJs });
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.warn('[SessionManager] Failed to register stealth via CDP:', (err as Error).message);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Also inject into current page immediately (for pages already loaded)
|
|
335
|
+
if (!webContents.isDestroyed()) {
|
|
336
|
+
webContents.executeJavaScript(stealthJs, true).catch(() => { /* page not ready */ });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
stealthCleanup = () => {
|
|
340
|
+
// CDP scripts are automatically removed when debugger detaches — no manual cleanup needed
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Start storage collector
|
|
345
|
+
storage.start(this.currentSessionId!, webContents);
|
|
346
|
+
storage.on("storage-collected", (data) => {
|
|
347
|
+
this.captureEngine.handleStorageCollected(data);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
this.tabCaptures.set(tabId, { cdp, injector, storage, stealthCleanup });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Detach and clean up capture pipeline for a tab.
|
|
355
|
+
*/
|
|
356
|
+
private detachCaptureFromTab(tabId: string): void {
|
|
357
|
+
const bundle = this.tabCaptures.get(tabId);
|
|
358
|
+
if (!bundle) return;
|
|
359
|
+
|
|
360
|
+
// Stop storage FIRST — its stop() does a final collectAll() that needs the debugger alive
|
|
361
|
+
bundle.storage.stop();
|
|
362
|
+
bundle.injector.stop();
|
|
363
|
+
bundle.stealthCleanup?.();
|
|
364
|
+
bundle.cdp.stop();
|
|
365
|
+
bundle.cdp.detach();
|
|
366
|
+
this.tabCaptures.delete(tabId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Pause capturing — stops interception on all tabs but keeps session open.
|
|
371
|
+
*/
|
|
372
|
+
async pauseCapture(sessionId: string): Promise<void> {
|
|
373
|
+
if (this.currentSessionId !== sessionId) return;
|
|
374
|
+
|
|
375
|
+
for (const bundle of this.tabCaptures.values()) {
|
|
376
|
+
bundle.storage.stop();
|
|
377
|
+
bundle.injector.stop();
|
|
378
|
+
await bundle.cdp.stop();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Pause interaction recorder
|
|
382
|
+
this.interactionRecorder?.pause();
|
|
383
|
+
|
|
384
|
+
this.sessionsRepo.updateStatus(sessionId, "paused");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Resume capturing after a pause — re-attaches capture pipelines to all tabs.
|
|
389
|
+
*/
|
|
390
|
+
async resumeCapture(sessionId: string): Promise<void> {
|
|
391
|
+
if (this.currentSessionId !== sessionId) return;
|
|
392
|
+
const session = this.sessionsRepo.findById(sessionId);
|
|
393
|
+
if (!session || session.status !== "paused") return;
|
|
394
|
+
|
|
395
|
+
// Detach stale bundles then re-attach fresh ones
|
|
396
|
+
for (const tabId of Array.from(this.tabCaptures.keys())) {
|
|
397
|
+
this.detachCaptureFromTab(tabId);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (this.tabManager) {
|
|
401
|
+
for (const tab of this.tabManager.getAllTabs()) {
|
|
402
|
+
if (!tab.view.webContents.isDestroyed()) {
|
|
403
|
+
await this.attachCaptureToTab(tab.id, tab.view.webContents);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Resume interaction recorder
|
|
409
|
+
this.interactionRecorder?.resume();
|
|
410
|
+
|
|
411
|
+
this.sessionsRepo.updateStatus(sessionId, "running");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Stop capturing and finalize the session.
|
|
416
|
+
*/
|
|
417
|
+
async stopCapture(sessionId: string): Promise<void> {
|
|
418
|
+
if (this.currentSessionId !== sessionId) return;
|
|
419
|
+
|
|
420
|
+
// Detach all tab capture pipelines
|
|
421
|
+
for (const tabId of Array.from(this.tabCaptures.keys())) {
|
|
422
|
+
this.detachCaptureFromTab(tabId);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Remove TabManager event listeners
|
|
426
|
+
if (this.tabManager) {
|
|
427
|
+
if (this.tabCreatedHandler)
|
|
428
|
+
this.tabManager.removeListener("tab-created", this.tabCreatedHandler);
|
|
429
|
+
if (this.tabClosedHandler)
|
|
430
|
+
this.tabManager.removeListener("tab-closed", this.tabClosedHandler);
|
|
431
|
+
}
|
|
432
|
+
this.tabCreatedHandler = null;
|
|
433
|
+
this.tabClosedHandler = null;
|
|
434
|
+
this.tabManager = null;
|
|
435
|
+
|
|
436
|
+
// Remove global hook IPC listener
|
|
437
|
+
if (this.hookIpcHandler) {
|
|
438
|
+
ipcMain.removeListener("capture:hook-data", this.hookIpcHandler);
|
|
439
|
+
this.hookIpcHandler = null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Stop interaction recorder
|
|
443
|
+
if (this.interactionIpcHandler) {
|
|
444
|
+
ipcMain.removeListener("capture:hook-data", this.interactionIpcHandler);
|
|
445
|
+
this.interactionIpcHandler = null;
|
|
446
|
+
}
|
|
447
|
+
if (this.interactionRecorder) {
|
|
448
|
+
this.interactionRecorder.stop();
|
|
449
|
+
this.interactionRecorder = null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.captureEngine.stop();
|
|
453
|
+
// Remove HTTP spoofing from the session's partition
|
|
454
|
+
removeHttpSpoofing(this.getElectronSession(sessionId));
|
|
455
|
+
this.sessionsRepo.updateStatus(sessionId, "stopped", Date.now());
|
|
456
|
+
this.currentSessionId = null;
|
|
457
|
+
|
|
458
|
+
// Restore standalone stealth if it was active before capture started
|
|
459
|
+
if (this.stealthSessionId && this.stealthTabManager) {
|
|
460
|
+
const profile = this.profileStore?.getOrCreate(this.stealthSessionId);
|
|
461
|
+
if (profile) {
|
|
462
|
+
applyHttpSpoofing(this.getElectronSession(this.stealthSessionId), profile);
|
|
463
|
+
}
|
|
464
|
+
this.restoreStealthListeners();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// =============================================
|
|
469
|
+
// Standalone Stealth (Fingerprint-Only) Mode
|
|
470
|
+
// Uses webContents events + executeJavaScript (no CDP debugger).
|
|
471
|
+
// CDP is only used during capture mode for early injection.
|
|
472
|
+
// =============================================
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Enable standalone stealth mode — applies fingerprint injection to all tabs
|
|
476
|
+
* WITHOUT starting capture. Uses webContents events (no CDP debugger attachment).
|
|
477
|
+
*/
|
|
478
|
+
async enableStealth(
|
|
479
|
+
sessionId: string,
|
|
480
|
+
tabManager: TabManager,
|
|
481
|
+
proxyConfig?: ProxyConfig | null,
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
if (!this.profileStore) return;
|
|
484
|
+
|
|
485
|
+
// If capture is running, stealth is already handled by the capture pipeline
|
|
486
|
+
if (this.currentSessionId) return;
|
|
487
|
+
|
|
488
|
+
// Disable previous stealth if switching sessions
|
|
489
|
+
if (this.stealthSessionId && this.stealthSessionId !== sessionId) {
|
|
490
|
+
await this.disableStealth();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Avoid re-enabling for the same session
|
|
494
|
+
if (this.stealthSessionId === sessionId) return;
|
|
495
|
+
|
|
496
|
+
this.stealthSessionId = sessionId;
|
|
497
|
+
this.stealthTabManager = tabManager;
|
|
498
|
+
|
|
499
|
+
// Switch browser to this session's isolated partition (hides old tabs, restores/creates new)
|
|
500
|
+
const createdNew = await this.switchBrowserToSession(sessionId, tabManager, proxyConfig);
|
|
501
|
+
|
|
502
|
+
// If this is the session's first visit (blank tab created), navigate to target URL
|
|
503
|
+
if (createdNew) {
|
|
504
|
+
const session = this.sessionsRepo.findById(sessionId);
|
|
505
|
+
if (session?.target_url) {
|
|
506
|
+
const wc = tabManager.getActiveWebContents();
|
|
507
|
+
if (wc && !wc.isDestroyed()) {
|
|
508
|
+
wc.loadURL(session.target_url).catch(() => {});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Apply HTTP-level spoofing to the session's partition
|
|
514
|
+
const elSession = this.getElectronSession(sessionId);
|
|
515
|
+
const profile = this.profileStore.getOrCreate(sessionId);
|
|
516
|
+
applyHttpSpoofing(elSession, profile);
|
|
517
|
+
|
|
518
|
+
// Attach stealth to all existing tabs
|
|
519
|
+
for (const tab of tabManager.getAllTabs()) {
|
|
520
|
+
if (!tab.view.webContents.isDestroyed()) {
|
|
521
|
+
this.attachStealthListeners(tab.id, tab.view.webContents);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Auto-attach/detach for new/closed tabs
|
|
526
|
+
this.stealthTabCreatedHandler = (tabInfo) => {
|
|
527
|
+
const tab = tabManager.getAllTabs().find((t) => t.id === tabInfo.id);
|
|
528
|
+
if (tab && !tab.view.webContents.isDestroyed()) {
|
|
529
|
+
this.attachStealthListeners(tab.id, tab.view.webContents);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
this.stealthTabClosedHandler = (data) => {
|
|
533
|
+
this.detachStealthListeners(data.tabId);
|
|
534
|
+
};
|
|
535
|
+
tabManager.on("tab-created", this.stealthTabCreatedHandler);
|
|
536
|
+
tabManager.on("tab-closed", this.stealthTabClosedHandler);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Disable standalone stealth mode — removes fingerprint injection from all tabs.
|
|
541
|
+
*/
|
|
542
|
+
async disableStealth(): Promise<void> {
|
|
543
|
+
// Detach all stealth listeners
|
|
544
|
+
for (const tabId of Array.from(this.stealthCleanups.keys())) {
|
|
545
|
+
this.detachStealthListeners(tabId);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Remove tab event listeners
|
|
549
|
+
if (this.stealthTabManager) {
|
|
550
|
+
if (this.stealthTabCreatedHandler)
|
|
551
|
+
this.stealthTabManager.removeListener("tab-created", this.stealthTabCreatedHandler);
|
|
552
|
+
if (this.stealthTabClosedHandler)
|
|
553
|
+
this.stealthTabManager.removeListener("tab-closed", this.stealthTabClosedHandler);
|
|
554
|
+
}
|
|
555
|
+
this.stealthTabCreatedHandler = null;
|
|
556
|
+
this.stealthTabClosedHandler = null;
|
|
557
|
+
this.stealthTabManager = null;
|
|
558
|
+
|
|
559
|
+
// Remove HTTP spoofing from the session's partition
|
|
560
|
+
if (this.stealthSessionId) {
|
|
561
|
+
removeHttpSpoofing(this.getElectronSession(this.stealthSessionId));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
this.stealthSessionId = null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Attach stealth injection via webContents navigation events (no CDP).
|
|
569
|
+
* Injects the stealth script on every navigation.
|
|
570
|
+
*/
|
|
571
|
+
private attachStealthListeners(
|
|
572
|
+
tabId: string,
|
|
573
|
+
webContents: WebContents,
|
|
574
|
+
): void {
|
|
575
|
+
if (this.stealthCleanups.has(tabId)) return;
|
|
576
|
+
if (!this.profileStore || !this.stealthSessionId) return;
|
|
577
|
+
|
|
578
|
+
const profile = this.profileStore.getOrCreate(this.stealthSessionId);
|
|
579
|
+
const stealthJs = buildStealthScript(JSON.stringify(profile));
|
|
580
|
+
|
|
581
|
+
const onNavigate = () => {
|
|
582
|
+
if (webContents.isDestroyed()) return;
|
|
583
|
+
webContents.executeJavaScript(stealthJs, true).catch(() => { /* page not ready or destroyed */ });
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
webContents.on("did-navigate", onNavigate);
|
|
587
|
+
webContents.on("did-navigate-in-page", onNavigate);
|
|
588
|
+
|
|
589
|
+
// Also inject into the current page immediately
|
|
590
|
+
if (!webContents.isDestroyed()) {
|
|
591
|
+
webContents.executeJavaScript(stealthJs, true).catch(() => { /* page not ready */ });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this.stealthCleanups.set(tabId, () => {
|
|
595
|
+
webContents.removeListener("did-navigate", onNavigate);
|
|
596
|
+
webContents.removeListener("did-navigate-in-page", onNavigate);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Detach stealth listeners from a single tab.
|
|
602
|
+
*/
|
|
603
|
+
private detachStealthListeners(tabId: string): void {
|
|
604
|
+
const cleanup = this.stealthCleanups.get(tabId);
|
|
605
|
+
if (cleanup) {
|
|
606
|
+
cleanup();
|
|
607
|
+
this.stealthCleanups.delete(tabId);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Temporarily suspend stealth listeners (before capture takes over).
|
|
613
|
+
*/
|
|
614
|
+
private suspendStealthListeners(): void {
|
|
615
|
+
for (const tabId of Array.from(this.stealthCleanups.keys())) {
|
|
616
|
+
this.detachStealthListeners(tabId);
|
|
617
|
+
}
|
|
618
|
+
// Remove tab listeners — capture will manage its own
|
|
619
|
+
if (this.stealthTabManager) {
|
|
620
|
+
if (this.stealthTabCreatedHandler)
|
|
621
|
+
this.stealthTabManager.removeListener("tab-created", this.stealthTabCreatedHandler);
|
|
622
|
+
if (this.stealthTabClosedHandler)
|
|
623
|
+
this.stealthTabManager.removeListener("tab-closed", this.stealthTabClosedHandler);
|
|
624
|
+
}
|
|
625
|
+
this.stealthTabCreatedHandler = null;
|
|
626
|
+
this.stealthTabClosedHandler = null;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Restore stealth listeners after capture stops (if stealth was active).
|
|
631
|
+
*/
|
|
632
|
+
private restoreStealthListeners(): void {
|
|
633
|
+
if (!this.stealthSessionId || !this.stealthTabManager) return;
|
|
634
|
+
|
|
635
|
+
const tabManager = this.stealthTabManager;
|
|
636
|
+
|
|
637
|
+
// Re-attach stealth to all tabs
|
|
638
|
+
for (const tab of tabManager.getAllTabs()) {
|
|
639
|
+
if (!tab.view.webContents.isDestroyed()) {
|
|
640
|
+
this.attachStealthListeners(tab.id, tab.view.webContents);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Re-register tab listeners
|
|
645
|
+
this.stealthTabCreatedHandler = (tabInfo) => {
|
|
646
|
+
const tab = tabManager.getAllTabs().find((t) => t.id === tabInfo.id);
|
|
647
|
+
if (tab && !tab.view.webContents.isDestroyed()) {
|
|
648
|
+
this.attachStealthListeners(tab.id, tab.view.webContents);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
this.stealthTabClosedHandler = (data) => {
|
|
652
|
+
this.detachStealthListeners(data.tabId);
|
|
653
|
+
};
|
|
654
|
+
tabManager.on("tab-created", this.stealthTabCreatedHandler);
|
|
655
|
+
tabManager.on("tab-closed", this.stealthTabClosedHandler);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
getStealthSessionId(): string | null {
|
|
659
|
+
return this.stealthSessionId;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* List all sessions.
|
|
664
|
+
*/
|
|
665
|
+
listSessions(): Session[] {
|
|
666
|
+
return this.sessionsRepo.findAll();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Delete a session.
|
|
671
|
+
*/
|
|
672
|
+
async deleteSession(sessionId: string, tabManager?: TabManager): Promise<void> {
|
|
673
|
+
if (this.currentSessionId === sessionId) {
|
|
674
|
+
await this.stopCapture(sessionId);
|
|
675
|
+
}
|
|
676
|
+
if (this.stealthSessionId === sessionId) {
|
|
677
|
+
await this.disableStealth();
|
|
678
|
+
}
|
|
679
|
+
// Destroy tabs belonging to this session
|
|
680
|
+
if (tabManager) {
|
|
681
|
+
tabManager.destroySessionGroup(sessionId);
|
|
682
|
+
}
|
|
683
|
+
// Clean up isolated browser data for this session's partition
|
|
684
|
+
const elSession = this.electronSessions.get(sessionId);
|
|
685
|
+
if (elSession) {
|
|
686
|
+
await elSession.clearStorageData().catch(() => {});
|
|
687
|
+
await elSession.clearCache().catch(() => {});
|
|
688
|
+
this.electronSessions.delete(sessionId);
|
|
689
|
+
}
|
|
690
|
+
if (this.activePartitionSessionId === sessionId) {
|
|
691
|
+
this.activePartitionSessionId = null;
|
|
692
|
+
}
|
|
693
|
+
this.sessionsRepo.delete(sessionId);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Recover from crash — mark any 'running' sessions as 'stopped'.
|
|
698
|
+
*/
|
|
699
|
+
recoverFromCrash(): void {
|
|
700
|
+
const sessions = this.sessionsRepo.findAll();
|
|
701
|
+
for (const session of sessions) {
|
|
702
|
+
if (session.status === "running" || session.status === "paused") {
|
|
703
|
+
this.sessionsRepo.updateStatus(session.id, "stopped", Date.now());
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
getCurrentSessionId(): string | null {
|
|
709
|
+
return this.currentSessionId;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Send a raw CDP command to the active tab's debugger.
|
|
714
|
+
* Requires an active capture session with CDP attached.
|
|
715
|
+
*/
|
|
716
|
+
async sendCdpCommand(method: string, params?: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
717
|
+
if (!this.tabManager) throw new Error("No active capture session");
|
|
718
|
+
const activeTab = this.tabManager.getActiveTab();
|
|
719
|
+
if (!activeTab) throw new Error("No active tab");
|
|
720
|
+
const bundle = this.tabCaptures.get(activeTab.id);
|
|
721
|
+
if (!bundle) throw new Error("CDP not attached to active tab");
|
|
722
|
+
return bundle.cdp.sendCommand(method, params || {});
|
|
723
|
+
}
|
|
724
|
+
}
|