@jshookmcp/jshook 0.1.5 → 0.1.7
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/README.md +72 -40
- package/README.zh.md +77 -40
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +13 -1
- package/dist/index.js +0 -0
- package/dist/modules/analyzer/IntelligentAnalyzer.js +19 -11
- package/dist/modules/browser/BrowserModeManager.d.ts +5 -0
- package/dist/modules/browser/BrowserModeManager.js +96 -10
- package/dist/modules/browser/CamoufoxBrowserManager.d.ts +4 -0
- package/dist/modules/browser/CamoufoxBrowserManager.js +64 -3
- package/dist/modules/browser/TabRegistry.js +3 -2
- package/dist/modules/browser/UnifiedBrowserManager.d.ts +5 -0
- package/dist/modules/browser/UnifiedBrowserManager.js +62 -9
- package/dist/modules/debugger/DebuggerSessionManager.d.ts +4 -0
- package/dist/modules/debugger/DebuggerSessionManager.js +29 -19
- package/dist/modules/debugger/ScriptManager.impl.class.d.ts +4 -0
- package/dist/modules/debugger/ScriptManager.impl.class.js +46 -21
- package/dist/modules/emulator/EnvironmentEmulator.js +2 -2
- package/dist/modules/monitor/NetworkMonitor.impl.d.ts +1 -0
- package/dist/modules/monitor/NetworkMonitor.impl.js +22 -15
- package/dist/modules/monitor/PerformanceMonitor.js +64 -32
- package/dist/modules/process/LinuxProcessManager.d.ts +3 -1
- package/dist/modules/process/LinuxProcessManager.js +7 -3
- package/dist/modules/process/MacProcessManager.d.ts +3 -1
- package/dist/modules/process/MacProcessManager.js +7 -3
- package/dist/modules/process/ProcessManager.impl.d.ts +5 -1
- package/dist/modules/process/ProcessManager.impl.js +54 -13
- package/dist/modules/process/index.d.ts +3 -1
- package/dist/modules/process/index.js +2 -2
- package/dist/modules/process/memory/AuditTrail.d.ts +25 -0
- package/dist/modules/process/memory/AuditTrail.js +44 -0
- package/dist/modules/process/memory/linux/mapsParser.d.ts +16 -0
- package/dist/modules/process/memory/linux/mapsParser.js +28 -0
- package/dist/modules/process/memory/regions.enumerate.js +45 -1
- package/dist/modules/process/memory/regions.protection.js +48 -2
- package/dist/modules/process/memory/scanner.d.ts +4 -1
- package/dist/modules/process/memory/scanner.js +225 -24
- package/dist/native/NativeMemoryManager.impl.d.ts +4 -0
- package/dist/native/NativeMemoryManager.impl.js +72 -24
- package/dist/native/NativeMemoryManager.utils.d.ts +1 -0
- package/dist/native/NativeMemoryManager.utils.js +44 -1
- package/dist/server/MCPServer.search.d.ts +3 -0
- package/dist/server/MCPServer.search.js +21 -2
- package/dist/server/ToolCallContextGuard.d.ts +2 -0
- package/dist/server/ToolCallContextGuard.js +29 -14
- package/dist/server/ToolSearch.js +11 -5
- package/dist/server/domains/browser/handlers/tab-workflow.js +6 -4
- package/dist/server/domains/maintenance/handlers.extensions.js +46 -26
- package/dist/server/domains/process/definitions.js +20 -7
- package/dist/server/domains/process/handlers.impl.core.runtime.base.d.ts +35 -0
- package/dist/server/domains/process/handlers.impl.core.runtime.base.js +107 -1
- package/dist/server/domains/process/handlers.impl.core.runtime.inject.js +111 -2
- package/dist/server/domains/process/handlers.impl.core.runtime.memory.d.ts +9 -0
- package/dist/server/domains/process/handlers.impl.core.runtime.memory.js +282 -31
- package/dist/server/domains/process/manifest.js +1 -0
- package/dist/server/domains/workflow/handlers.impl.workflow-api.js +14 -4
- package/dist/server/registry/discovery.js +17 -12
- package/dist/server/registry/index.js +10 -2
- package/dist/utils/TokenBudgetManager.d.ts +1 -0
- package/dist/utils/TokenBudgetManager.js +22 -0
- package/package.json +28 -41
|
@@ -7,6 +7,8 @@ export class BrowserModeManager {
|
|
|
7
7
|
browser = null;
|
|
8
8
|
currentPage = null;
|
|
9
9
|
isHeadless = true;
|
|
10
|
+
isClosing = false;
|
|
11
|
+
launchPromise;
|
|
10
12
|
config;
|
|
11
13
|
captchaDetector;
|
|
12
14
|
launchOptions;
|
|
@@ -24,6 +26,27 @@ export class BrowserModeManager {
|
|
|
24
26
|
this.launchOptions = launchOptions;
|
|
25
27
|
}
|
|
26
28
|
async launch() {
|
|
29
|
+
if (this.browser?.isConnected()) {
|
|
30
|
+
return this.browser;
|
|
31
|
+
}
|
|
32
|
+
if (this.isClosing) {
|
|
33
|
+
throw new Error('Cannot launch browser while closing');
|
|
34
|
+
}
|
|
35
|
+
if (this.launchPromise) {
|
|
36
|
+
return this.launchPromise;
|
|
37
|
+
}
|
|
38
|
+
const launchPromise = this.doLaunch();
|
|
39
|
+
this.launchPromise = launchPromise;
|
|
40
|
+
try {
|
|
41
|
+
return await launchPromise;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (this.launchPromise === launchPromise) {
|
|
45
|
+
this.launchPromise = undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async doLaunch() {
|
|
27
50
|
const headlessMode = this.isHeadless;
|
|
28
51
|
const executablePath = this.resolveExecutablePath();
|
|
29
52
|
logger.info(`Launching browser (${headlessMode ? 'headless' : 'headed'} mode)...`);
|
|
@@ -43,7 +66,14 @@ export class BrowserModeManager {
|
|
|
43
66
|
if (executablePath) {
|
|
44
67
|
options.executablePath = executablePath;
|
|
45
68
|
}
|
|
46
|
-
|
|
69
|
+
const browser = await puppeteer.launch(options);
|
|
70
|
+
if (this.isClosing) {
|
|
71
|
+
await browser.close().catch(error => {
|
|
72
|
+
logger.warn('Failed to close browser launched during shutdown', error);
|
|
73
|
+
});
|
|
74
|
+
throw new Error('Browser launch aborted because close was requested');
|
|
75
|
+
}
|
|
76
|
+
this.browser = browser;
|
|
47
77
|
logger.info('Browser launched successfully');
|
|
48
78
|
return this.browser;
|
|
49
79
|
}
|
|
@@ -64,10 +94,8 @@ export class BrowserModeManager {
|
|
|
64
94
|
return undefined;
|
|
65
95
|
}
|
|
66
96
|
async newPage() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
const page = await this.browser.newPage();
|
|
97
|
+
const browser = this.browser?.isConnected() ? this.browser : await this.launch();
|
|
98
|
+
const page = await browser.newPage();
|
|
71
99
|
this.currentPage = page;
|
|
72
100
|
await this.injectAntiDetectionScripts(page);
|
|
73
101
|
if (this.sessionData.cookies && this.sessionData.cookies.length > 0) {
|
|
@@ -75,6 +103,20 @@ export class BrowserModeManager {
|
|
|
75
103
|
}
|
|
76
104
|
return page;
|
|
77
105
|
}
|
|
106
|
+
async finalizeClose() {
|
|
107
|
+
try {
|
|
108
|
+
const browser = this.browser;
|
|
109
|
+
this.browser = null;
|
|
110
|
+
this.currentPage = null;
|
|
111
|
+
if (browser) {
|
|
112
|
+
await browser.close();
|
|
113
|
+
logger.info('Browser closed');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
this.isClosing = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
78
120
|
async goto(url, page) {
|
|
79
121
|
const targetPage = page || this.currentPage;
|
|
80
122
|
if (!targetPage) {
|
|
@@ -111,6 +153,10 @@ export class BrowserModeManager {
|
|
|
111
153
|
await this.launch();
|
|
112
154
|
const newPage = await this.newPage();
|
|
113
155
|
await newPage.goto(url, { waitUntil: 'networkidle2' });
|
|
156
|
+
await this.restoreSessionData(newPage);
|
|
157
|
+
if (this.sessionData.localStorage || this.sessionData.sessionStorage) {
|
|
158
|
+
await newPage.reload({ waitUntil: 'networkidle2' });
|
|
159
|
+
}
|
|
114
160
|
this.showCaptchaPrompt(captchaInfo);
|
|
115
161
|
const completed = await this.captchaDetector.waitForCompletion(newPage, this.config.captchaTimeout);
|
|
116
162
|
if (completed) {
|
|
@@ -147,7 +193,10 @@ export class BrowserModeManager {
|
|
|
147
193
|
}
|
|
148
194
|
}
|
|
149
195
|
async saveSessionData(page) {
|
|
196
|
+
this.sessionData = {};
|
|
150
197
|
try {
|
|
198
|
+
const url = page.url();
|
|
199
|
+
this.sessionData.origin = url !== 'about:blank' ? new URL(url).origin : undefined;
|
|
151
200
|
this.sessionData.cookies = await page.cookies();
|
|
152
201
|
const storageData = await page.evaluate(() => {
|
|
153
202
|
const local = {};
|
|
@@ -174,6 +223,37 @@ export class BrowserModeManager {
|
|
|
174
223
|
logger.error('Failed to capture session data before mode switch', error);
|
|
175
224
|
}
|
|
176
225
|
}
|
|
226
|
+
async restoreSessionData(page) {
|
|
227
|
+
try {
|
|
228
|
+
const currentUrl = page.url();
|
|
229
|
+
const currentOrigin = currentUrl !== 'about:blank' ? new URL(currentUrl).origin : undefined;
|
|
230
|
+
if (this.sessionData.origin && currentOrigin && this.sessionData.origin !== currentOrigin) {
|
|
231
|
+
logger.warn(`Origin mismatch: session data from ${this.sessionData.origin} cannot be restored to ${currentOrigin}. ` +
|
|
232
|
+
'This prevents cross-origin data leakage.');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (this.sessionData.localStorage || this.sessionData.sessionStorage) {
|
|
236
|
+
await page.evaluate((data) => {
|
|
237
|
+
const restoreStorage = (storage, items) => {
|
|
238
|
+
if (items) {
|
|
239
|
+
for (const [key, value] of Object.entries(items)) {
|
|
240
|
+
storage.setItem(key, value);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
restoreStorage(localStorage, data.local);
|
|
245
|
+
restoreStorage(sessionStorage, data.session);
|
|
246
|
+
}, {
|
|
247
|
+
local: this.sessionData.localStorage,
|
|
248
|
+
session: this.sessionData.sessionStorage
|
|
249
|
+
});
|
|
250
|
+
logger.info('Session storage data restored');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
logger.error('Failed to restore session storage data', error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
177
257
|
async injectAntiDetectionScripts(page) {
|
|
178
258
|
await page.evaluateOnNewDocument(() => {
|
|
179
259
|
Object.defineProperty(navigator, 'webdriver', {
|
|
@@ -265,12 +345,18 @@ export class BrowserModeManager {
|
|
|
265
345
|
logger.info('Injected anti-detection scripts');
|
|
266
346
|
}
|
|
267
347
|
async close() {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
348
|
+
this.sessionData = {};
|
|
349
|
+
this.isClosing = true;
|
|
350
|
+
const pendingLaunch = this.launchPromise;
|
|
351
|
+
if (pendingLaunch) {
|
|
352
|
+
void pendingLaunch
|
|
353
|
+
.catch(() => undefined)
|
|
354
|
+
.finally(() => {
|
|
355
|
+
void this.finalizeClose();
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
273
358
|
}
|
|
359
|
+
await this.finalizeClose();
|
|
274
360
|
}
|
|
275
361
|
getBrowser() {
|
|
276
362
|
return this.browser;
|
|
@@ -30,11 +30,15 @@ export declare class CamoufoxBrowserManager {
|
|
|
30
30
|
private browser;
|
|
31
31
|
private browserServer;
|
|
32
32
|
private config;
|
|
33
|
+
private isClosing;
|
|
34
|
+
private launchPromise?;
|
|
33
35
|
constructor(config?: CamoufoxBrowserConfig);
|
|
34
36
|
launch(): Promise<CamoufoxBrowserLike>;
|
|
37
|
+
private doLaunch;
|
|
35
38
|
newPage(): Promise<CamoufoxPageLike>;
|
|
36
39
|
goto(url: string, page?: CamoufoxPageLike): Promise<CamoufoxPageLike>;
|
|
37
40
|
close(): Promise<void>;
|
|
41
|
+
private finalizeClose;
|
|
38
42
|
launchAsServer(port?: number, ws_path?: string): Promise<string>;
|
|
39
43
|
connectToServer(wsEndpoint: string): Promise<CamoufoxBrowserLike>;
|
|
40
44
|
closeBrowserServer(): Promise<void>;
|
|
@@ -4,6 +4,8 @@ export class CamoufoxBrowserManager {
|
|
|
4
4
|
browser = null;
|
|
5
5
|
browserServer = null;
|
|
6
6
|
config;
|
|
7
|
+
isClosing = false;
|
|
8
|
+
launchPromise;
|
|
7
9
|
constructor(config = {}) {
|
|
8
10
|
this.config = {
|
|
9
11
|
os: config.os ?? 'windows',
|
|
@@ -16,6 +18,29 @@ export class CamoufoxBrowserManager {
|
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
async launch() {
|
|
21
|
+
if (this.browser?.isConnected()) {
|
|
22
|
+
return this.browser;
|
|
23
|
+
}
|
|
24
|
+
if (this.isClosing) {
|
|
25
|
+
throw new Error('Cannot launch browser while closing');
|
|
26
|
+
}
|
|
27
|
+
if (this.launchPromise) {
|
|
28
|
+
return this.launchPromise;
|
|
29
|
+
}
|
|
30
|
+
this.launchPromise = this.doLaunch();
|
|
31
|
+
try {
|
|
32
|
+
return await this.launchPromise;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
this.launchPromise = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async doLaunch() {
|
|
39
|
+
if (this.browser) {
|
|
40
|
+
logger.info('Closing existing Camoufox browser before relaunch');
|
|
41
|
+
await this.browser.close().catch(err => logger.warn('Failed to close previous browser:', err));
|
|
42
|
+
this.browser = null;
|
|
43
|
+
}
|
|
19
44
|
logger.info(`Launching Camoufox (Firefox) [os=${this.config.os}, headless=${this.config.headless}]...`);
|
|
20
45
|
let Camoufox;
|
|
21
46
|
try {
|
|
@@ -33,6 +58,13 @@ export class CamoufoxBrowserManager {
|
|
|
33
58
|
block_images: this.config.blockImages,
|
|
34
59
|
block_webrtc: this.config.blockWebrtc,
|
|
35
60
|
}));
|
|
61
|
+
if (this.isClosing) {
|
|
62
|
+
await this.browser.close().catch(error => {
|
|
63
|
+
logger.warn('Failed to close Camoufox browser launched during shutdown:', error);
|
|
64
|
+
});
|
|
65
|
+
this.browser = null;
|
|
66
|
+
throw new Error('Camoufox launch aborted because close was requested');
|
|
67
|
+
}
|
|
36
68
|
logger.info('Camoufox browser launched');
|
|
37
69
|
return this.browser;
|
|
38
70
|
}
|
|
@@ -51,10 +83,29 @@ export class CamoufoxBrowserManager {
|
|
|
51
83
|
return targetPage;
|
|
52
84
|
}
|
|
53
85
|
async close() {
|
|
54
|
-
|
|
55
|
-
|
|
86
|
+
this.isClosing = true;
|
|
87
|
+
const pendingLaunch = this.launchPromise;
|
|
88
|
+
if (pendingLaunch) {
|
|
89
|
+
void pendingLaunch
|
|
90
|
+
.catch(() => undefined)
|
|
91
|
+
.finally(() => {
|
|
92
|
+
void this.finalizeClose();
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await this.finalizeClose();
|
|
97
|
+
}
|
|
98
|
+
async finalizeClose() {
|
|
99
|
+
try {
|
|
100
|
+
const browser = this.browser;
|
|
56
101
|
this.browser = null;
|
|
57
|
-
|
|
102
|
+
if (browser) {
|
|
103
|
+
await browser.close();
|
|
104
|
+
logger.info('Camoufox browser closed');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
this.isClosing = false;
|
|
58
109
|
}
|
|
59
110
|
}
|
|
60
111
|
async launchAsServer(port, ws_path) {
|
|
@@ -77,6 +128,11 @@ export class CamoufoxBrowserManager {
|
|
|
77
128
|
port,
|
|
78
129
|
ws_path,
|
|
79
130
|
};
|
|
131
|
+
if (this.browserServer) {
|
|
132
|
+
logger.info('Closing existing Camoufox server before relaunch');
|
|
133
|
+
await this.browserServer.close().catch(err => logger.warn('Failed to close previous server:', err));
|
|
134
|
+
this.browserServer = null;
|
|
135
|
+
}
|
|
80
136
|
this.browserServer = await launchServer(serverOptions);
|
|
81
137
|
const endpoint = this.browserServer.wsEndpoint();
|
|
82
138
|
logger.info(`Camoufox server listening on: ${endpoint}`);
|
|
@@ -84,6 +140,11 @@ export class CamoufoxBrowserManager {
|
|
|
84
140
|
}
|
|
85
141
|
async connectToServer(wsEndpoint) {
|
|
86
142
|
logger.info(`Connecting to Camoufox server: ${wsEndpoint}`);
|
|
143
|
+
if (this.browser) {
|
|
144
|
+
logger.info('Disconnecting existing browser before new connection');
|
|
145
|
+
await this.browser.close().catch(err => logger.warn('Failed to close previous browser:', err));
|
|
146
|
+
this.browser = null;
|
|
147
|
+
}
|
|
87
148
|
const playwrightModule = await import('playwright-core');
|
|
88
149
|
const firefox = playwrightModule.firefox;
|
|
89
150
|
this.browser = (await firefox.connect(wsEndpoint));
|
|
@@ -113,7 +113,8 @@ export class TabRegistry {
|
|
|
113
113
|
return null;
|
|
114
114
|
}
|
|
115
115
|
setCurrentPageId(pageId) {
|
|
116
|
-
|
|
116
|
+
const entry = this.tabsById.get(pageId);
|
|
117
|
+
if (!entry || entry.stale)
|
|
117
118
|
return false;
|
|
118
119
|
this.currentPageId = pageId;
|
|
119
120
|
return true;
|
|
@@ -167,7 +168,7 @@ export class TabRegistry {
|
|
|
167
168
|
url: current?.meta.url ?? null,
|
|
168
169
|
title: current?.meta.title ?? null,
|
|
169
170
|
tabIndex: current?.meta.index ?? null,
|
|
170
|
-
pageId: this.currentPageId,
|
|
171
|
+
pageId: current ? this.currentPageId : null,
|
|
171
172
|
};
|
|
172
173
|
}
|
|
173
174
|
listTabs() {
|
|
@@ -46,10 +46,15 @@ export declare class UnifiedBrowserManager implements IBrowserManager {
|
|
|
46
46
|
private camoufoxManager;
|
|
47
47
|
private browserDiscovery;
|
|
48
48
|
private activePage;
|
|
49
|
+
private chromeLaunchPromise?;
|
|
50
|
+
private camoufoxLaunchPromise?;
|
|
51
|
+
private isClosing;
|
|
49
52
|
constructor(config?: UnifiedBrowserConfig);
|
|
50
53
|
launch(): Promise<PuppeteerBrowser | CamoufoxBrowserLike>;
|
|
51
54
|
private launchChrome;
|
|
55
|
+
private doLaunchChrome;
|
|
52
56
|
private launchCamoufox;
|
|
57
|
+
private doLaunchCamoufox;
|
|
53
58
|
connect(wsEndpoint: string): Promise<PuppeteerBrowser | CamoufoxBrowserLike>;
|
|
54
59
|
private connectChrome;
|
|
55
60
|
private connectCamoufox;
|
|
@@ -9,6 +9,9 @@ export class UnifiedBrowserManager {
|
|
|
9
9
|
camoufoxManager = null;
|
|
10
10
|
browserDiscovery;
|
|
11
11
|
activePage = null;
|
|
12
|
+
chromeLaunchPromise;
|
|
13
|
+
camoufoxLaunchPromise;
|
|
14
|
+
isClosing = false;
|
|
12
15
|
constructor(config = {}) {
|
|
13
16
|
this.config = config;
|
|
14
17
|
this.driver = config.driver ?? 'chrome';
|
|
@@ -21,6 +24,25 @@ export class UnifiedBrowserManager {
|
|
|
21
24
|
return this.launchChrome();
|
|
22
25
|
}
|
|
23
26
|
async launchChrome() {
|
|
27
|
+
if (this.isClosing) {
|
|
28
|
+
throw new Error('Cannot launch browser while closing');
|
|
29
|
+
}
|
|
30
|
+
const existingBrowser = this.chromeManager?.getBrowser();
|
|
31
|
+
if (existingBrowser?.isConnected()) {
|
|
32
|
+
return existingBrowser;
|
|
33
|
+
}
|
|
34
|
+
if (this.chromeLaunchPromise) {
|
|
35
|
+
return this.chromeLaunchPromise;
|
|
36
|
+
}
|
|
37
|
+
this.chromeLaunchPromise = this.doLaunchChrome();
|
|
38
|
+
try {
|
|
39
|
+
return await this.chromeLaunchPromise;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
this.chromeLaunchPromise = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async doLaunchChrome() {
|
|
24
46
|
logger.info(`Launching Chrome [headless=${this.config.headless ?? true}]...`);
|
|
25
47
|
const modeConfig = {
|
|
26
48
|
autoDetectCaptcha: this.config.autoDetectCaptcha,
|
|
@@ -46,6 +68,25 @@ export class UnifiedBrowserManager {
|
|
|
46
68
|
return browser;
|
|
47
69
|
}
|
|
48
70
|
async launchCamoufox() {
|
|
71
|
+
if (this.isClosing) {
|
|
72
|
+
throw new Error('Cannot launch browser while closing');
|
|
73
|
+
}
|
|
74
|
+
const existingBrowser = this.camoufoxManager?.getBrowser();
|
|
75
|
+
if (existingBrowser?.isConnected()) {
|
|
76
|
+
return existingBrowser;
|
|
77
|
+
}
|
|
78
|
+
if (this.camoufoxLaunchPromise) {
|
|
79
|
+
return this.camoufoxLaunchPromise;
|
|
80
|
+
}
|
|
81
|
+
this.camoufoxLaunchPromise = this.doLaunchCamoufox();
|
|
82
|
+
try {
|
|
83
|
+
return await this.camoufoxLaunchPromise;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
this.camoufoxLaunchPromise = undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async doLaunchCamoufox() {
|
|
49
90
|
const headless = this.normalizeCamoufoxHeadless();
|
|
50
91
|
logger.info(`Launching Camoufox (Firefox) [os=${this.config.os ?? 'windows'}, headless=${headless}]...`);
|
|
51
92
|
const camoufoxConfig = {
|
|
@@ -114,18 +155,30 @@ export class UnifiedBrowserManager {
|
|
|
114
155
|
return this.chromeManager.goto(url, targetPage);
|
|
115
156
|
}
|
|
116
157
|
async close() {
|
|
117
|
-
|
|
118
|
-
|
|
158
|
+
this.isClosing = true;
|
|
159
|
+
try {
|
|
160
|
+
const camoufoxManager = this.camoufoxManager;
|
|
161
|
+
const chromeManager = this.chromeManager;
|
|
119
162
|
this.camoufoxManager = null;
|
|
120
|
-
this.activePage = null;
|
|
121
|
-
logger.info('Camoufox browser closed');
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (this.chromeManager) {
|
|
125
|
-
await this.chromeManager.close();
|
|
126
163
|
this.chromeManager = null;
|
|
164
|
+
this.chromeLaunchPromise = undefined;
|
|
165
|
+
this.camoufoxLaunchPromise = undefined;
|
|
127
166
|
this.activePage = null;
|
|
128
|
-
|
|
167
|
+
const closeTasks = [];
|
|
168
|
+
if (camoufoxManager) {
|
|
169
|
+
closeTasks.push(camoufoxManager.close().then(() => {
|
|
170
|
+
logger.info('Camoufox browser closed');
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
if (chromeManager) {
|
|
174
|
+
closeTasks.push(chromeManager.close().then(() => {
|
|
175
|
+
logger.info('Chrome browser closed');
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
await Promise.all(closeTasks);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
this.isClosing = false;
|
|
129
182
|
}
|
|
130
183
|
}
|
|
131
184
|
getBrowser() {
|
|
@@ -7,7 +7,11 @@ type SavedDebuggerSessionSummary = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare class DebuggerSessionManager {
|
|
9
9
|
private debuggerManager;
|
|
10
|
+
private readonly SESSION_IMPORT_BATCH_SIZE;
|
|
11
|
+
private readonly SESSION_FILE_READ_BATCH_SIZE;
|
|
10
12
|
constructor(debuggerManager: DebuggerManager);
|
|
13
|
+
private processInBatches;
|
|
14
|
+
private readSessionFile;
|
|
11
15
|
private validateFilePath;
|
|
12
16
|
exportSession(metadata?: DebuggerSession['metadata']): DebuggerSession;
|
|
13
17
|
saveSession(filePath?: string, metadata?: DebuggerSession['metadata']): Promise<string>;
|
|
@@ -4,9 +4,20 @@ import * as path from 'path';
|
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
export class DebuggerSessionManager {
|
|
6
6
|
debuggerManager;
|
|
7
|
+
SESSION_IMPORT_BATCH_SIZE = 8;
|
|
8
|
+
SESSION_FILE_READ_BATCH_SIZE = 8;
|
|
7
9
|
constructor(debuggerManager) {
|
|
8
10
|
this.debuggerManager = debuggerManager;
|
|
9
11
|
}
|
|
12
|
+
async processInBatches(items, batchSize, worker) {
|
|
13
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
14
|
+
const batch = items.slice(i, i + batchSize);
|
|
15
|
+
await Promise.all(batch.map((item) => worker(item)));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async readSessionFile(filePath) {
|
|
19
|
+
return fs.readFile(filePath, 'utf-8');
|
|
20
|
+
}
|
|
10
21
|
async validateFilePath(filePath) {
|
|
11
22
|
const resolved = path.resolve(filePath);
|
|
12
23
|
const cwd = await fs.realpath(process.cwd());
|
|
@@ -70,7 +81,7 @@ export class DebuggerSessionManager {
|
|
|
70
81
|
}
|
|
71
82
|
async loadSessionFromFile(filePath) {
|
|
72
83
|
const resolvedPath = await this.validateFilePath(filePath);
|
|
73
|
-
const content = await
|
|
84
|
+
const content = await this.readSessionFile(resolvedPath);
|
|
74
85
|
const session = JSON.parse(content);
|
|
75
86
|
await this.importSession(session);
|
|
76
87
|
logger.info(`Session loaded from ${resolvedPath}`, {
|
|
@@ -93,7 +104,7 @@ export class DebuggerSessionManager {
|
|
|
93
104
|
await this.debuggerManager.clearAllBreakpoints();
|
|
94
105
|
let successCount = 0;
|
|
95
106
|
let failCount = 0;
|
|
96
|
-
|
|
107
|
+
await this.processInBatches(session.breakpoints, this.SESSION_IMPORT_BATCH_SIZE, async (bp) => {
|
|
97
108
|
try {
|
|
98
109
|
if (bp.location.url) {
|
|
99
110
|
await this.debuggerManager.setBreakpointByUrl({
|
|
@@ -122,7 +133,7 @@ export class DebuggerSessionManager {
|
|
|
122
133
|
logger.error('Failed to restore breakpoint:', error, bp);
|
|
123
134
|
failCount++;
|
|
124
135
|
}
|
|
125
|
-
}
|
|
136
|
+
});
|
|
126
137
|
if (session.pauseOnExceptions) {
|
|
127
138
|
await this.debuggerManager.setPauseOnExceptions(session.pauseOnExceptions);
|
|
128
139
|
}
|
|
@@ -143,23 +154,22 @@ export class DebuggerSessionManager {
|
|
|
143
154
|
}
|
|
144
155
|
const files = await fs.readdir(sessionsDir);
|
|
145
156
|
const sessions = [];
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
logger.warn(`Failed to read session file ${file}:`, error);
|
|
160
|
-
}
|
|
157
|
+
const sessionFiles = files.filter((file) => file.endsWith('.json'));
|
|
158
|
+
await this.processInBatches(sessionFiles, this.SESSION_FILE_READ_BATCH_SIZE, async (file) => {
|
|
159
|
+
const filePath = path.join(sessionsDir, file);
|
|
160
|
+
try {
|
|
161
|
+
const content = await this.readSessionFile(filePath);
|
|
162
|
+
const session = JSON.parse(content);
|
|
163
|
+
sessions.push({
|
|
164
|
+
path: filePath,
|
|
165
|
+
timestamp: session.timestamp,
|
|
166
|
+
metadata: session.metadata,
|
|
167
|
+
});
|
|
161
168
|
}
|
|
162
|
-
|
|
169
|
+
catch (error) {
|
|
170
|
+
logger.warn(`Failed to read session file ${file}:`, error);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
163
173
|
sessions.sort((a, b) => b.timestamp - a.timestamp);
|
|
164
174
|
return sessions;
|
|
165
175
|
}
|
|
@@ -12,6 +12,9 @@ export interface ScriptInfo {
|
|
|
12
12
|
}
|
|
13
13
|
export declare class ScriptManager {
|
|
14
14
|
private collector;
|
|
15
|
+
private static readonly SOURCE_LOAD_BATCH_SIZE;
|
|
16
|
+
private static readonly SEARCH_LINE_YIELD_INTERVAL;
|
|
17
|
+
private static readonly SEARCH_SCRIPT_YIELD_INTERVAL;
|
|
15
18
|
private cdpSession;
|
|
16
19
|
private scripts;
|
|
17
20
|
private scriptsByUrl;
|
|
@@ -23,6 +26,7 @@ export declare class ScriptManager {
|
|
|
23
26
|
constructor(collector: CodeCollector);
|
|
24
27
|
init(): Promise<void>;
|
|
25
28
|
private doInit;
|
|
29
|
+
private loadScriptSourceInternal;
|
|
26
30
|
enable(): Promise<void>;
|
|
27
31
|
getAllScripts(includeSource?: boolean, maxScripts?: number): Promise<ScriptInfo[]>;
|
|
28
32
|
getScriptSource(scriptId?: string, url?: string): Promise<ScriptInfo | null>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { setImmediate as waitForImmediate } from 'node:timers/promises';
|
|
1
2
|
import { logger } from '../../utils/logger.js';
|
|
2
3
|
import { extractFunctionTreeCore, } from '../debugger/ScriptManager.impl.extract-function-tree.js';
|
|
3
4
|
export class ScriptManager {
|
|
4
5
|
collector;
|
|
6
|
+
static SOURCE_LOAD_BATCH_SIZE = 8;
|
|
7
|
+
static SEARCH_LINE_YIELD_INTERVAL = 250;
|
|
8
|
+
static SEARCH_SCRIPT_YIELD_INTERVAL = 10;
|
|
5
9
|
cdpSession = null;
|
|
6
10
|
scripts = new Map();
|
|
7
11
|
scriptsByUrl = new Map();
|
|
@@ -54,6 +58,25 @@ export class ScriptManager {
|
|
|
54
58
|
this.initialized = true;
|
|
55
59
|
logger.info('ScriptManager initialized');
|
|
56
60
|
}
|
|
61
|
+
async loadScriptSourceInternal(script) {
|
|
62
|
+
if (script.source) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const { scriptSource } = await this.cdpSession.send('Debugger.getScriptSource', {
|
|
67
|
+
scriptId: script.scriptId,
|
|
68
|
+
});
|
|
69
|
+
script.source = scriptSource;
|
|
70
|
+
script.sourceLength = scriptSource.length;
|
|
71
|
+
this.buildKeywordIndex(script.scriptId, script.url, scriptSource);
|
|
72
|
+
this.chunkScript(script.scriptId, scriptSource);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
logger.warn(`Failed to get source for script ${script.scriptId}:`, error);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
57
80
|
async enable() {
|
|
58
81
|
return this.init();
|
|
59
82
|
}
|
|
@@ -70,23 +93,27 @@ export class ScriptManager {
|
|
|
70
93
|
logger.warn(`Loading source code for ${limitedScripts.length} scripts. This may use significant memory.`);
|
|
71
94
|
let loadedCount = 0;
|
|
72
95
|
let failedCount = 0;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
script.source = scriptSource;
|
|
96
|
+
const missingScripts = limitedScripts.filter((script) => !script.source);
|
|
97
|
+
for (let batchStart = 0; batchStart < missingScripts.length; batchStart += ScriptManager.SOURCE_LOAD_BATCH_SIZE) {
|
|
98
|
+
const batch = missingScripts.slice(batchStart, batchStart + ScriptManager.SOURCE_LOAD_BATCH_SIZE);
|
|
99
|
+
const settled = await Promise.allSettled(batch.map(async (script) => {
|
|
100
|
+
const loaded = await this.loadScriptSourceInternal(script);
|
|
101
|
+
if (loaded) {
|
|
80
102
|
loadedCount++;
|
|
81
103
|
if (loadedCount % 10 === 0) {
|
|
82
104
|
logger.debug(`Loaded ${loadedCount}/${limitedScripts.length} scripts...`);
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
else {
|
|
108
|
+
failedCount++;
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
for (const result of settled) {
|
|
112
|
+
if (result.status === 'rejected') {
|
|
87
113
|
failedCount++;
|
|
88
114
|
}
|
|
89
115
|
}
|
|
116
|
+
await waitForImmediate();
|
|
90
117
|
}
|
|
91
118
|
logger.info(`getAllScripts: ${limitedScripts.length} scripts (loaded: ${loadedCount}, failed: ${failedCount})`);
|
|
92
119
|
}
|
|
@@ -121,17 +148,9 @@ export class ScriptManager {
|
|
|
121
148
|
return null;
|
|
122
149
|
}
|
|
123
150
|
if (!targetScript.source) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
128
|
-
targetScript.source = scriptSource;
|
|
129
|
-
targetScript.sourceLength = scriptSource.length;
|
|
130
|
-
this.buildKeywordIndex(targetScript.scriptId, targetScript.url, scriptSource);
|
|
131
|
-
this.chunkScript(targetScript.scriptId, scriptSource);
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
logger.error(`Failed to get script source for ${targetScript.scriptId}:`, error);
|
|
151
|
+
const loaded = await this.loadScriptSourceInternal(targetScript);
|
|
152
|
+
if (!loaded) {
|
|
153
|
+
logger.error(`Failed to get script source for ${targetScript.scriptId}`);
|
|
135
154
|
return null;
|
|
136
155
|
}
|
|
137
156
|
}
|
|
@@ -166,7 +185,7 @@ export class ScriptManager {
|
|
|
166
185
|
: new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), caseSensitive ? 'g' : 'gi');
|
|
167
186
|
const matches = [];
|
|
168
187
|
const scripts = await this.getAllScripts(true, 500);
|
|
169
|
-
for (const script of scripts) {
|
|
188
|
+
for (const [scriptIndex, script] of scripts.entries()) {
|
|
170
189
|
if (!script.source)
|
|
171
190
|
continue;
|
|
172
191
|
if (matches.length >= maxMatches)
|
|
@@ -193,6 +212,12 @@ export class ScriptManager {
|
|
|
193
212
|
context,
|
|
194
213
|
});
|
|
195
214
|
}
|
|
215
|
+
if ((i + 1) % ScriptManager.SEARCH_LINE_YIELD_INTERVAL === 0) {
|
|
216
|
+
await waitForImmediate();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if ((scriptIndex + 1) % ScriptManager.SEARCH_SCRIPT_YIELD_INTERVAL === 0) {
|
|
220
|
+
await waitForImmediate();
|
|
196
221
|
}
|
|
197
222
|
}
|
|
198
223
|
logger.info(`searchInScripts: "${keyword}" - found ${matches.length} matches`);
|
|
@@ -12,8 +12,8 @@ export class EnvironmentEmulator {
|
|
|
12
12
|
llm;
|
|
13
13
|
constructor(llm) {
|
|
14
14
|
this.llm = llm;
|
|
15
|
-
if (llm) {
|
|
16
|
-
logger.
|
|
15
|
+
if (!llm) {
|
|
16
|
+
logger.debug('LLM service unavailable, skipping AI environment analysis');
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
async analyze(options) {
|