@hasna/browser 0.0.7 → 0.1.0
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/dist/cli/index.js +2095 -609
- package/dist/db/sessions.d.ts +15 -0
- package/dist/db/sessions.d.ts.map +1 -1
- package/dist/db/timeline.d.ts +11 -0
- package/dist/db/timeline.d.ts.map +1 -0
- package/dist/engines/cdp.d.ts +2 -1
- package/dist/engines/cdp.d.ts.map +1 -1
- package/dist/engines/playwright.d.ts +1 -1
- package/dist/engines/playwright.d.ts.map +1 -1
- package/dist/index.js +614 -226
- package/dist/lib/actions.d.ts +22 -4
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/auth-flow.d.ts +43 -0
- package/dist/lib/auth-flow.d.ts.map +1 -0
- package/dist/lib/sanitize.d.ts +21 -0
- package/dist/lib/sanitize.d.ts.map +1 -0
- package/dist/lib/self-heal.d.ts +18 -0
- package/dist/lib/self-heal.d.ts.map +1 -0
- package/dist/lib/session.d.ts +7 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/storage-state.d.ts +15 -0
- package/dist/lib/storage-state.d.ts.map +1 -0
- package/dist/lib/vision-fallback.d.ts +29 -0
- package/dist/lib/vision-fallback.d.ts.map +1 -0
- package/dist/mcp/index.js +2480 -1041
- package/dist/server/index.js +472 -172
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/server/index.js
CHANGED
|
@@ -259,6 +259,55 @@ function runMigrations(db) {
|
|
|
259
259
|
CREATE INDEX IF NOT EXISTS idx_gallery_favorite ON gallery_entries(is_favorite);
|
|
260
260
|
CREATE INDEX IF NOT EXISTS idx_gallery_created ON gallery_entries(created_at);
|
|
261
261
|
`
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
version: 3,
|
|
265
|
+
sql: `
|
|
266
|
+
-- Session lock/claim for multi-agent ownership
|
|
267
|
+
ALTER TABLE sessions ADD COLUMN locked_by TEXT;
|
|
268
|
+
ALTER TABLE sessions ADD COLUMN locked_at TEXT;
|
|
269
|
+
`
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
version: 4,
|
|
273
|
+
sql: `
|
|
274
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
275
|
+
id TEXT PRIMARY KEY,
|
|
276
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
277
|
+
event_type TEXT NOT NULL,
|
|
278
|
+
details TEXT DEFAULT '{}',
|
|
279
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
280
|
+
);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id, timestamp);
|
|
282
|
+
`
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
version: 5,
|
|
286
|
+
sql: `
|
|
287
|
+
CREATE TABLE IF NOT EXISTS session_tags (
|
|
288
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
289
|
+
tag TEXT NOT NULL,
|
|
290
|
+
PRIMARY KEY (session_id, tag)
|
|
291
|
+
);
|
|
292
|
+
CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
|
|
293
|
+
`
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
version: 6,
|
|
297
|
+
sql: `
|
|
298
|
+
CREATE TABLE IF NOT EXISTS auth_flows (
|
|
299
|
+
id TEXT PRIMARY KEY,
|
|
300
|
+
name TEXT NOT NULL UNIQUE,
|
|
301
|
+
domain TEXT NOT NULL,
|
|
302
|
+
recording_id TEXT REFERENCES recordings(id),
|
|
303
|
+
storage_state_path TEXT,
|
|
304
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
305
|
+
last_used TEXT
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
|
|
309
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
|
|
310
|
+
`
|
|
262
311
|
}
|
|
263
312
|
];
|
|
264
313
|
for (const m of migrations) {
|
|
@@ -307,6 +356,188 @@ var init_console_log = __esm(() => {
|
|
|
307
356
|
init_schema();
|
|
308
357
|
});
|
|
309
358
|
|
|
359
|
+
// src/engines/cdp.ts
|
|
360
|
+
var exports_cdp = {};
|
|
361
|
+
__export(exports_cdp, {
|
|
362
|
+
connectToExistingBrowser: () => connectToExistingBrowser,
|
|
363
|
+
CDPClient: () => CDPClient
|
|
364
|
+
});
|
|
365
|
+
async function connectToExistingBrowser(cdpUrl) {
|
|
366
|
+
const { chromium: chromium3 } = await import("playwright");
|
|
367
|
+
try {
|
|
368
|
+
return await chromium3.connectOverCDP(cdpUrl);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
throw new BrowserError(`Failed to connect to browser at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}. Start Chrome with: google-chrome --remote-debugging-port=9222`, "CDP_CONNECT_FAILED", true);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
class CDPClient {
|
|
375
|
+
session;
|
|
376
|
+
networkEnabled = false;
|
|
377
|
+
performanceEnabled = false;
|
|
378
|
+
constructor(session) {
|
|
379
|
+
this.session = session;
|
|
380
|
+
}
|
|
381
|
+
static async fromPage(page) {
|
|
382
|
+
try {
|
|
383
|
+
const session = await page.context().newCDPSession(page);
|
|
384
|
+
return new CDPClient(session);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async send(method, params) {
|
|
390
|
+
try {
|
|
391
|
+
return await this.session.send(method, params);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
on(event, handler) {
|
|
397
|
+
this.session.on(event, handler);
|
|
398
|
+
}
|
|
399
|
+
off(event, handler) {
|
|
400
|
+
this.session.off(event, handler);
|
|
401
|
+
}
|
|
402
|
+
async enableNetwork() {
|
|
403
|
+
if (!this.networkEnabled) {
|
|
404
|
+
await this.send("Network.enable");
|
|
405
|
+
this.networkEnabled = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async enablePerformance() {
|
|
409
|
+
if (!this.performanceEnabled) {
|
|
410
|
+
await this.send("Performance.enable");
|
|
411
|
+
this.performanceEnabled = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async getPerformanceMetrics() {
|
|
415
|
+
await this.enablePerformance();
|
|
416
|
+
const result = await this.send("Performance.getMetrics");
|
|
417
|
+
const m = {};
|
|
418
|
+
for (const metric of result.metrics) {
|
|
419
|
+
m[metric.name] = metric.value;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
js_heap_size_used: m["JSHeapUsedSize"],
|
|
423
|
+
js_heap_size_total: m["JSHeapTotalSize"],
|
|
424
|
+
dom_interactive: m["DOMInteractive"],
|
|
425
|
+
dom_complete: m["DOMComplete"],
|
|
426
|
+
load_event: m["LoadEventEnd"]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async startJSCoverage() {
|
|
430
|
+
await this.send("Profiler.enable");
|
|
431
|
+
await this.send("Debugger.enable");
|
|
432
|
+
await this.send("Profiler.startPreciseCoverage", {
|
|
433
|
+
callCount: false,
|
|
434
|
+
detailed: true
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
async stopJSCoverage() {
|
|
438
|
+
const result = await this.send("Profiler.takePreciseCoverage");
|
|
439
|
+
await this.send("Profiler.stopPreciseCoverage");
|
|
440
|
+
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
441
|
+
url: r.url,
|
|
442
|
+
text: "",
|
|
443
|
+
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
async getCoverage() {
|
|
447
|
+
await this.startJSCoverage();
|
|
448
|
+
const js = await this.stopJSCoverage();
|
|
449
|
+
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
450
|
+
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
451
|
+
}
|
|
452
|
+
async captureHAREntries(page, handler) {
|
|
453
|
+
await this.enableNetwork();
|
|
454
|
+
const requestTimings = new Map;
|
|
455
|
+
const onRequest = (params) => {
|
|
456
|
+
requestTimings.set(params.requestId, params.timestamp);
|
|
457
|
+
};
|
|
458
|
+
const onResponse = (params) => {
|
|
459
|
+
const start = requestTimings.get(params.requestId);
|
|
460
|
+
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
461
|
+
handler({
|
|
462
|
+
method: "GET",
|
|
463
|
+
url: params.response.url,
|
|
464
|
+
status: params.response.status,
|
|
465
|
+
duration
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
this.on("Network.requestWillBeSent", onRequest);
|
|
469
|
+
this.on("Network.responseReceived", onResponse);
|
|
470
|
+
return () => {
|
|
471
|
+
this.off("Network.requestWillBeSent", onRequest);
|
|
472
|
+
this.off("Network.responseReceived", onResponse);
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async detach() {
|
|
476
|
+
try {
|
|
477
|
+
await this.session.detach();
|
|
478
|
+
} catch {}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
var init_cdp = __esm(() => {
|
|
482
|
+
init_types();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// src/lib/storage-state.ts
|
|
486
|
+
var exports_storage_state = {};
|
|
487
|
+
__export(exports_storage_state, {
|
|
488
|
+
saveStateFromPage: () => saveStateFromPage,
|
|
489
|
+
saveState: () => saveState,
|
|
490
|
+
loadStatePath: () => loadStatePath,
|
|
491
|
+
listStates: () => listStates,
|
|
492
|
+
deleteState: () => deleteState
|
|
493
|
+
});
|
|
494
|
+
import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
|
|
495
|
+
import { join as join3 } from "path";
|
|
496
|
+
import { homedir as homedir3 } from "os";
|
|
497
|
+
function ensureDir() {
|
|
498
|
+
mkdirSync3(STATES_DIR, { recursive: true });
|
|
499
|
+
}
|
|
500
|
+
function statePath(name) {
|
|
501
|
+
return join3(STATES_DIR, `${name}.json`);
|
|
502
|
+
}
|
|
503
|
+
async function saveState(context, name) {
|
|
504
|
+
ensureDir();
|
|
505
|
+
const path = statePath(name);
|
|
506
|
+
const state = await context.storageState({ path });
|
|
507
|
+
return path;
|
|
508
|
+
}
|
|
509
|
+
async function saveStateFromPage(page, name) {
|
|
510
|
+
return saveState(page.context(), name);
|
|
511
|
+
}
|
|
512
|
+
function loadStatePath(name) {
|
|
513
|
+
const path = statePath(name);
|
|
514
|
+
return existsSync(path) ? path : null;
|
|
515
|
+
}
|
|
516
|
+
function listStates() {
|
|
517
|
+
ensureDir();
|
|
518
|
+
return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
519
|
+
const path = join3(STATES_DIR, f);
|
|
520
|
+
const stat = Bun.file(path);
|
|
521
|
+
return {
|
|
522
|
+
name: f.replace(".json", ""),
|
|
523
|
+
path,
|
|
524
|
+
modified: new Date(stat.lastModified).toISOString()
|
|
525
|
+
};
|
|
526
|
+
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
527
|
+
}
|
|
528
|
+
function deleteState(name) {
|
|
529
|
+
const path = statePath(name);
|
|
530
|
+
if (existsSync(path)) {
|
|
531
|
+
unlinkSync(path);
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
var STATES_DIR;
|
|
537
|
+
var init_storage_state = __esm(() => {
|
|
538
|
+
STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
|
|
539
|
+
});
|
|
540
|
+
|
|
310
541
|
// node_modules/sharp/lib/is.js
|
|
311
542
|
var require_is = __commonJS((exports, module) => {
|
|
312
543
|
/*!
|
|
@@ -6901,8 +7132,8 @@ var init_snapshots = __esm(() => {
|
|
|
6901
7132
|
});
|
|
6902
7133
|
|
|
6903
7134
|
// src/server/index.ts
|
|
6904
|
-
import { join as
|
|
6905
|
-
import { existsSync as
|
|
7135
|
+
import { join as join7 } from "path";
|
|
7136
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6906
7137
|
|
|
6907
7138
|
// src/lib/session.ts
|
|
6908
7139
|
init_types();
|
|
@@ -6954,6 +7185,8 @@ function updateSessionStatus(id, status) {
|
|
|
6954
7185
|
return getSession(id);
|
|
6955
7186
|
}
|
|
6956
7187
|
function closeSession(id) {
|
|
7188
|
+
const db = getDatabase();
|
|
7189
|
+
db.prepare("UPDATE sessions SET locked_by = NULL, locked_at = NULL WHERE id = ?").run(id);
|
|
6957
7190
|
return updateSessionStatus(id, "closed");
|
|
6958
7191
|
}
|
|
6959
7192
|
|
|
@@ -6979,10 +7212,51 @@ async function getPage(browser, options) {
|
|
|
6979
7212
|
});
|
|
6980
7213
|
return context.newPage();
|
|
6981
7214
|
}
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
7215
|
+
class BrowserPool {
|
|
7216
|
+
pool = [];
|
|
7217
|
+
maxSize;
|
|
7218
|
+
options;
|
|
7219
|
+
constructor(maxSize = 3, options) {
|
|
7220
|
+
this.maxSize = maxSize;
|
|
7221
|
+
this.options = options;
|
|
7222
|
+
}
|
|
7223
|
+
async acquire(headless = true) {
|
|
7224
|
+
const available = this.pool.find((e) => !e.inUse);
|
|
7225
|
+
if (available) {
|
|
7226
|
+
available.inUse = true;
|
|
7227
|
+
return available.browser;
|
|
7228
|
+
}
|
|
7229
|
+
if (this.pool.length < this.maxSize) {
|
|
7230
|
+
const browser = await launchPlaywright({ ...this.options, headless });
|
|
7231
|
+
this.pool.push({ browser, inUse: true, createdAt: Date.now() });
|
|
7232
|
+
return browser;
|
|
7233
|
+
}
|
|
7234
|
+
return new Promise((resolve) => {
|
|
7235
|
+
const interval = setInterval(() => {
|
|
7236
|
+
const free = this.pool.find((e) => !e.inUse);
|
|
7237
|
+
if (free) {
|
|
7238
|
+
clearInterval(interval);
|
|
7239
|
+
free.inUse = true;
|
|
7240
|
+
resolve(free.browser);
|
|
7241
|
+
}
|
|
7242
|
+
}, 100);
|
|
7243
|
+
});
|
|
7244
|
+
}
|
|
7245
|
+
release(browser) {
|
|
7246
|
+
const entry = this.pool.find((e) => e.browser === browser);
|
|
7247
|
+
if (entry)
|
|
7248
|
+
entry.inUse = false;
|
|
7249
|
+
}
|
|
7250
|
+
async destroyAll() {
|
|
7251
|
+
await Promise.all(this.pool.map((e) => e.browser.close().catch(() => {})));
|
|
7252
|
+
this.pool = [];
|
|
7253
|
+
}
|
|
7254
|
+
get size() {
|
|
7255
|
+
return this.pool.length;
|
|
7256
|
+
}
|
|
7257
|
+
get available() {
|
|
7258
|
+
return this.pool.filter((e) => !e.inUse).length;
|
|
7259
|
+
}
|
|
6986
7260
|
}
|
|
6987
7261
|
|
|
6988
7262
|
// src/engines/lightpanda.ts
|
|
@@ -7769,10 +8043,55 @@ function setupDialogHandler(page, sessionId) {
|
|
|
7769
8043
|
|
|
7770
8044
|
// src/lib/session.ts
|
|
7771
8045
|
var handles = new Map;
|
|
8046
|
+
var pool = new BrowserPool(5);
|
|
8047
|
+
var SESSION_TTL_MS = parseInt(process.env["SESSION_TTL_MINUTES"] ?? "10", 10) * 60000;
|
|
8048
|
+
var ttlInterval = setInterval(async () => {
|
|
8049
|
+
const now = Date.now();
|
|
8050
|
+
for (const [id, handle] of handles) {
|
|
8051
|
+
if (now - handle.lastActivity > SESSION_TTL_MS) {
|
|
8052
|
+
try {
|
|
8053
|
+
await closeSession2(id);
|
|
8054
|
+
} catch {}
|
|
8055
|
+
}
|
|
8056
|
+
}
|
|
8057
|
+
}, 60000);
|
|
8058
|
+
if (ttlInterval.unref)
|
|
8059
|
+
ttlInterval.unref();
|
|
7772
8060
|
function createBunProxy(view) {
|
|
7773
8061
|
return view;
|
|
7774
8062
|
}
|
|
7775
8063
|
async function createSession2(opts = {}) {
|
|
8064
|
+
if (opts.cdpUrl) {
|
|
8065
|
+
const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
|
|
8066
|
+
const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
|
|
8067
|
+
const contexts = cdpBrowser.contexts();
|
|
8068
|
+
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
8069
|
+
const pages = context.pages();
|
|
8070
|
+
const page2 = pages.length > 0 ? pages[0] : await context.newPage();
|
|
8071
|
+
const session2 = createSession({
|
|
8072
|
+
engine: "cdp",
|
|
8073
|
+
projectId: opts.projectId,
|
|
8074
|
+
agentId: opts.agentId,
|
|
8075
|
+
startUrl: page2.url(),
|
|
8076
|
+
name: opts.name ?? "attached"
|
|
8077
|
+
});
|
|
8078
|
+
const cleanups2 = [];
|
|
8079
|
+
if (opts.captureNetwork !== false) {
|
|
8080
|
+
try {
|
|
8081
|
+
cleanups2.push(enableNetworkLogging(page2, session2.id));
|
|
8082
|
+
} catch {}
|
|
8083
|
+
}
|
|
8084
|
+
if (opts.captureConsole !== false) {
|
|
8085
|
+
try {
|
|
8086
|
+
cleanups2.push(enableConsoleCapture(page2, session2.id));
|
|
8087
|
+
} catch {}
|
|
8088
|
+
}
|
|
8089
|
+
try {
|
|
8090
|
+
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
8091
|
+
} catch {}
|
|
8092
|
+
handles.set(session2.id, { browser: cdpBrowser, bunView: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
|
|
8093
|
+
return { session: session2, page: page2 };
|
|
8094
|
+
}
|
|
7776
8095
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
7777
8096
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
7778
8097
|
let browser = null;
|
|
@@ -7797,12 +8116,23 @@ async function createSession2(opts = {}) {
|
|
|
7797
8116
|
const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
|
|
7798
8117
|
page = await context.newPage();
|
|
7799
8118
|
} else {
|
|
7800
|
-
browser = await
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
8119
|
+
browser = await pool.acquire(opts.headless ?? true);
|
|
8120
|
+
if (opts.storageState) {
|
|
8121
|
+
const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
8122
|
+
const statePath2 = loadStatePath2(opts.storageState);
|
|
8123
|
+
if (statePath2) {
|
|
8124
|
+
const context = await browser.newContext({
|
|
8125
|
+
viewport: opts.viewport ?? { width: 1280, height: 720 },
|
|
8126
|
+
userAgent: opts.userAgent,
|
|
8127
|
+
storageState: statePath2
|
|
8128
|
+
});
|
|
8129
|
+
page = await context.newPage();
|
|
8130
|
+
} else {
|
|
8131
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8132
|
+
}
|
|
8133
|
+
} else {
|
|
8134
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8135
|
+
}
|
|
7806
8136
|
}
|
|
7807
8137
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
7808
8138
|
try {
|
|
@@ -7855,7 +8185,7 @@ async function createSession2(opts = {}) {
|
|
|
7855
8185
|
} catch {}
|
|
7856
8186
|
}
|
|
7857
8187
|
}
|
|
7858
|
-
handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
8188
|
+
handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
|
|
7859
8189
|
if (opts.startUrl) {
|
|
7860
8190
|
try {
|
|
7861
8191
|
if (bunView) {
|
|
@@ -7881,6 +8211,7 @@ function getSessionPage(sessionId) {
|
|
|
7881
8211
|
handles.delete(sessionId);
|
|
7882
8212
|
throw new SessionNotFoundError(sessionId);
|
|
7883
8213
|
}
|
|
8214
|
+
handle.lastActivity = Date.now();
|
|
7884
8215
|
return handle.page;
|
|
7885
8216
|
}
|
|
7886
8217
|
async function closeSession2(sessionId) {
|
|
@@ -7899,10 +8230,8 @@ async function closeSession2(sessionId) {
|
|
|
7899
8230
|
try {
|
|
7900
8231
|
await handle.page.context().close();
|
|
7901
8232
|
} catch {}
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
await closeBrowser(handle.browser);
|
|
7905
|
-
} catch {}
|
|
8233
|
+
if (handle.browser)
|
|
8234
|
+
pool.release(handle.browser);
|
|
7906
8235
|
}
|
|
7907
8236
|
handles.delete(sessionId);
|
|
7908
8237
|
}
|
|
@@ -7919,6 +8248,66 @@ init_types();
|
|
|
7919
8248
|
var lastSnapshots = new Map;
|
|
7920
8249
|
var sessionRefMaps = new Map;
|
|
7921
8250
|
|
|
8251
|
+
// src/lib/self-heal.ts
|
|
8252
|
+
async function healSelector(page, selector, sessionId) {
|
|
8253
|
+
const attempts = [];
|
|
8254
|
+
attempts.push(`selector: ${selector}`);
|
|
8255
|
+
try {
|
|
8256
|
+
const loc = page.locator(selector).first();
|
|
8257
|
+
if (await loc.count() > 0) {
|
|
8258
|
+
return { found: true, locator: loc, method: "original", healed: false, attempts };
|
|
8259
|
+
}
|
|
8260
|
+
} catch {}
|
|
8261
|
+
if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
|
|
8262
|
+
attempts.push(`text: "${selector}"`);
|
|
8263
|
+
try {
|
|
8264
|
+
const loc = page.getByText(selector, { exact: false }).first();
|
|
8265
|
+
if (await loc.count() > 0) {
|
|
8266
|
+
return { found: true, locator: loc, method: "text", healed: true, attempts };
|
|
8267
|
+
}
|
|
8268
|
+
} catch {}
|
|
8269
|
+
}
|
|
8270
|
+
const roleMap = {
|
|
8271
|
+
button: ["button", "submit", "reset"],
|
|
8272
|
+
link: ["a"],
|
|
8273
|
+
input: ["input", "textarea"],
|
|
8274
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
8275
|
+
};
|
|
8276
|
+
const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
|
|
8277
|
+
for (const [role, tags] of Object.entries(roleMap)) {
|
|
8278
|
+
attempts.push(`role: ${role} name~="${nameHint}"`);
|
|
8279
|
+
try {
|
|
8280
|
+
const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
|
|
8281
|
+
if (await loc.count() > 0) {
|
|
8282
|
+
return { found: true, locator: loc, method: "role", healed: true, attempts };
|
|
8283
|
+
}
|
|
8284
|
+
} catch {}
|
|
8285
|
+
}
|
|
8286
|
+
if (selector.startsWith("#")) {
|
|
8287
|
+
const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8288
|
+
const partialSel = `[id*="${idPart}"]`;
|
|
8289
|
+
attempts.push(`partial_id: ${partialSel}`);
|
|
8290
|
+
try {
|
|
8291
|
+
const loc = page.locator(partialSel).first();
|
|
8292
|
+
if (await loc.count() > 0) {
|
|
8293
|
+
return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
|
|
8294
|
+
}
|
|
8295
|
+
} catch {}
|
|
8296
|
+
}
|
|
8297
|
+
if (selector.startsWith(".")) {
|
|
8298
|
+
const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8299
|
+
const partialSel = `[class*="${classPart}"]`;
|
|
8300
|
+
attempts.push(`partial_class: ${partialSel}`);
|
|
8301
|
+
try {
|
|
8302
|
+
const loc = page.locator(partialSel).first();
|
|
8303
|
+
if (await loc.count() > 0) {
|
|
8304
|
+
return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
|
|
8305
|
+
}
|
|
8306
|
+
} catch {}
|
|
8307
|
+
}
|
|
8308
|
+
return { found: false, locator: null, method: "none", healed: false, attempts };
|
|
8309
|
+
}
|
|
8310
|
+
|
|
7922
8311
|
// src/lib/actions.ts
|
|
7923
8312
|
async function click(page, selector, opts) {
|
|
7924
8313
|
try {
|
|
@@ -7928,11 +8317,22 @@ async function click(page, selector, opts) {
|
|
|
7928
8317
|
delay: opts?.delay,
|
|
7929
8318
|
timeout: opts?.timeout ?? 1e4
|
|
7930
8319
|
});
|
|
7931
|
-
|
|
7932
|
-
|
|
8320
|
+
return {};
|
|
8321
|
+
} catch (originalError) {
|
|
8322
|
+
if (opts?.selfHeal !== false) {
|
|
8323
|
+
const result = await healSelector(page, selector);
|
|
8324
|
+
if (result.found && result.locator) {
|
|
8325
|
+
await result.locator.click({
|
|
8326
|
+
button: opts?.button ?? "left",
|
|
8327
|
+
timeout: opts?.timeout ?? 1e4
|
|
8328
|
+
});
|
|
8329
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8332
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
7933
8333
|
throw new ElementNotFoundError(selector);
|
|
7934
8334
|
}
|
|
7935
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
8335
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
7936
8336
|
}
|
|
7937
8337
|
}
|
|
7938
8338
|
async function type(page, selector, text, opts) {
|
|
@@ -7941,11 +8341,21 @@ async function type(page, selector, text, opts) {
|
|
|
7941
8341
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
7942
8342
|
}
|
|
7943
8343
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
7944
|
-
|
|
7945
|
-
|
|
8344
|
+
return {};
|
|
8345
|
+
} catch (originalError) {
|
|
8346
|
+
if (opts?.selfHeal !== false) {
|
|
8347
|
+
const result = await healSelector(page, selector);
|
|
8348
|
+
if (result.found && result.locator) {
|
|
8349
|
+
if (opts?.clear)
|
|
8350
|
+
await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
|
|
8351
|
+
await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8352
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
7946
8356
|
throw new ElementNotFoundError(selector);
|
|
7947
8357
|
}
|
|
7948
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
8358
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
7949
8359
|
}
|
|
7950
8360
|
}
|
|
7951
8361
|
async function scroll(page, direction = "down", amount = 300) {
|
|
@@ -8045,9 +8455,9 @@ async function extract(page, opts = {}) {
|
|
|
8045
8455
|
// src/lib/screenshot.ts
|
|
8046
8456
|
init_types();
|
|
8047
8457
|
var import_sharp = __toESM(require_lib(), 1);
|
|
8048
|
-
import { join as
|
|
8049
|
-
import { mkdirSync as
|
|
8050
|
-
import { homedir as
|
|
8458
|
+
import { join as join4 } from "path";
|
|
8459
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
8460
|
+
import { homedir as homedir4 } from "os";
|
|
8051
8461
|
|
|
8052
8462
|
// src/db/gallery.ts
|
|
8053
8463
|
init_schema();
|
|
@@ -8177,13 +8587,13 @@ function getGalleryStats(projectId) {
|
|
|
8177
8587
|
|
|
8178
8588
|
// src/lib/screenshot.ts
|
|
8179
8589
|
function getDataDir2() {
|
|
8180
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
8590
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
8181
8591
|
}
|
|
8182
8592
|
function getScreenshotDir(projectId) {
|
|
8183
|
-
const base =
|
|
8593
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
8184
8594
|
const date = new Date().toISOString().split("T")[0];
|
|
8185
|
-
const dir = projectId ?
|
|
8186
|
-
|
|
8595
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
8596
|
+
mkdirSync4(dir, { recursive: true });
|
|
8187
8597
|
return dir;
|
|
8188
8598
|
}
|
|
8189
8599
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -8198,7 +8608,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
8198
8608
|
}
|
|
8199
8609
|
}
|
|
8200
8610
|
async function generateThumbnail(raw, dir, stem) {
|
|
8201
|
-
const thumbPath =
|
|
8611
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
8202
8612
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
8203
8613
|
await Bun.write(thumbPath, thumbBuffer);
|
|
8204
8614
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -8255,7 +8665,7 @@ async function takeScreenshot(page, opts) {
|
|
|
8255
8665
|
const compressedSizeBytes = finalBuffer.length;
|
|
8256
8666
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
8257
8667
|
const ext = format;
|
|
8258
|
-
const screenshotPath = opts?.path ??
|
|
8668
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
8259
8669
|
await Bun.write(screenshotPath, finalBuffer);
|
|
8260
8670
|
let thumbnailPath;
|
|
8261
8671
|
let thumbnailBase64;
|
|
@@ -8314,118 +8724,8 @@ async function takeScreenshot(page, opts) {
|
|
|
8314
8724
|
}
|
|
8315
8725
|
}
|
|
8316
8726
|
|
|
8317
|
-
// src/engines/cdp.ts
|
|
8318
|
-
init_types();
|
|
8319
|
-
|
|
8320
|
-
class CDPClient {
|
|
8321
|
-
session;
|
|
8322
|
-
networkEnabled = false;
|
|
8323
|
-
performanceEnabled = false;
|
|
8324
|
-
constructor(session) {
|
|
8325
|
-
this.session = session;
|
|
8326
|
-
}
|
|
8327
|
-
static async fromPage(page) {
|
|
8328
|
-
try {
|
|
8329
|
-
const session = await page.context().newCDPSession(page);
|
|
8330
|
-
return new CDPClient(session);
|
|
8331
|
-
} catch (err) {
|
|
8332
|
-
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
8333
|
-
}
|
|
8334
|
-
}
|
|
8335
|
-
async send(method, params) {
|
|
8336
|
-
try {
|
|
8337
|
-
return await this.session.send(method, params);
|
|
8338
|
-
} catch (err) {
|
|
8339
|
-
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
8340
|
-
}
|
|
8341
|
-
}
|
|
8342
|
-
on(event, handler) {
|
|
8343
|
-
this.session.on(event, handler);
|
|
8344
|
-
}
|
|
8345
|
-
off(event, handler) {
|
|
8346
|
-
this.session.off(event, handler);
|
|
8347
|
-
}
|
|
8348
|
-
async enableNetwork() {
|
|
8349
|
-
if (!this.networkEnabled) {
|
|
8350
|
-
await this.send("Network.enable");
|
|
8351
|
-
this.networkEnabled = true;
|
|
8352
|
-
}
|
|
8353
|
-
}
|
|
8354
|
-
async enablePerformance() {
|
|
8355
|
-
if (!this.performanceEnabled) {
|
|
8356
|
-
await this.send("Performance.enable");
|
|
8357
|
-
this.performanceEnabled = true;
|
|
8358
|
-
}
|
|
8359
|
-
}
|
|
8360
|
-
async getPerformanceMetrics() {
|
|
8361
|
-
await this.enablePerformance();
|
|
8362
|
-
const result = await this.send("Performance.getMetrics");
|
|
8363
|
-
const m = {};
|
|
8364
|
-
for (const metric of result.metrics) {
|
|
8365
|
-
m[metric.name] = metric.value;
|
|
8366
|
-
}
|
|
8367
|
-
return {
|
|
8368
|
-
js_heap_size_used: m["JSHeapUsedSize"],
|
|
8369
|
-
js_heap_size_total: m["JSHeapTotalSize"],
|
|
8370
|
-
dom_interactive: m["DOMInteractive"],
|
|
8371
|
-
dom_complete: m["DOMComplete"],
|
|
8372
|
-
load_event: m["LoadEventEnd"]
|
|
8373
|
-
};
|
|
8374
|
-
}
|
|
8375
|
-
async startJSCoverage() {
|
|
8376
|
-
await this.send("Profiler.enable");
|
|
8377
|
-
await this.send("Debugger.enable");
|
|
8378
|
-
await this.send("Profiler.startPreciseCoverage", {
|
|
8379
|
-
callCount: false,
|
|
8380
|
-
detailed: true
|
|
8381
|
-
});
|
|
8382
|
-
}
|
|
8383
|
-
async stopJSCoverage() {
|
|
8384
|
-
const result = await this.send("Profiler.takePreciseCoverage");
|
|
8385
|
-
await this.send("Profiler.stopPreciseCoverage");
|
|
8386
|
-
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
8387
|
-
url: r.url,
|
|
8388
|
-
text: "",
|
|
8389
|
-
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
8390
|
-
}));
|
|
8391
|
-
}
|
|
8392
|
-
async getCoverage() {
|
|
8393
|
-
await this.startJSCoverage();
|
|
8394
|
-
const js = await this.stopJSCoverage();
|
|
8395
|
-
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
8396
|
-
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
8397
|
-
}
|
|
8398
|
-
async captureHAREntries(page, handler) {
|
|
8399
|
-
await this.enableNetwork();
|
|
8400
|
-
const requestTimings = new Map;
|
|
8401
|
-
const onRequest = (params) => {
|
|
8402
|
-
requestTimings.set(params.requestId, params.timestamp);
|
|
8403
|
-
};
|
|
8404
|
-
const onResponse = (params) => {
|
|
8405
|
-
const start = requestTimings.get(params.requestId);
|
|
8406
|
-
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
8407
|
-
handler({
|
|
8408
|
-
method: "GET",
|
|
8409
|
-
url: params.response.url,
|
|
8410
|
-
status: params.response.status,
|
|
8411
|
-
duration
|
|
8412
|
-
});
|
|
8413
|
-
};
|
|
8414
|
-
this.on("Network.requestWillBeSent", onRequest);
|
|
8415
|
-
this.on("Network.responseReceived", onResponse);
|
|
8416
|
-
return () => {
|
|
8417
|
-
this.off("Network.requestWillBeSent", onRequest);
|
|
8418
|
-
this.off("Network.responseReceived", onResponse);
|
|
8419
|
-
};
|
|
8420
|
-
}
|
|
8421
|
-
async detach() {
|
|
8422
|
-
try {
|
|
8423
|
-
await this.session.detach();
|
|
8424
|
-
} catch {}
|
|
8425
|
-
}
|
|
8426
|
-
}
|
|
8427
|
-
|
|
8428
8727
|
// src/lib/performance.ts
|
|
8728
|
+
init_cdp();
|
|
8429
8729
|
async function getPerformanceMetrics(page) {
|
|
8430
8730
|
const navTiming = await page.evaluate(() => {
|
|
8431
8731
|
const t = performance.timing;
|
|
@@ -8658,16 +8958,16 @@ init_console_log();
|
|
|
8658
8958
|
init_recordings();
|
|
8659
8959
|
|
|
8660
8960
|
// src/lib/downloads.ts
|
|
8661
|
-
import { join as
|
|
8662
|
-
import { mkdirSync as
|
|
8663
|
-
import { homedir as
|
|
8961
|
+
import { join as join5, basename, extname } from "path";
|
|
8962
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
8963
|
+
import { homedir as homedir5 } from "os";
|
|
8664
8964
|
function getDataDir3() {
|
|
8665
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
8965
|
+
return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
8666
8966
|
}
|
|
8667
8967
|
function getDownloadsDir(sessionId) {
|
|
8668
|
-
const base =
|
|
8669
|
-
const dir = sessionId ?
|
|
8670
|
-
|
|
8968
|
+
const base = join5(getDataDir3(), "downloads");
|
|
8969
|
+
const dir = sessionId ? join5(base, sessionId) : base;
|
|
8970
|
+
mkdirSync5(dir, { recursive: true });
|
|
8671
8971
|
return dir;
|
|
8672
8972
|
}
|
|
8673
8973
|
function metaPath(filePath) {
|
|
@@ -8677,20 +8977,20 @@ function listDownloads(sessionId) {
|
|
|
8677
8977
|
const dir = getDownloadsDir(sessionId);
|
|
8678
8978
|
const results = [];
|
|
8679
8979
|
function scanDir(d) {
|
|
8680
|
-
if (!
|
|
8980
|
+
if (!existsSync2(d))
|
|
8681
8981
|
return;
|
|
8682
|
-
const entries =
|
|
8982
|
+
const entries = readdirSync2(d);
|
|
8683
8983
|
for (const entry of entries) {
|
|
8684
8984
|
if (entry.endsWith(".meta.json"))
|
|
8685
8985
|
continue;
|
|
8686
|
-
const full =
|
|
8986
|
+
const full = join5(d, entry);
|
|
8687
8987
|
const stat = statSync(full);
|
|
8688
8988
|
if (stat.isDirectory()) {
|
|
8689
8989
|
scanDir(full);
|
|
8690
8990
|
continue;
|
|
8691
8991
|
}
|
|
8692
8992
|
const mpath = metaPath(full);
|
|
8693
|
-
if (!
|
|
8993
|
+
if (!existsSync2(mpath))
|
|
8694
8994
|
continue;
|
|
8695
8995
|
try {
|
|
8696
8996
|
const meta = JSON.parse(readFileSync(mpath, "utf8"));
|
|
@@ -8720,9 +9020,9 @@ function deleteDownload(id, sessionId) {
|
|
|
8720
9020
|
if (!file)
|
|
8721
9021
|
return false;
|
|
8722
9022
|
try {
|
|
8723
|
-
|
|
8724
|
-
if (
|
|
8725
|
-
|
|
9023
|
+
unlinkSync2(file.path);
|
|
9024
|
+
if (existsSync2(file.meta_path))
|
|
9025
|
+
unlinkSync2(file.meta_path);
|
|
8726
9026
|
return true;
|
|
8727
9027
|
} catch {
|
|
8728
9028
|
return false;
|
|
@@ -8744,9 +9044,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
|
|
|
8744
9044
|
|
|
8745
9045
|
// src/lib/gallery-diff.ts
|
|
8746
9046
|
var import_sharp2 = __toESM(require_lib(), 1);
|
|
8747
|
-
import { join as
|
|
8748
|
-
import { mkdirSync as
|
|
8749
|
-
import { homedir as
|
|
9047
|
+
import { join as join6 } from "path";
|
|
9048
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
9049
|
+
import { homedir as homedir6 } from "os";
|
|
8750
9050
|
async function diffImages(path1, path2) {
|
|
8751
9051
|
const img1 = import_sharp2.default(path1);
|
|
8752
9052
|
const img2 = import_sharp2.default(path2);
|
|
@@ -8777,10 +9077,10 @@ async function diffImages(path1, path2) {
|
|
|
8777
9077
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
8778
9078
|
}
|
|
8779
9079
|
}
|
|
8780
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
8781
|
-
const diffDir =
|
|
8782
|
-
|
|
8783
|
-
const diffPath =
|
|
9080
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
9081
|
+
const diffDir = join6(dataDir, "diffs");
|
|
9082
|
+
mkdirSync6(diffDir, { recursive: true });
|
|
9083
|
+
const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
|
|
8784
9084
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
8785
9085
|
await Bun.write(diffPath, diffImageBuffer);
|
|
8786
9086
|
return {
|
|
@@ -9024,14 +9324,14 @@ var server = Bun.serve({
|
|
|
9024
9324
|
if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
|
|
9025
9325
|
const id = path.split("/")[3];
|
|
9026
9326
|
const entry = getEntry(id);
|
|
9027
|
-
if (!entry?.thumbnail_path || !
|
|
9327
|
+
if (!entry?.thumbnail_path || !existsSync4(entry.thumbnail_path))
|
|
9028
9328
|
return notFound("Thumbnail not found");
|
|
9029
9329
|
return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
|
|
9030
9330
|
}
|
|
9031
9331
|
if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
|
|
9032
9332
|
const id = path.split("/")[3];
|
|
9033
9333
|
const entry = getEntry(id);
|
|
9034
|
-
if (!entry?.path || !
|
|
9334
|
+
if (!entry?.path || !existsSync4(entry.path))
|
|
9035
9335
|
return notFound("Image not found");
|
|
9036
9336
|
return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
|
|
9037
9337
|
}
|
|
@@ -9059,7 +9359,7 @@ var server = Bun.serve({
|
|
|
9059
9359
|
if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
|
|
9060
9360
|
const id = path.split("/")[3];
|
|
9061
9361
|
const file = getDownload(id);
|
|
9062
|
-
if (!file || !
|
|
9362
|
+
if (!file || !existsSync4(file.path))
|
|
9063
9363
|
return notFound("Download not found");
|
|
9064
9364
|
return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
|
|
9065
9365
|
}
|
|
@@ -9067,13 +9367,13 @@ var server = Bun.serve({
|
|
|
9067
9367
|
const id = path.split("/")[3];
|
|
9068
9368
|
return ok({ deleted: deleteDownload(id) });
|
|
9069
9369
|
}
|
|
9070
|
-
const dashboardDist =
|
|
9071
|
-
if (
|
|
9072
|
-
const filePath = path === "/" ?
|
|
9073
|
-
if (
|
|
9370
|
+
const dashboardDist = join7(import.meta.dir, "../../dashboard/dist");
|
|
9371
|
+
if (existsSync4(dashboardDist)) {
|
|
9372
|
+
const filePath = path === "/" ? join7(dashboardDist, "index.html") : join7(dashboardDist, path);
|
|
9373
|
+
if (existsSync4(filePath)) {
|
|
9074
9374
|
return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
|
|
9075
9375
|
}
|
|
9076
|
-
return new Response(Bun.file(
|
|
9376
|
+
return new Response(Bun.file(join7(dashboardDist, "index.html")), { headers: CORS_HEADERS });
|
|
9077
9377
|
}
|
|
9078
9378
|
if (path === "/" || path === "") {
|
|
9079
9379
|
return new Response("@hasna/browser REST API running. Dashboard not built.", {
|