mcp-scraper 0.1.9 → 0.2.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/bin/api-server.cjs +845 -40
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/browser-agent-stdio-server.cjs +314 -0
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -0
- package/dist/bin/browser-agent-stdio-server.d.cts +1 -0
- package/dist/bin/browser-agent-stdio-server.d.ts +1 -0
- package/dist/bin/browser-agent-stdio-server.js +313 -0
- package/dist/bin/browser-agent-stdio-server.js.map +1 -0
- package/dist/bin/mcp-stdio-server.cjs +1 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -1
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/chunk-2BS7BUEE.js +7 -0
- package/dist/chunk-2BS7BUEE.js.map +1 -0
- package/dist/{chunk-JNC32DMS.js → chunk-BMVQB3WN.js} +4 -4
- package/dist/chunk-BMVQB3WN.js.map +1 -0
- package/dist/{chunk-ZK456YXN.js → chunk-GXBT5CDU.js} +20 -3
- package/dist/chunk-GXBT5CDU.js.map +1 -0
- package/dist/{server-MTXAJG5J.js → server-ASCMKUQ5.js} +790 -28
- package/dist/server-ASCMKUQ5.js.map +1 -0
- package/dist/{worker-AUCXFHEL.js → worker-KJ4A7WIR.js} +2 -2
- package/docs/specs/api-forge-spec.md +234 -0
- package/docs/specs/deferred-work-spec.md +74 -0
- package/docs/specs/oauth-mcp-spec.md +213 -0
- package/package.json +3 -2
- package/dist/chunk-JNC32DMS.js.map +0 -1
- package/dist/chunk-ZK456YXN.js.map +0 -1
- package/dist/server-MTXAJG5J.js.map +0 -1
- /package/dist/{worker-AUCXFHEL.js.map → worker-KJ4A7WIR.js.map} +0 -0
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
configureReportSaving,
|
|
7
7
|
harvestTimeoutBudget,
|
|
8
8
|
liveWebToolAnnotations
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-BMVQB3WN.js";
|
|
10
|
+
import "./chunk-2BS7BUEE.js";
|
|
10
11
|
import {
|
|
11
12
|
BALANCE_PACK_LABELS,
|
|
12
13
|
BALANCE_PRICE_IDS,
|
|
14
|
+
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
13
15
|
CONCURRENCY_PRICE_ID,
|
|
14
16
|
CREDIT_COST_CATALOG,
|
|
15
17
|
FREE_MONTHLY_REFRESH_MC,
|
|
@@ -17,12 +19,13 @@ import {
|
|
|
17
19
|
LedgerOperation,
|
|
18
20
|
MC_COSTS,
|
|
19
21
|
MC_PER_CREDIT,
|
|
22
|
+
browserActiveCostMc,
|
|
20
23
|
classifyHarvestProblem,
|
|
21
24
|
createHarvestAttemptRecorder,
|
|
22
25
|
harvestProblemResponse,
|
|
23
26
|
insufficientBalanceResponse,
|
|
24
27
|
serializeHarvestProblem
|
|
25
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-GXBT5CDU.js";
|
|
26
29
|
import {
|
|
27
30
|
BrowserDriver,
|
|
28
31
|
MapsPlaceOptionsSchema,
|
|
@@ -3497,8 +3500,8 @@ import { chromium } from "playwright";
|
|
|
3497
3500
|
async function fetchWithKernel(url) {
|
|
3498
3501
|
const apiKey = browserServiceApiKey();
|
|
3499
3502
|
if (!apiKey) throw new Error("Browser backend API key not set");
|
|
3500
|
-
const
|
|
3501
|
-
const kb = await
|
|
3503
|
+
const client2 = new Kernel({ apiKey });
|
|
3504
|
+
const kb = await client2.browsers.create({ stealth: true, timeout_seconds: 60 });
|
|
3502
3505
|
const browser = await chromium.connectOverCDP(kb.cdp_ws_url);
|
|
3503
3506
|
try {
|
|
3504
3507
|
const context = browser.contexts()[0] ?? await browser.newContext({
|
|
@@ -3511,7 +3514,7 @@ async function fetchWithKernel(url) {
|
|
|
3511
3514
|
} finally {
|
|
3512
3515
|
await browser.close().catch(() => {
|
|
3513
3516
|
});
|
|
3514
|
-
await
|
|
3517
|
+
await client2.browsers.deleteByID(kb.session_id).catch(() => {
|
|
3515
3518
|
});
|
|
3516
3519
|
}
|
|
3517
3520
|
}
|
|
@@ -4843,7 +4846,7 @@ async function extractSite(opts) {
|
|
|
4843
4846
|
}
|
|
4844
4847
|
|
|
4845
4848
|
// src/api/server.ts
|
|
4846
|
-
import { Hono as
|
|
4849
|
+
import { Hono as Hono10 } from "hono";
|
|
4847
4850
|
import { serve as serveInngest } from "inngest/hono";
|
|
4848
4851
|
|
|
4849
4852
|
// src/inngest/client.ts
|
|
@@ -8532,13 +8535,13 @@ var FacebookAdExtractor = class {
|
|
|
8532
8535
|
}
|
|
8533
8536
|
await page.waitForTimeout(1500);
|
|
8534
8537
|
let prevCount = 0;
|
|
8535
|
-
for (let
|
|
8538
|
+
for (let scroll2 = 0; scroll2 < 20; scroll2++) {
|
|
8536
8539
|
const count = await page.evaluate(() => {
|
|
8537
8540
|
const bt = document.body ? document.body.innerText ?? "" : "";
|
|
8538
8541
|
return [...bt.matchAll(/Library ID/g)].length;
|
|
8539
8542
|
});
|
|
8540
8543
|
if (count >= maxAds) break;
|
|
8541
|
-
if (count === prevCount &&
|
|
8544
|
+
if (count === prevCount && scroll2 > 0) break;
|
|
8542
8545
|
prevCount = count;
|
|
8543
8546
|
await page.evaluate(() => {
|
|
8544
8547
|
if (document.body) window.scrollTo(0, document.body.scrollHeight);
|
|
@@ -9184,7 +9187,7 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
|
|
|
9184
9187
|
return c.json(searchResult2);
|
|
9185
9188
|
}
|
|
9186
9189
|
await page.waitForTimeout(1500);
|
|
9187
|
-
for (let
|
|
9190
|
+
for (let scroll2 = 0; scroll2 < 3; scroll2++) {
|
|
9188
9191
|
await page.evaluate(() => {
|
|
9189
9192
|
if (document.body) window.scrollTo(0, document.body.scrollHeight);
|
|
9190
9193
|
});
|
|
@@ -10952,11 +10955,767 @@ mcpApp.all("/", async (c) => {
|
|
|
10952
10955
|
}
|
|
10953
10956
|
});
|
|
10954
10957
|
|
|
10958
|
+
// src/api/browser-agent-routes.ts
|
|
10959
|
+
import { Hono as Hono8 } from "hono";
|
|
10960
|
+
|
|
10961
|
+
// src/api/browser-agent-db.ts
|
|
10962
|
+
import { randomUUID } from "crypto";
|
|
10963
|
+
var _ready = false;
|
|
10964
|
+
async function migrateBrowserAgent() {
|
|
10965
|
+
if (_ready) return;
|
|
10966
|
+
const db = getDb();
|
|
10967
|
+
await db.execute(`
|
|
10968
|
+
CREATE TABLE IF NOT EXISTS browser_agent_sessions (
|
|
10969
|
+
id TEXT PRIMARY KEY,
|
|
10970
|
+
runtime_session_id TEXT NOT NULL,
|
|
10971
|
+
live_view_url TEXT,
|
|
10972
|
+
cdp_ws_url TEXT NOT NULL,
|
|
10973
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
10974
|
+
label TEXT,
|
|
10975
|
+
user_id INTEGER,
|
|
10976
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10977
|
+
closed_at TEXT,
|
|
10978
|
+
last_action_at TEXT,
|
|
10979
|
+
active_ms INTEGER NOT NULL DEFAULT 0,
|
|
10980
|
+
billed_mc INTEGER NOT NULL DEFAULT 0
|
|
10981
|
+
)
|
|
10982
|
+
`);
|
|
10983
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_sessions_status ON browser_agent_sessions(status)`);
|
|
10984
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_sessions_user ON browser_agent_sessions(user_id)`);
|
|
10985
|
+
await db.execute(`
|
|
10986
|
+
CREATE TABLE IF NOT EXISTS browser_agent_actions (
|
|
10987
|
+
id TEXT PRIMARY KEY,
|
|
10988
|
+
session_id TEXT NOT NULL,
|
|
10989
|
+
type TEXT NOT NULL,
|
|
10990
|
+
params_json TEXT,
|
|
10991
|
+
ok INTEGER NOT NULL DEFAULT 1,
|
|
10992
|
+
error TEXT,
|
|
10993
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
10994
|
+
)
|
|
10995
|
+
`);
|
|
10996
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_actions_session ON browser_agent_actions(session_id)`);
|
|
10997
|
+
await db.execute(`
|
|
10998
|
+
CREATE TABLE IF NOT EXISTS browser_agent_replays (
|
|
10999
|
+
replay_id TEXT PRIMARY KEY,
|
|
11000
|
+
session_id TEXT NOT NULL,
|
|
11001
|
+
view_url TEXT,
|
|
11002
|
+
label TEXT,
|
|
11003
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
11004
|
+
stopped_at TEXT
|
|
11005
|
+
)
|
|
11006
|
+
`);
|
|
11007
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_replays_session ON browser_agent_replays(session_id)`);
|
|
11008
|
+
_ready = true;
|
|
11009
|
+
}
|
|
11010
|
+
async function createSessionRow(input) {
|
|
11011
|
+
const db = getDb();
|
|
11012
|
+
const id = `bas_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
|
|
11013
|
+
await db.execute({
|
|
11014
|
+
sql: `INSERT INTO browser_agent_sessions (id, runtime_session_id, live_view_url, cdp_ws_url, status, label, user_id, last_action_at)
|
|
11015
|
+
VALUES (?, ?, ?, ?, 'open', ?, ?, datetime('now'))`,
|
|
11016
|
+
args: [id, input.runtimeSessionId, input.liveViewUrl, input.cdpWsUrl, input.label, input.userId]
|
|
11017
|
+
});
|
|
11018
|
+
const row = await getSessionRow(id);
|
|
11019
|
+
if (!row) throw new Error("session insert failed");
|
|
11020
|
+
return row;
|
|
11021
|
+
}
|
|
11022
|
+
async function getSessionRow(id) {
|
|
11023
|
+
const db = getDb();
|
|
11024
|
+
const res = await db.execute({ sql: `SELECT * FROM browser_agent_sessions WHERE id = ?`, args: [id] });
|
|
11025
|
+
return res.rows[0] ?? null;
|
|
11026
|
+
}
|
|
11027
|
+
async function listSessionRows(userId, includeClosed = false) {
|
|
11028
|
+
const db = getDb();
|
|
11029
|
+
const clauses = [];
|
|
11030
|
+
const args = [];
|
|
11031
|
+
if (userId != null) {
|
|
11032
|
+
clauses.push("(user_id = ? OR user_id IS NULL)");
|
|
11033
|
+
args.push(userId);
|
|
11034
|
+
}
|
|
11035
|
+
if (!includeClosed) clauses.push(`status = 'open'`);
|
|
11036
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
11037
|
+
const res = await db.execute({
|
|
11038
|
+
sql: `SELECT * FROM browser_agent_sessions ${where} ORDER BY created_at DESC LIMIT 100`,
|
|
11039
|
+
args
|
|
11040
|
+
});
|
|
11041
|
+
return res.rows;
|
|
11042
|
+
}
|
|
11043
|
+
async function addActiveMs(id, deltaMs) {
|
|
11044
|
+
const db = getDb();
|
|
11045
|
+
await db.execute({
|
|
11046
|
+
sql: `UPDATE browser_agent_sessions SET active_ms = active_ms + ?, last_action_at = datetime('now') WHERE id = ?`,
|
|
11047
|
+
args: [Math.max(0, Math.round(deltaMs)), id]
|
|
11048
|
+
});
|
|
11049
|
+
const res = await db.execute({ sql: `SELECT active_ms, billed_mc FROM browser_agent_sessions WHERE id = ?`, args: [id] });
|
|
11050
|
+
const row = res.rows[0];
|
|
11051
|
+
return { active_ms: Number(row?.active_ms ?? 0), billed_mc: Number(row?.billed_mc ?? 0) };
|
|
11052
|
+
}
|
|
11053
|
+
async function setBilledMc(id, billedMc) {
|
|
11054
|
+
const db = getDb();
|
|
11055
|
+
await db.execute({
|
|
11056
|
+
sql: `UPDATE browser_agent_sessions SET billed_mc = ? WHERE id = ?`,
|
|
11057
|
+
args: [Math.round(billedMc), id]
|
|
11058
|
+
});
|
|
11059
|
+
}
|
|
11060
|
+
async function markSessionClosed(id) {
|
|
11061
|
+
const db = getDb();
|
|
11062
|
+
await db.execute({
|
|
11063
|
+
sql: `UPDATE browser_agent_sessions SET status = 'closed', closed_at = datetime('now') WHERE id = ?`,
|
|
11064
|
+
args: [id]
|
|
11065
|
+
});
|
|
11066
|
+
}
|
|
11067
|
+
async function recordAction(input) {
|
|
11068
|
+
const db = getDb();
|
|
11069
|
+
await db.execute({
|
|
11070
|
+
sql: `INSERT INTO browser_agent_actions (id, session_id, type, params_json, ok, error)
|
|
11071
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
11072
|
+
args: [
|
|
11073
|
+
`baa_${randomUUID().replace(/-/g, "").slice(0, 20)}`,
|
|
11074
|
+
input.sessionId,
|
|
11075
|
+
input.type,
|
|
11076
|
+
input.params == null ? null : JSON.stringify(input.params),
|
|
11077
|
+
input.ok ? 1 : 0,
|
|
11078
|
+
input.error ?? null
|
|
11079
|
+
]
|
|
11080
|
+
});
|
|
11081
|
+
}
|
|
11082
|
+
async function recordReplayStart(input) {
|
|
11083
|
+
const db = getDb();
|
|
11084
|
+
await db.execute({
|
|
11085
|
+
sql: `INSERT INTO browser_agent_replays (replay_id, session_id, view_url, label)
|
|
11086
|
+
VALUES (?, ?, ?, ?)`,
|
|
11087
|
+
args: [input.replayId, input.sessionId, input.viewUrl, input.label]
|
|
11088
|
+
});
|
|
11089
|
+
}
|
|
11090
|
+
async function recordReplayStop(replayId, viewUrl) {
|
|
11091
|
+
const db = getDb();
|
|
11092
|
+
await db.execute({
|
|
11093
|
+
sql: `UPDATE browser_agent_replays SET stopped_at = datetime('now'), view_url = COALESCE(?, view_url) WHERE replay_id = ?`,
|
|
11094
|
+
args: [viewUrl, replayId]
|
|
11095
|
+
});
|
|
11096
|
+
}
|
|
11097
|
+
async function listReplayRows(sessionId) {
|
|
11098
|
+
const db = getDb();
|
|
11099
|
+
const res = await db.execute({
|
|
11100
|
+
sql: `SELECT * FROM browser_agent_replays WHERE session_id = ? ORDER BY started_at DESC`,
|
|
11101
|
+
args: [sessionId]
|
|
11102
|
+
});
|
|
11103
|
+
return res.rows;
|
|
11104
|
+
}
|
|
11105
|
+
|
|
11106
|
+
// src/services/browser-agent/browser-agent-service.ts
|
|
11107
|
+
import Kernel3 from "@onkernel/sdk";
|
|
11108
|
+
import { chromium as playwrightChromium } from "playwright";
|
|
11109
|
+
var DEFAULT_TIMEOUT_SECONDS = 600;
|
|
11110
|
+
function client() {
|
|
11111
|
+
const apiKey = browserServiceApiKey();
|
|
11112
|
+
if (!apiKey) throw new Error("Browser backend API key is required");
|
|
11113
|
+
return new Kernel3({ apiKey });
|
|
11114
|
+
}
|
|
11115
|
+
async function createSession(opts = {}) {
|
|
11116
|
+
const k = client();
|
|
11117
|
+
const resolvedProxyId = opts.proxyId ?? browserServiceProxyId();
|
|
11118
|
+
const browser = await k.browsers.create({
|
|
11119
|
+
stealth: opts.stealth ?? true,
|
|
11120
|
+
timeout_seconds: opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS,
|
|
11121
|
+
...resolvedProxyId ? { proxy_id: resolvedProxyId } : {},
|
|
11122
|
+
...opts.profileName ? { profile: { name: opts.profileName } } : {}
|
|
11123
|
+
});
|
|
11124
|
+
const runtimeSessionId = browser.session_id;
|
|
11125
|
+
if (opts.disableDefaultProxy) {
|
|
11126
|
+
try {
|
|
11127
|
+
await k.browsers.update(runtimeSessionId, { disable_default_proxy: true });
|
|
11128
|
+
} catch {
|
|
11129
|
+
}
|
|
11130
|
+
}
|
|
11131
|
+
if (opts.viewport) {
|
|
11132
|
+
try {
|
|
11133
|
+
await k.browsers.update(runtimeSessionId, { viewport: opts.viewport });
|
|
11134
|
+
} catch {
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
return {
|
|
11138
|
+
runtimeSessionId,
|
|
11139
|
+
liveViewUrl: browser.browser_live_view_url ?? null,
|
|
11140
|
+
cdpWsUrl: browser.cdp_ws_url
|
|
11141
|
+
};
|
|
11142
|
+
}
|
|
11143
|
+
async function closeSession(runtimeSessionId) {
|
|
11144
|
+
const k = client();
|
|
11145
|
+
await k.browsers.deleteByID(runtimeSessionId);
|
|
11146
|
+
}
|
|
11147
|
+
async function screenshot(runtimeSessionId) {
|
|
11148
|
+
const k = client();
|
|
11149
|
+
const res = await k.browsers.computer.captureScreenshot(runtimeSessionId);
|
|
11150
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
11151
|
+
return { base64: buf.toString("base64"), mimeType: "image/png" };
|
|
11152
|
+
}
|
|
11153
|
+
async function click(runtimeSessionId, x, y, opts = {}) {
|
|
11154
|
+
const k = client();
|
|
11155
|
+
await k.browsers.computer.clickMouse(runtimeSessionId, {
|
|
11156
|
+
x,
|
|
11157
|
+
y,
|
|
11158
|
+
button: opts.button ?? "left",
|
|
11159
|
+
...opts.numClicks ? { num_clicks: opts.numClicks } : {}
|
|
11160
|
+
});
|
|
11161
|
+
}
|
|
11162
|
+
async function typeText(runtimeSessionId, text, delayMs) {
|
|
11163
|
+
const k = client();
|
|
11164
|
+
await k.browsers.computer.typeText(runtimeSessionId, {
|
|
11165
|
+
text,
|
|
11166
|
+
...typeof delayMs === "number" ? { delay: delayMs } : {}
|
|
11167
|
+
});
|
|
11168
|
+
}
|
|
11169
|
+
async function scroll(runtimeSessionId, x, y, deltaX, deltaY) {
|
|
11170
|
+
const k = client();
|
|
11171
|
+
await k.browsers.computer.scroll(runtimeSessionId, { x, y, delta_x: deltaX, delta_y: deltaY });
|
|
11172
|
+
}
|
|
11173
|
+
async function pressKeys(runtimeSessionId, keys) {
|
|
11174
|
+
const k = client();
|
|
11175
|
+
await k.browsers.computer.pressKey(runtimeSessionId, { keys });
|
|
11176
|
+
}
|
|
11177
|
+
async function goto(cdpWsUrl, url) {
|
|
11178
|
+
const browser = await playwrightChromium.connectOverCDP(cdpWsUrl);
|
|
11179
|
+
try {
|
|
11180
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
11181
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
11182
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
11183
|
+
return { url: page.url(), title: await page.title() };
|
|
11184
|
+
} finally {
|
|
11185
|
+
await browser.close().catch(() => {
|
|
11186
|
+
});
|
|
11187
|
+
}
|
|
11188
|
+
}
|
|
11189
|
+
async function readPage(cdpWsUrl) {
|
|
11190
|
+
const browser = await playwrightChromium.connectOverCDP(cdpWsUrl);
|
|
11191
|
+
try {
|
|
11192
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
11193
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
11194
|
+
const url = page.url();
|
|
11195
|
+
const title = await page.title().catch(() => "");
|
|
11196
|
+
const data = await page.evaluate(() => {
|
|
11197
|
+
const SEL = 'a[href], button, input, textarea, select, [role="button"], [role="link"], [role="tab"], [onclick]';
|
|
11198
|
+
const out = [];
|
|
11199
|
+
const nodes = Array.from(document.querySelectorAll(SEL)).slice(0, 120);
|
|
11200
|
+
for (const el2 of nodes) {
|
|
11201
|
+
const r = el2.getBoundingClientRect();
|
|
11202
|
+
if (r.width < 1 || r.height < 1) continue;
|
|
11203
|
+
if (r.bottom < 0 || r.top > window.innerHeight) continue;
|
|
11204
|
+
const e = el2;
|
|
11205
|
+
const name = (e.getAttribute("aria-label") || e.placeholder || e.innerText || e.getAttribute("value") || e.getAttribute("title") || "").trim().replace(/\s+/g, " ").slice(0, 80);
|
|
11206
|
+
out.push({
|
|
11207
|
+
role: el2.getAttribute("role") || el2.tagName.toLowerCase(),
|
|
11208
|
+
name,
|
|
11209
|
+
x: Math.round(r.left + r.width / 2),
|
|
11210
|
+
y: Math.round(r.top + r.height / 2)
|
|
11211
|
+
});
|
|
11212
|
+
}
|
|
11213
|
+
const text = (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").trim().slice(0, 6e3);
|
|
11214
|
+
return { text, els: out };
|
|
11215
|
+
});
|
|
11216
|
+
return {
|
|
11217
|
+
url,
|
|
11218
|
+
title,
|
|
11219
|
+
text: data.text,
|
|
11220
|
+
elements: data.els.map((e, i) => ({ ref: i + 1, role: e.role, name: e.name, x: e.x, y: e.y }))
|
|
11221
|
+
};
|
|
11222
|
+
} finally {
|
|
11223
|
+
await browser.close().catch(() => {
|
|
11224
|
+
});
|
|
11225
|
+
}
|
|
11226
|
+
}
|
|
11227
|
+
async function replayStart(runtimeSessionId) {
|
|
11228
|
+
const k = client();
|
|
11229
|
+
const res = await k.browsers.replays.start(runtimeSessionId);
|
|
11230
|
+
return { replayId: res.replay_id, viewUrl: res.replay_view_url ?? null };
|
|
11231
|
+
}
|
|
11232
|
+
async function replayStop(runtimeSessionId, replayId) {
|
|
11233
|
+
const k = client();
|
|
11234
|
+
await k.browsers.replays.stop(replayId, { id: runtimeSessionId });
|
|
11235
|
+
}
|
|
11236
|
+
async function replayList(runtimeSessionId) {
|
|
11237
|
+
const k = client();
|
|
11238
|
+
const res = await k.browsers.replays.list(runtimeSessionId);
|
|
11239
|
+
return res.map((r) => ({
|
|
11240
|
+
replayId: r.replay_id,
|
|
11241
|
+
viewUrl: r.replay_view_url ?? null,
|
|
11242
|
+
startedAt: r.started_at ?? null,
|
|
11243
|
+
finishedAt: r.finished_at ?? null
|
|
11244
|
+
}));
|
|
11245
|
+
}
|
|
11246
|
+
|
|
11247
|
+
// src/api/browser-agent-routes.ts
|
|
11248
|
+
var auth = createApiKeyAuth();
|
|
11249
|
+
async function charge(sessionId, userId, startedAtMs) {
|
|
11250
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
11251
|
+
const { active_ms, billed_mc } = await addActiveMs(sessionId, elapsedMs);
|
|
11252
|
+
const owed = browserActiveCostMc(active_ms);
|
|
11253
|
+
const delta = owed - billed_mc;
|
|
11254
|
+
if (delta > 0) {
|
|
11255
|
+
const res = await debitMc(userId, delta, LedgerOperation.BROWSER_SESSION, sessionId);
|
|
11256
|
+
if (res.ok) await setBilledMc(sessionId, owed);
|
|
11257
|
+
}
|
|
11258
|
+
}
|
|
11259
|
+
function publicSession(row) {
|
|
11260
|
+
return {
|
|
11261
|
+
session_id: row.id,
|
|
11262
|
+
status: row.status,
|
|
11263
|
+
label: row.label,
|
|
11264
|
+
created_at: row.created_at,
|
|
11265
|
+
last_action_at: row.last_action_at,
|
|
11266
|
+
closed_at: row.closed_at,
|
|
11267
|
+
active_seconds: Math.round((row.active_ms ?? 0) / 1e3),
|
|
11268
|
+
credits_used: Math.round((row.billed_mc ?? 0) / 10) / 100
|
|
11269
|
+
};
|
|
11270
|
+
}
|
|
11271
|
+
function failure(err) {
|
|
11272
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11273
|
+
return { error: sanitizeVendorName(msg) };
|
|
11274
|
+
}
|
|
11275
|
+
async function loadOpenSession(id, userId) {
|
|
11276
|
+
const row = await getSessionRow(id);
|
|
11277
|
+
if (!row) return null;
|
|
11278
|
+
if (row.user_id != null && row.user_id !== userId) return null;
|
|
11279
|
+
return row;
|
|
11280
|
+
}
|
|
11281
|
+
function buildBrowserAgentRoutes() {
|
|
11282
|
+
const app2 = new Hono8();
|
|
11283
|
+
app2.use("*", async (c, next) => {
|
|
11284
|
+
await migrateBrowserAgent();
|
|
11285
|
+
return next();
|
|
11286
|
+
});
|
|
11287
|
+
app2.use("*", auth);
|
|
11288
|
+
app2.post("/sessions", async (c) => {
|
|
11289
|
+
const user = c.get("user");
|
|
11290
|
+
if (Number(user.balance_mc ?? 0) < BROWSER_OPEN_MIN_BALANCE_MC) {
|
|
11291
|
+
return c.json(insufficientBalanceResponse(Number(user.balance_mc ?? 0), BROWSER_OPEN_MIN_BALANCE_MC), 402);
|
|
11292
|
+
}
|
|
11293
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11294
|
+
try {
|
|
11295
|
+
const created = await createSession({
|
|
11296
|
+
timeoutSeconds: typeof body.timeout_seconds === "number" ? body.timeout_seconds : void 0,
|
|
11297
|
+
proxyId: typeof body.proxy_id === "string" ? body.proxy_id : void 0,
|
|
11298
|
+
profileName: typeof body.profile === "string" ? body.profile : void 0,
|
|
11299
|
+
disableDefaultProxy: body.disable_default_proxy === true,
|
|
11300
|
+
viewport: body.viewport && typeof body.viewport === "object" ? body.viewport : void 0
|
|
11301
|
+
});
|
|
11302
|
+
const row = await createSessionRow({
|
|
11303
|
+
runtimeSessionId: created.runtimeSessionId,
|
|
11304
|
+
liveViewUrl: created.liveViewUrl,
|
|
11305
|
+
cdpWsUrl: created.cdpWsUrl,
|
|
11306
|
+
label: typeof body.label === "string" ? body.label : null,
|
|
11307
|
+
userId: user.id
|
|
11308
|
+
});
|
|
11309
|
+
return c.json({ ...publicSession(row), watch_url: `/console/${row.id}` });
|
|
11310
|
+
} catch (err) {
|
|
11311
|
+
return c.json(failure(err), 502);
|
|
11312
|
+
}
|
|
11313
|
+
});
|
|
11314
|
+
app2.get("/sessions", async (c) => {
|
|
11315
|
+
const user = c.get("user");
|
|
11316
|
+
const includeClosed = c.req.query("all") === "1";
|
|
11317
|
+
const rows = await listSessionRows(user.id, includeClosed);
|
|
11318
|
+
return c.json({ sessions: rows.map(publicSession) });
|
|
11319
|
+
});
|
|
11320
|
+
app2.get("/sessions/:id", async (c) => {
|
|
11321
|
+
const user = c.get("user");
|
|
11322
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11323
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11324
|
+
return c.json({ ...publicSession(row), watch_url: `/console/${row.id}` });
|
|
11325
|
+
});
|
|
11326
|
+
app2.get("/sessions/:id/live-view", async (c) => {
|
|
11327
|
+
const user = c.get("user");
|
|
11328
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11329
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11330
|
+
return c.json({ live_view_url: row.live_view_url });
|
|
11331
|
+
});
|
|
11332
|
+
app2.delete("/sessions/:id", async (c) => {
|
|
11333
|
+
const user = c.get("user");
|
|
11334
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11335
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11336
|
+
try {
|
|
11337
|
+
await closeSession(row.runtime_session_id);
|
|
11338
|
+
} catch {
|
|
11339
|
+
}
|
|
11340
|
+
await markSessionClosed(row.id);
|
|
11341
|
+
return c.json({ ok: true });
|
|
11342
|
+
});
|
|
11343
|
+
app2.post("/sessions/:id/goto", async (c) => {
|
|
11344
|
+
const user = c.get("user");
|
|
11345
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11346
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11347
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11348
|
+
const url = typeof body.url === "string" ? body.url : "";
|
|
11349
|
+
if (!url) return c.json({ error: "url is required" }, 400);
|
|
11350
|
+
const t0 = Date.now();
|
|
11351
|
+
try {
|
|
11352
|
+
const result = await goto(row.cdp_ws_url, url);
|
|
11353
|
+
await charge(row.id, user.id, t0);
|
|
11354
|
+
await recordAction({ sessionId: row.id, type: "goto", params: { url }, ok: true });
|
|
11355
|
+
return c.json(result);
|
|
11356
|
+
} catch (err) {
|
|
11357
|
+
await charge(row.id, user.id, t0);
|
|
11358
|
+
await recordAction({ sessionId: row.id, type: "goto", params: { url }, ok: false, error: String(err) });
|
|
11359
|
+
return c.json(failure(err), 502);
|
|
11360
|
+
}
|
|
11361
|
+
});
|
|
11362
|
+
app2.post("/sessions/:id/screenshot", async (c) => {
|
|
11363
|
+
const user = c.get("user");
|
|
11364
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11365
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11366
|
+
const t0 = Date.now();
|
|
11367
|
+
try {
|
|
11368
|
+
const shot = await screenshot(row.runtime_session_id);
|
|
11369
|
+
let page = null;
|
|
11370
|
+
try {
|
|
11371
|
+
page = await readPage(row.cdp_ws_url);
|
|
11372
|
+
} catch {
|
|
11373
|
+
page = null;
|
|
11374
|
+
}
|
|
11375
|
+
await charge(row.id, user.id, t0);
|
|
11376
|
+
await recordAction({ sessionId: row.id, type: "screenshot", params: null, ok: true });
|
|
11377
|
+
return c.json({
|
|
11378
|
+
image_base64: shot.base64,
|
|
11379
|
+
mime_type: shot.mimeType,
|
|
11380
|
+
url: page?.url ?? null,
|
|
11381
|
+
title: page?.title ?? null,
|
|
11382
|
+
elements: page?.elements ?? [],
|
|
11383
|
+
text: page?.text ?? null
|
|
11384
|
+
});
|
|
11385
|
+
} catch (err) {
|
|
11386
|
+
await charge(row.id, user.id, t0);
|
|
11387
|
+
await recordAction({ sessionId: row.id, type: "screenshot", params: null, ok: false, error: String(err) });
|
|
11388
|
+
return c.json(failure(err), 502);
|
|
11389
|
+
}
|
|
11390
|
+
});
|
|
11391
|
+
app2.post("/sessions/:id/read", async (c) => {
|
|
11392
|
+
const user = c.get("user");
|
|
11393
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11394
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11395
|
+
const t0 = Date.now();
|
|
11396
|
+
try {
|
|
11397
|
+
const page = await readPage(row.cdp_ws_url);
|
|
11398
|
+
await charge(row.id, user.id, t0);
|
|
11399
|
+
await recordAction({ sessionId: row.id, type: "read", params: null, ok: true });
|
|
11400
|
+
return c.json(page);
|
|
11401
|
+
} catch (err) {
|
|
11402
|
+
await charge(row.id, user.id, t0);
|
|
11403
|
+
return c.json(failure(err), 502);
|
|
11404
|
+
}
|
|
11405
|
+
});
|
|
11406
|
+
app2.post("/sessions/:id/click", async (c) => {
|
|
11407
|
+
const user = c.get("user");
|
|
11408
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11409
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11410
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11411
|
+
const x = Number(body.x);
|
|
11412
|
+
const y = Number(body.y);
|
|
11413
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return c.json({ error: "x and y are required" }, 400);
|
|
11414
|
+
const t0 = Date.now();
|
|
11415
|
+
try {
|
|
11416
|
+
await click(row.runtime_session_id, x, y, {
|
|
11417
|
+
button: body.button === "right" || body.button === "middle" ? body.button : "left",
|
|
11418
|
+
numClicks: typeof body.num_clicks === "number" ? body.num_clicks : void 0
|
|
11419
|
+
});
|
|
11420
|
+
await charge(row.id, user.id, t0);
|
|
11421
|
+
await recordAction({ sessionId: row.id, type: "click", params: { x, y }, ok: true });
|
|
11422
|
+
return c.json({ ok: true });
|
|
11423
|
+
} catch (err) {
|
|
11424
|
+
await charge(row.id, user.id, t0);
|
|
11425
|
+
await recordAction({ sessionId: row.id, type: "click", params: { x, y }, ok: false, error: String(err) });
|
|
11426
|
+
return c.json(failure(err), 502);
|
|
11427
|
+
}
|
|
11428
|
+
});
|
|
11429
|
+
app2.post("/sessions/:id/type", async (c) => {
|
|
11430
|
+
const user = c.get("user");
|
|
11431
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11432
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11433
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11434
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
11435
|
+
if (!text) return c.json({ error: "text is required" }, 400);
|
|
11436
|
+
const t0 = Date.now();
|
|
11437
|
+
try {
|
|
11438
|
+
await typeText(row.runtime_session_id, text, typeof body.delay === "number" ? body.delay : void 0);
|
|
11439
|
+
await charge(row.id, user.id, t0);
|
|
11440
|
+
await recordAction({ sessionId: row.id, type: "type", params: { length: text.length }, ok: true });
|
|
11441
|
+
return c.json({ ok: true });
|
|
11442
|
+
} catch (err) {
|
|
11443
|
+
await charge(row.id, user.id, t0);
|
|
11444
|
+
return c.json(failure(err), 502);
|
|
11445
|
+
}
|
|
11446
|
+
});
|
|
11447
|
+
app2.post("/sessions/:id/scroll", async (c) => {
|
|
11448
|
+
const user = c.get("user");
|
|
11449
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11450
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11451
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11452
|
+
const x = typeof body.x === "number" ? body.x : 640;
|
|
11453
|
+
const y = typeof body.y === "number" ? body.y : 400;
|
|
11454
|
+
const deltaX = typeof body.delta_x === "number" ? body.delta_x : 0;
|
|
11455
|
+
const deltaY = typeof body.delta_y === "number" ? body.delta_y : 5;
|
|
11456
|
+
const t0 = Date.now();
|
|
11457
|
+
try {
|
|
11458
|
+
await scroll(row.runtime_session_id, x, y, deltaX, deltaY);
|
|
11459
|
+
await charge(row.id, user.id, t0);
|
|
11460
|
+
await recordAction({ sessionId: row.id, type: "scroll", params: { deltaX, deltaY }, ok: true });
|
|
11461
|
+
return c.json({ ok: true });
|
|
11462
|
+
} catch (err) {
|
|
11463
|
+
await charge(row.id, user.id, t0);
|
|
11464
|
+
return c.json(failure(err), 502);
|
|
11465
|
+
}
|
|
11466
|
+
});
|
|
11467
|
+
app2.post("/sessions/:id/press", async (c) => {
|
|
11468
|
+
const user = c.get("user");
|
|
11469
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11470
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11471
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11472
|
+
const keys = Array.isArray(body.keys) ? body.keys.map(String) : [];
|
|
11473
|
+
if (!keys.length) return c.json({ error: "keys is required" }, 400);
|
|
11474
|
+
const t0 = Date.now();
|
|
11475
|
+
try {
|
|
11476
|
+
await pressKeys(row.runtime_session_id, keys);
|
|
11477
|
+
await charge(row.id, user.id, t0);
|
|
11478
|
+
await recordAction({ sessionId: row.id, type: "press", params: { keys }, ok: true });
|
|
11479
|
+
return c.json({ ok: true });
|
|
11480
|
+
} catch (err) {
|
|
11481
|
+
await charge(row.id, user.id, t0);
|
|
11482
|
+
return c.json(failure(err), 502);
|
|
11483
|
+
}
|
|
11484
|
+
});
|
|
11485
|
+
app2.post("/sessions/:id/replay/start", async (c) => {
|
|
11486
|
+
const user = c.get("user");
|
|
11487
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11488
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11489
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11490
|
+
try {
|
|
11491
|
+
const started = await replayStart(row.runtime_session_id);
|
|
11492
|
+
await recordReplayStart({
|
|
11493
|
+
sessionId: row.id,
|
|
11494
|
+
replayId: started.replayId,
|
|
11495
|
+
viewUrl: started.viewUrl,
|
|
11496
|
+
label: typeof body.label === "string" ? body.label : null
|
|
11497
|
+
});
|
|
11498
|
+
return c.json({ replay_id: started.replayId });
|
|
11499
|
+
} catch (err) {
|
|
11500
|
+
return c.json(failure(err), 502);
|
|
11501
|
+
}
|
|
11502
|
+
});
|
|
11503
|
+
app2.post("/sessions/:id/replay/stop", async (c) => {
|
|
11504
|
+
const user = c.get("user");
|
|
11505
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11506
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11507
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11508
|
+
const replayId = typeof body.replay_id === "string" ? body.replay_id : "";
|
|
11509
|
+
if (!replayId) return c.json({ error: "replay_id is required" }, 400);
|
|
11510
|
+
try {
|
|
11511
|
+
await replayStop(row.runtime_session_id, replayId);
|
|
11512
|
+
let viewUrl = null;
|
|
11513
|
+
try {
|
|
11514
|
+
const all = await replayList(row.runtime_session_id);
|
|
11515
|
+
viewUrl = all.find((r) => r.replayId === replayId)?.viewUrl ?? null;
|
|
11516
|
+
} catch {
|
|
11517
|
+
viewUrl = null;
|
|
11518
|
+
}
|
|
11519
|
+
await recordReplayStop(replayId, viewUrl);
|
|
11520
|
+
return c.json({ ok: true });
|
|
11521
|
+
} catch (err) {
|
|
11522
|
+
return c.json(failure(err), 502);
|
|
11523
|
+
}
|
|
11524
|
+
});
|
|
11525
|
+
app2.get("/sessions/:id/replays", async (c) => {
|
|
11526
|
+
const user = c.get("user");
|
|
11527
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
11528
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
11529
|
+
const rows = await listReplayRows(row.id);
|
|
11530
|
+
return c.json({
|
|
11531
|
+
replays: rows.map((r) => ({
|
|
11532
|
+
replay_id: r.replay_id,
|
|
11533
|
+
view_url: r.view_url,
|
|
11534
|
+
label: r.label,
|
|
11535
|
+
started_at: r.started_at,
|
|
11536
|
+
stopped_at: r.stopped_at
|
|
11537
|
+
}))
|
|
11538
|
+
});
|
|
11539
|
+
});
|
|
11540
|
+
return app2;
|
|
11541
|
+
}
|
|
11542
|
+
|
|
11543
|
+
// src/api/browser-agent-console.ts
|
|
11544
|
+
function renderConsoleHtml(initialSessionId) {
|
|
11545
|
+
const initial = JSON.stringify(initialSessionId ?? "");
|
|
11546
|
+
return `<!DOCTYPE html>
|
|
11547
|
+
<html lang="en">
|
|
11548
|
+
<head>
|
|
11549
|
+
<meta charset="UTF-8" />
|
|
11550
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
11551
|
+
<title>Browser Agent Console</title>
|
|
11552
|
+
<style>
|
|
11553
|
+
:root { color-scheme: dark; }
|
|
11554
|
+
:where(*) { box-sizing: border-box; }
|
|
11555
|
+
body { margin: 0; font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif; background: #0b0e14; color: #d7dce5; }
|
|
11556
|
+
header { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid #1c2230; background: #0f131c; }
|
|
11557
|
+
header h1 { font-size: 15px; margin: 0; font-weight: 600; color: #fff; letter-spacing: .2px; }
|
|
11558
|
+
header .spacer { flex: 1; }
|
|
11559
|
+
input, button, select { font: inherit; }
|
|
11560
|
+
input[type=text], input[type=password], input[type=url] { background: #141925; border: 1px solid #232b3a; color: #e6eaf2; border-radius: 7px; padding: 7px 10px; }
|
|
11561
|
+
button { background: #2b6cff; border: 0; color: #fff; border-radius: 7px; padding: 7px 12px; cursor: pointer; font-weight: 500; }
|
|
11562
|
+
button.ghost { background: #1a2030; color: #cdd5e4; border: 1px solid #28303f; }
|
|
11563
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
11564
|
+
.layout { display: grid; grid-template-columns: 280px 1fr; height: calc(100vh - 53px); }
|
|
11565
|
+
aside { border-right: 1px solid #1c2230; overflow-y: auto; padding: 12px; }
|
|
11566
|
+
aside h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #6b7689; margin: 4px 4px 10px; }
|
|
11567
|
+
.sess { padding: 9px 10px; border-radius: 8px; border: 1px solid #1c2230; margin-bottom: 8px; cursor: pointer; }
|
|
11568
|
+
.sess:hover { border-color: #2b6cff; }
|
|
11569
|
+
.sess.active { border-color: #2b6cff; background: #131b2e; }
|
|
11570
|
+
.sess .id { font-family: ui-monospace, monospace; font-size: 12px; color: #aeb8cc; }
|
|
11571
|
+
.sess .meta { font-size: 11px; color: #6b7689; margin-top: 3px; }
|
|
11572
|
+
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; }
|
|
11573
|
+
.dot.open { background: #36d399; } .dot.closed { background: #5a6677; }
|
|
11574
|
+
main { display: flex; flex-direction: column; overflow: hidden; }
|
|
11575
|
+
.toolbar { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid #1c2230; }
|
|
11576
|
+
.toolbar label { font-size: 13px; color: #aeb8cc; display: flex; align-items: center; gap: 6px; }
|
|
11577
|
+
.stage { flex: 1; position: relative; background: #05070b; overflow: auto; }
|
|
11578
|
+
.stage iframe { width: 100%; height: 100%; border: 0; display: block; }
|
|
11579
|
+
.empty { display: flex; align-items: center; justify-content: center; height: 100%; color: #5a6677; flex-direction: column; gap: 10px; }
|
|
11580
|
+
.replays { border-top: 1px solid #1c2230; padding: 10px 16px; max-height: 200px; overflow-y: auto; }
|
|
11581
|
+
.replays h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #6b7689; margin: 0 0 8px; }
|
|
11582
|
+
.replay { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; }
|
|
11583
|
+
.replay a { color: #7aa2ff; }
|
|
11584
|
+
.gate { max-width: 380px; margin: 80px auto; padding: 24px; border: 1px solid #1c2230; border-radius: 12px; background: #0f131c; }
|
|
11585
|
+
.gate h2 { margin: 0 0 6px; font-size: 16px; color: #fff; }
|
|
11586
|
+
.gate p { color: #8893a7; margin: 0 0 16px; }
|
|
11587
|
+
.gate input { width: 100%; margin-bottom: 12px; }
|
|
11588
|
+
.gate button { width: 100%; }
|
|
11589
|
+
.muted { color: #6b7689; font-size: 12px; }
|
|
11590
|
+
</style>
|
|
11591
|
+
</head>
|
|
11592
|
+
<body>
|
|
11593
|
+
<div id="app"></div>
|
|
11594
|
+
<script>
|
|
11595
|
+
const INITIAL_SESSION = ${initial};
|
|
11596
|
+
const KEY_STORE = 'browser_agent_api_key';
|
|
11597
|
+
let state = { key: localStorage.getItem(KEY_STORE) || '', sessions: [], current: INITIAL_SESSION || null, readOnly: true, liveUrl: null, replays: [] };
|
|
11598
|
+
|
|
11599
|
+
function api(method, path, body) {
|
|
11600
|
+
return fetch('/agent' + path, {
|
|
11601
|
+
method,
|
|
11602
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': state.key },
|
|
11603
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
11604
|
+
}).then(async r => ({ ok: r.ok, data: await r.json().catch(() => ({})) }));
|
|
11605
|
+
}
|
|
11606
|
+
|
|
11607
|
+
async function refreshSessions() {
|
|
11608
|
+
const r = await api('GET', '/sessions?all=1');
|
|
11609
|
+
if (r.ok) { state.sessions = r.data.sessions || []; render(); }
|
|
11610
|
+
}
|
|
11611
|
+
|
|
11612
|
+
async function selectSession(id) {
|
|
11613
|
+
state.current = id; state.liveUrl = null; state.replays = [];
|
|
11614
|
+
history.replaceState(null, '', '/console/' + id);
|
|
11615
|
+
render();
|
|
11616
|
+
const live = await api('GET', '/sessions/' + id + '/live-view');
|
|
11617
|
+
state.liveUrl = live.ok ? live.data.live_view_url : null;
|
|
11618
|
+
const reps = await api('GET', '/sessions/' + id + '/replays');
|
|
11619
|
+
state.replays = reps.ok ? (reps.data.replays || []) : [];
|
|
11620
|
+
render();
|
|
11621
|
+
}
|
|
11622
|
+
|
|
11623
|
+
async function openSession() {
|
|
11624
|
+
const r = await api('POST', '/sessions', { label: 'console' });
|
|
11625
|
+
if (r.ok) { await refreshSessions(); selectSession(r.data.session_id); }
|
|
11626
|
+
else alert('Open failed: ' + JSON.stringify(r.data));
|
|
11627
|
+
}
|
|
11628
|
+
|
|
11629
|
+
async function closeCurrent() {
|
|
11630
|
+
if (!state.current) return;
|
|
11631
|
+
await api('DELETE', '/sessions/' + state.current);
|
|
11632
|
+
await refreshSessions();
|
|
11633
|
+
}
|
|
11634
|
+
|
|
11635
|
+
function frameSrc() {
|
|
11636
|
+
if (!state.liveUrl) return null;
|
|
11637
|
+
const sep = state.liveUrl.includes('?') ? '&' : '?';
|
|
11638
|
+
return state.readOnly ? state.liveUrl + sep + 'readOnly=true' : state.liveUrl;
|
|
11639
|
+
}
|
|
11640
|
+
|
|
11641
|
+
function saveKey(v) { state.key = v.trim(); localStorage.setItem(KEY_STORE, state.key); render(); if (state.key) { refreshSessions(); if (state.current) selectSession(state.current); } }
|
|
11642
|
+
|
|
11643
|
+
function h(html) { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstChild; }
|
|
11644
|
+
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
11645
|
+
|
|
11646
|
+
function render() {
|
|
11647
|
+
const app = document.getElementById('app');
|
|
11648
|
+
app.innerHTML = '';
|
|
11649
|
+
if (!state.key) {
|
|
11650
|
+
app.appendChild(h('<div class="gate"><h2>Browser Agent Console</h2><p>Paste your API key to watch and control browser sessions.</p><input id="k" type="password" placeholder="API key" /><button id="kb">Continue</button></div>'));
|
|
11651
|
+
document.getElementById('kb').onclick = () => saveKey(document.getElementById('k').value);
|
|
11652
|
+
document.getElementById('k').onkeydown = e => { if (e.key === 'Enter') saveKey(e.target.value); };
|
|
11653
|
+
return;
|
|
11654
|
+
}
|
|
11655
|
+
const header = h('<header><h1>Browser Agent</h1><span class="muted">live control + replays</span><span class="spacer"></span><button id="open">+ New Session</button><button class="ghost" id="logout">Forget key</button></header>');
|
|
11656
|
+
app.appendChild(header);
|
|
11657
|
+
document.getElementById('open').onclick = openSession;
|
|
11658
|
+
document.getElementById('logout').onclick = () => saveKey('');
|
|
11659
|
+
|
|
11660
|
+
const layout = h('<div class="layout"></div>');
|
|
11661
|
+
const aside = h('<aside><h2>Sessions</h2></aside>');
|
|
11662
|
+
if (!state.sessions.length) aside.appendChild(h('<div class="muted" style="padding:4px">No sessions yet.</div>'));
|
|
11663
|
+
for (const s of state.sessions) {
|
|
11664
|
+
const el = h('<div class="sess ' + (s.session_id === state.current ? 'active' : '') + '"><div class="id">' + esc(s.session_id) + '</div><div class="meta"><span class="dot ' + esc(s.status) + '"></span>' + esc(s.status) + (s.label ? ' \xB7 ' + esc(s.label) : '') + '</div></div>');
|
|
11665
|
+
el.onclick = () => selectSession(s.session_id);
|
|
11666
|
+
aside.appendChild(el);
|
|
11667
|
+
}
|
|
11668
|
+
layout.appendChild(aside);
|
|
11669
|
+
|
|
11670
|
+
const main = h('<main></main>');
|
|
11671
|
+
if (!state.current) {
|
|
11672
|
+
main.appendChild(h('<div class="empty"><div>Select or open a session to watch.</div></div>'));
|
|
11673
|
+
} else {
|
|
11674
|
+
const tb = h('<div class="toolbar"><label><input type="checkbox" id="ro" ' + (state.readOnly ? 'checked' : '') + ' /> Read-only (uncheck to take control)</label><span class="spacer"></span><button class="ghost" id="reload">Reload view</button><button class="ghost" id="close">Close session</button></div>');
|
|
11675
|
+
main.appendChild(tb);
|
|
11676
|
+
const stage = h('<div class="stage"></div>');
|
|
11677
|
+
const src = frameSrc();
|
|
11678
|
+
if (src) {
|
|
11679
|
+
const f = h('<iframe allow="autoplay; clipboard-read; clipboard-write" src="' + esc(src) + '"></iframe>');
|
|
11680
|
+
stage.appendChild(f);
|
|
11681
|
+
} else {
|
|
11682
|
+
stage.appendChild(h('<div class="empty"><div>Live view unavailable for this session.</div><div class="muted">It may be closed or still starting.</div></div>'));
|
|
11683
|
+
}
|
|
11684
|
+
main.appendChild(stage);
|
|
11685
|
+
|
|
11686
|
+
const rep = h('<div class="replays"><h3>Replays</h3></div>');
|
|
11687
|
+
if (!state.replays.length) rep.appendChild(h('<div class="muted">No replays recorded.</div>'));
|
|
11688
|
+
for (const r of state.replays) {
|
|
11689
|
+
const status = r.stopped_at ? 'ready' : 'recording\u2026';
|
|
11690
|
+
const link = r.view_url ? '<a href="' + esc(r.view_url) + '" target="_blank" rel="noopener">view mp4</a>' : '<span class="muted">' + status + '</span>';
|
|
11691
|
+
rep.appendChild(h('<div class="replay"><span class="muted">' + esc(r.started_at || '') + '</span><span class="spacer"></span>' + link + '</div>'));
|
|
11692
|
+
}
|
|
11693
|
+
main.appendChild(rep);
|
|
11694
|
+
|
|
11695
|
+
layout.appendChild(main);
|
|
11696
|
+
}
|
|
11697
|
+
app.appendChild(layout);
|
|
11698
|
+
|
|
11699
|
+
const ro = document.getElementById('ro');
|
|
11700
|
+
if (ro) ro.onchange = e => { state.readOnly = e.target.checked; render(); };
|
|
11701
|
+
const reload = document.getElementById('reload');
|
|
11702
|
+
if (reload) reload.onclick = () => selectSession(state.current);
|
|
11703
|
+
const close = document.getElementById('close');
|
|
11704
|
+
if (close) close.onclick = closeCurrent;
|
|
11705
|
+
}
|
|
11706
|
+
|
|
11707
|
+
render();
|
|
11708
|
+
if (state.key) { refreshSessions(); if (state.current) selectSession(state.current); }
|
|
11709
|
+
</script>
|
|
11710
|
+
</body>
|
|
11711
|
+
</html>`;
|
|
11712
|
+
}
|
|
11713
|
+
|
|
10955
11714
|
// src/api/stripe-routes.ts
|
|
10956
11715
|
import Stripe from "stripe";
|
|
10957
|
-
import { Hono as
|
|
11716
|
+
import { Hono as Hono9 } from "hono";
|
|
10958
11717
|
var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
10959
|
-
var stripeApp = new
|
|
11718
|
+
var stripeApp = new Hono9();
|
|
10960
11719
|
stripeApp.post("/webhooks", async (c) => {
|
|
10961
11720
|
const sig = c.req.header("stripe-signature");
|
|
10962
11721
|
const body = await c.req.text();
|
|
@@ -11253,7 +12012,7 @@ var requireAllowedOrigin = createMiddleware3(async (c, next) => {
|
|
|
11253
12012
|
if (!configuredOrigins().has(origin)) return c.json({ error: "Origin not allowed" }, 403);
|
|
11254
12013
|
return next();
|
|
11255
12014
|
});
|
|
11256
|
-
var
|
|
12015
|
+
var auth2 = createMiddleware3(async (c, next) => {
|
|
11257
12016
|
const key = c.req.header("x-api-key");
|
|
11258
12017
|
if (!key) return c.json({ error: "Missing API key" }, 401);
|
|
11259
12018
|
const user = await getUserByApiKey(key);
|
|
@@ -11282,7 +12041,7 @@ var sessionAuth = createMiddleware3(async (c, next) => {
|
|
|
11282
12041
|
c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
|
|
11283
12042
|
return next();
|
|
11284
12043
|
});
|
|
11285
|
-
var app = new
|
|
12044
|
+
var app = new Hono10();
|
|
11286
12045
|
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
11287
12046
|
function requireStripeSecret() {
|
|
11288
12047
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
@@ -11492,7 +12251,7 @@ async function checkHarvestLimits(userId, email, extraSlots = 0) {
|
|
|
11492
12251
|
if (active >= limit) return { error: `You have ${active} job${active !== 1 ? "s" : ""} running. Your account allows ${limit} concurrent job${limit !== 1 ? "s" : ""}. Wait for one to finish or add a concurrency slot at mcpscraper.dev/billing.` };
|
|
11493
12252
|
return null;
|
|
11494
12253
|
}
|
|
11495
|
-
app.post("/harvest",
|
|
12254
|
+
app.post("/harvest", auth2, async (c) => {
|
|
11496
12255
|
const user = c.get("user");
|
|
11497
12256
|
const raw = await c.req.json().catch(() => ({}));
|
|
11498
12257
|
const bodyResult = HarvestBodySchema.safeParse(raw);
|
|
@@ -11534,7 +12293,7 @@ app.post("/harvest", auth, async (c) => {
|
|
|
11534
12293
|
}
|
|
11535
12294
|
return c.json({ job_id: jobId, status: "pending" }, 202);
|
|
11536
12295
|
});
|
|
11537
|
-
app.post("/harvest/sync",
|
|
12296
|
+
app.post("/harvest/sync", auth2, async (c) => {
|
|
11538
12297
|
const user = c.get("user");
|
|
11539
12298
|
const raw = await c.req.json().catch(() => ({}));
|
|
11540
12299
|
const bodyResult = HarvestBodySchema.safeParse(raw);
|
|
@@ -11599,17 +12358,17 @@ app.post("/harvest/sync", auth, async (c) => {
|
|
|
11599
12358
|
return c.json({ job_id: jobId, status: "failed", ...response, attempts: sanitizeAttempts(attempts) }, problem.httpStatus);
|
|
11600
12359
|
}
|
|
11601
12360
|
});
|
|
11602
|
-
app.get("/jobs/:id",
|
|
12361
|
+
app.get("/jobs/:id", auth2, async (c) => {
|
|
11603
12362
|
const job = await getJob(c.req.param("id"), c.get("user").id);
|
|
11604
12363
|
if (!job) return c.json({ error: "Job not found" }, 404);
|
|
11605
12364
|
const attempts = await listHarvestAttempts(job.id, c.get("user").id);
|
|
11606
12365
|
const safeResult = job.result && typeof job.result === "object" ? sanitizeHarvestResult(job.result) : job.result;
|
|
11607
12366
|
return c.json({ ...job, result: safeResult, attempts: sanitizeAttempts(attempts) });
|
|
11608
12367
|
});
|
|
11609
|
-
app.get("/jobs",
|
|
12368
|
+
app.get("/jobs", auth2, async (c) => {
|
|
11610
12369
|
return c.json(await listJobs(c.get("user").id));
|
|
11611
12370
|
});
|
|
11612
|
-
app.get("/history",
|
|
12371
|
+
app.get("/history", auth2, async (c) => {
|
|
11613
12372
|
const userId = c.get("user").id;
|
|
11614
12373
|
const [jobs, events] = await Promise.all([
|
|
11615
12374
|
listJobs(userId),
|
|
@@ -11639,7 +12398,7 @@ app.get("/history", auth, async (c) => {
|
|
|
11639
12398
|
const rows = [...jobRows, ...eventRows].sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
|
|
11640
12399
|
return c.json(rows.slice(0, 100));
|
|
11641
12400
|
});
|
|
11642
|
-
app.get("/ledger",
|
|
12401
|
+
app.get("/ledger", auth2, async (c) => {
|
|
11643
12402
|
return c.json(await getLedger(c.get("user").id, 100));
|
|
11644
12403
|
});
|
|
11645
12404
|
app.post("/admin/users", adminAuth, async (c) => {
|
|
@@ -11677,11 +12436,11 @@ app.post("/admin/backfill-signup-credits", adminAuth, async (c) => {
|
|
|
11677
12436
|
}
|
|
11678
12437
|
return c.json({ processed, credited, skipped, users_credited });
|
|
11679
12438
|
});
|
|
11680
|
-
app.post("/extract-url",
|
|
12439
|
+
app.post("/extract-url", auth2, async (c) => {
|
|
11681
12440
|
const raw = await c.req.json().catch(() => ({}));
|
|
11682
12441
|
const bodyResult = ExtractUrlBodySchema.safeParse(raw);
|
|
11683
12442
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
11684
|
-
const { url, screenshot, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
12443
|
+
const { url, screenshot: screenshot2, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
11685
12444
|
if (!allowLocal) {
|
|
11686
12445
|
const checked = await validatePublicHttpUrl(url, { field: "URL" });
|
|
11687
12446
|
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
@@ -11707,7 +12466,7 @@ app.post("/extract-url", auth, async (c) => {
|
|
|
11707
12466
|
const device = screenshotDevice === "mobile" ? "mobile" : "desktop";
|
|
11708
12467
|
const [result, pageData] = await Promise.all([
|
|
11709
12468
|
extractKpo({ url: canonicalUrl, kernelApiKey }),
|
|
11710
|
-
|
|
12469
|
+
screenshot2 || extractBranding ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding }).catch(() => null) : null
|
|
11711
12470
|
]);
|
|
11712
12471
|
const screenshotBuf = pageData?.screenshot ?? null;
|
|
11713
12472
|
const brandingData = pageData?.branding ?? null;
|
|
@@ -11725,7 +12484,7 @@ app.post("/extract-url", auth, async (c) => {
|
|
|
11725
12484
|
return c.json({ error: msg }, 500);
|
|
11726
12485
|
}
|
|
11727
12486
|
});
|
|
11728
|
-
app.post("/map-urls",
|
|
12487
|
+
app.post("/map-urls", auth2, async (c) => {
|
|
11729
12488
|
const raw = await c.req.json().catch(() => ({}));
|
|
11730
12489
|
const bodyResult = MapUrlsBodySchema.safeParse(raw);
|
|
11731
12490
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -11765,7 +12524,7 @@ app.post("/map-urls", auth, async (c) => {
|
|
|
11765
12524
|
return c.json({ error: msg }, 500);
|
|
11766
12525
|
}
|
|
11767
12526
|
});
|
|
11768
|
-
app.post("/extract-site",
|
|
12527
|
+
app.post("/extract-site", auth2, async (c) => {
|
|
11769
12528
|
const raw = await c.req.json().catch(() => ({}));
|
|
11770
12529
|
const bodyResult = ExtractSiteBodySchema.safeParse(raw);
|
|
11771
12530
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -11887,7 +12646,7 @@ app.post("/billing/concurrency/cancel", requireAllowedOrigin, sessionAuth, async
|
|
|
11887
12646
|
await setConcurrencySubId(user.id, null);
|
|
11888
12647
|
return c.json({ ok: true, concurrency_limit: user.extra_concurrency_slots });
|
|
11889
12648
|
});
|
|
11890
|
-
app.get("/billing/balance",
|
|
12649
|
+
app.get("/billing/balance", auth2, async (c) => {
|
|
11891
12650
|
const user = c.get("user");
|
|
11892
12651
|
const balanceMc = await reconcileBalanceMc(user.id);
|
|
11893
12652
|
const ledger = await getLedger(user.id, 20);
|
|
@@ -11899,7 +12658,7 @@ app.get("/billing/balance", auth, async (c) => {
|
|
|
11899
12658
|
ledger
|
|
11900
12659
|
});
|
|
11901
12660
|
});
|
|
11902
|
-
app.post("/billing/credits",
|
|
12661
|
+
app.post("/billing/credits", auth2, async (c) => {
|
|
11903
12662
|
const user = c.get("user");
|
|
11904
12663
|
const balanceMc = await reconcileBalanceMc(user.id);
|
|
11905
12664
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -11928,7 +12687,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
11928
12687
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
11929
12688
|
return c.json({ error: "Unauthorized" }, 401);
|
|
11930
12689
|
}
|
|
11931
|
-
const { drainQueue } = await import("./worker-
|
|
12690
|
+
const { drainQueue } = await import("./worker-KJ4A7WIR.js");
|
|
11932
12691
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
11933
12692
|
const [results, sweepResult] = await Promise.all([
|
|
11934
12693
|
drainQueue(budget),
|
|
@@ -11944,6 +12703,9 @@ app.route("/facebook", facebookAdApp);
|
|
|
11944
12703
|
app.route("/maps", mapsApp);
|
|
11945
12704
|
app.route("/serp-intelligence", serpIntelligenceApp);
|
|
11946
12705
|
app.route("/mcp", mcpApp);
|
|
12706
|
+
app.route("/agent", buildBrowserAgentRoutes());
|
|
12707
|
+
app.get("/console", (c) => c.html(renderConsoleHtml()));
|
|
12708
|
+
app.get("/console/:id", (c) => c.html(renderConsoleHtml(c.req.param("id"))));
|
|
11947
12709
|
app.route("/stripe", stripeApp);
|
|
11948
12710
|
if (!process.env.INNGEST_EVENT_KEY) {
|
|
11949
12711
|
startSiteAuditWorker();
|
|
@@ -12050,4 +12812,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
12050
12812
|
export {
|
|
12051
12813
|
app
|
|
12052
12814
|
};
|
|
12053
|
-
//# sourceMappingURL=server-
|
|
12815
|
+
//# sourceMappingURL=server-ASCMKUQ5.js.map
|