mcp-scraper 0.1.8 → 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/README.md +4 -0
- package/dist/bin/api-server.cjs +1398 -552
- 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 +351 -314
- 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-RE6HCRYC.js → chunk-BMVQB3WN.js} +352 -315
- 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-QXVVTKJP.js → server-ASCMKUQ5.js} +794 -29
- 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-RE6HCRYC.js.map +0 -1
- package/dist/chunk-ZK456YXN.js.map +0 -1
- package/dist/server-QXVVTKJP.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
|
});
|
|
@@ -10901,7 +10904,10 @@ function mcpAuthError() {
|
|
|
10901
10904
|
});
|
|
10902
10905
|
return new Response(body, {
|
|
10903
10906
|
status: 401,
|
|
10904
|
-
headers: {
|
|
10907
|
+
headers: {
|
|
10908
|
+
"Content-Type": "application/json",
|
|
10909
|
+
"WWW-Authenticate": 'Bearer realm="mcp-scraper", error="invalid_token", error_description="Pass an MCP Scraper API key as x-api-key or Bearer token"'
|
|
10910
|
+
}
|
|
10905
10911
|
});
|
|
10906
10912
|
}
|
|
10907
10913
|
async function requireMcpCallerKey(c) {
|
|
@@ -10949,11 +10955,767 @@ mcpApp.all("/", async (c) => {
|
|
|
10949
10955
|
}
|
|
10950
10956
|
});
|
|
10951
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
|
+
|
|
10952
11714
|
// src/api/stripe-routes.ts
|
|
10953
11715
|
import Stripe from "stripe";
|
|
10954
|
-
import { Hono as
|
|
11716
|
+
import { Hono as Hono9 } from "hono";
|
|
10955
11717
|
var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
10956
|
-
var stripeApp = new
|
|
11718
|
+
var stripeApp = new Hono9();
|
|
10957
11719
|
stripeApp.post("/webhooks", async (c) => {
|
|
10958
11720
|
const sig = c.req.header("stripe-signature");
|
|
10959
11721
|
const body = await c.req.text();
|
|
@@ -11250,7 +12012,7 @@ var requireAllowedOrigin = createMiddleware3(async (c, next) => {
|
|
|
11250
12012
|
if (!configuredOrigins().has(origin)) return c.json({ error: "Origin not allowed" }, 403);
|
|
11251
12013
|
return next();
|
|
11252
12014
|
});
|
|
11253
|
-
var
|
|
12015
|
+
var auth2 = createMiddleware3(async (c, next) => {
|
|
11254
12016
|
const key = c.req.header("x-api-key");
|
|
11255
12017
|
if (!key) return c.json({ error: "Missing API key" }, 401);
|
|
11256
12018
|
const user = await getUserByApiKey(key);
|
|
@@ -11279,7 +12041,7 @@ var sessionAuth = createMiddleware3(async (c, next) => {
|
|
|
11279
12041
|
c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
|
|
11280
12042
|
return next();
|
|
11281
12043
|
});
|
|
11282
|
-
var app = new
|
|
12044
|
+
var app = new Hono10();
|
|
11283
12045
|
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
11284
12046
|
function requireStripeSecret() {
|
|
11285
12047
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
@@ -11489,7 +12251,7 @@ async function checkHarvestLimits(userId, email, extraSlots = 0) {
|
|
|
11489
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.` };
|
|
11490
12252
|
return null;
|
|
11491
12253
|
}
|
|
11492
|
-
app.post("/harvest",
|
|
12254
|
+
app.post("/harvest", auth2, async (c) => {
|
|
11493
12255
|
const user = c.get("user");
|
|
11494
12256
|
const raw = await c.req.json().catch(() => ({}));
|
|
11495
12257
|
const bodyResult = HarvestBodySchema.safeParse(raw);
|
|
@@ -11531,7 +12293,7 @@ app.post("/harvest", auth, async (c) => {
|
|
|
11531
12293
|
}
|
|
11532
12294
|
return c.json({ job_id: jobId, status: "pending" }, 202);
|
|
11533
12295
|
});
|
|
11534
|
-
app.post("/harvest/sync",
|
|
12296
|
+
app.post("/harvest/sync", auth2, async (c) => {
|
|
11535
12297
|
const user = c.get("user");
|
|
11536
12298
|
const raw = await c.req.json().catch(() => ({}));
|
|
11537
12299
|
const bodyResult = HarvestBodySchema.safeParse(raw);
|
|
@@ -11596,17 +12358,17 @@ app.post("/harvest/sync", auth, async (c) => {
|
|
|
11596
12358
|
return c.json({ job_id: jobId, status: "failed", ...response, attempts: sanitizeAttempts(attempts) }, problem.httpStatus);
|
|
11597
12359
|
}
|
|
11598
12360
|
});
|
|
11599
|
-
app.get("/jobs/:id",
|
|
12361
|
+
app.get("/jobs/:id", auth2, async (c) => {
|
|
11600
12362
|
const job = await getJob(c.req.param("id"), c.get("user").id);
|
|
11601
12363
|
if (!job) return c.json({ error: "Job not found" }, 404);
|
|
11602
12364
|
const attempts = await listHarvestAttempts(job.id, c.get("user").id);
|
|
11603
12365
|
const safeResult = job.result && typeof job.result === "object" ? sanitizeHarvestResult(job.result) : job.result;
|
|
11604
12366
|
return c.json({ ...job, result: safeResult, attempts: sanitizeAttempts(attempts) });
|
|
11605
12367
|
});
|
|
11606
|
-
app.get("/jobs",
|
|
12368
|
+
app.get("/jobs", auth2, async (c) => {
|
|
11607
12369
|
return c.json(await listJobs(c.get("user").id));
|
|
11608
12370
|
});
|
|
11609
|
-
app.get("/history",
|
|
12371
|
+
app.get("/history", auth2, async (c) => {
|
|
11610
12372
|
const userId = c.get("user").id;
|
|
11611
12373
|
const [jobs, events] = await Promise.all([
|
|
11612
12374
|
listJobs(userId),
|
|
@@ -11636,7 +12398,7 @@ app.get("/history", auth, async (c) => {
|
|
|
11636
12398
|
const rows = [...jobRows, ...eventRows].sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
|
|
11637
12399
|
return c.json(rows.slice(0, 100));
|
|
11638
12400
|
});
|
|
11639
|
-
app.get("/ledger",
|
|
12401
|
+
app.get("/ledger", auth2, async (c) => {
|
|
11640
12402
|
return c.json(await getLedger(c.get("user").id, 100));
|
|
11641
12403
|
});
|
|
11642
12404
|
app.post("/admin/users", adminAuth, async (c) => {
|
|
@@ -11674,11 +12436,11 @@ app.post("/admin/backfill-signup-credits", adminAuth, async (c) => {
|
|
|
11674
12436
|
}
|
|
11675
12437
|
return c.json({ processed, credited, skipped, users_credited });
|
|
11676
12438
|
});
|
|
11677
|
-
app.post("/extract-url",
|
|
12439
|
+
app.post("/extract-url", auth2, async (c) => {
|
|
11678
12440
|
const raw = await c.req.json().catch(() => ({}));
|
|
11679
12441
|
const bodyResult = ExtractUrlBodySchema.safeParse(raw);
|
|
11680
12442
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
11681
|
-
const { url, screenshot, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
12443
|
+
const { url, screenshot: screenshot2, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
11682
12444
|
if (!allowLocal) {
|
|
11683
12445
|
const checked = await validatePublicHttpUrl(url, { field: "URL" });
|
|
11684
12446
|
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
@@ -11704,7 +12466,7 @@ app.post("/extract-url", auth, async (c) => {
|
|
|
11704
12466
|
const device = screenshotDevice === "mobile" ? "mobile" : "desktop";
|
|
11705
12467
|
const [result, pageData] = await Promise.all([
|
|
11706
12468
|
extractKpo({ url: canonicalUrl, kernelApiKey }),
|
|
11707
|
-
|
|
12469
|
+
screenshot2 || extractBranding ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding }).catch(() => null) : null
|
|
11708
12470
|
]);
|
|
11709
12471
|
const screenshotBuf = pageData?.screenshot ?? null;
|
|
11710
12472
|
const brandingData = pageData?.branding ?? null;
|
|
@@ -11722,7 +12484,7 @@ app.post("/extract-url", auth, async (c) => {
|
|
|
11722
12484
|
return c.json({ error: msg }, 500);
|
|
11723
12485
|
}
|
|
11724
12486
|
});
|
|
11725
|
-
app.post("/map-urls",
|
|
12487
|
+
app.post("/map-urls", auth2, async (c) => {
|
|
11726
12488
|
const raw = await c.req.json().catch(() => ({}));
|
|
11727
12489
|
const bodyResult = MapUrlsBodySchema.safeParse(raw);
|
|
11728
12490
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -11762,7 +12524,7 @@ app.post("/map-urls", auth, async (c) => {
|
|
|
11762
12524
|
return c.json({ error: msg }, 500);
|
|
11763
12525
|
}
|
|
11764
12526
|
});
|
|
11765
|
-
app.post("/extract-site",
|
|
12527
|
+
app.post("/extract-site", auth2, async (c) => {
|
|
11766
12528
|
const raw = await c.req.json().catch(() => ({}));
|
|
11767
12529
|
const bodyResult = ExtractSiteBodySchema.safeParse(raw);
|
|
11768
12530
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -11884,7 +12646,7 @@ app.post("/billing/concurrency/cancel", requireAllowedOrigin, sessionAuth, async
|
|
|
11884
12646
|
await setConcurrencySubId(user.id, null);
|
|
11885
12647
|
return c.json({ ok: true, concurrency_limit: user.extra_concurrency_slots });
|
|
11886
12648
|
});
|
|
11887
|
-
app.get("/billing/balance",
|
|
12649
|
+
app.get("/billing/balance", auth2, async (c) => {
|
|
11888
12650
|
const user = c.get("user");
|
|
11889
12651
|
const balanceMc = await reconcileBalanceMc(user.id);
|
|
11890
12652
|
const ledger = await getLedger(user.id, 20);
|
|
@@ -11896,7 +12658,7 @@ app.get("/billing/balance", auth, async (c) => {
|
|
|
11896
12658
|
ledger
|
|
11897
12659
|
});
|
|
11898
12660
|
});
|
|
11899
|
-
app.post("/billing/credits",
|
|
12661
|
+
app.post("/billing/credits", auth2, async (c) => {
|
|
11900
12662
|
const user = c.get("user");
|
|
11901
12663
|
const balanceMc = await reconcileBalanceMc(user.id);
|
|
11902
12664
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -11925,7 +12687,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
11925
12687
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
11926
12688
|
return c.json({ error: "Unauthorized" }, 401);
|
|
11927
12689
|
}
|
|
11928
|
-
const { drainQueue } = await import("./worker-
|
|
12690
|
+
const { drainQueue } = await import("./worker-KJ4A7WIR.js");
|
|
11929
12691
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
11930
12692
|
const [results, sweepResult] = await Promise.all([
|
|
11931
12693
|
drainQueue(budget),
|
|
@@ -11941,6 +12703,9 @@ app.route("/facebook", facebookAdApp);
|
|
|
11941
12703
|
app.route("/maps", mapsApp);
|
|
11942
12704
|
app.route("/serp-intelligence", serpIntelligenceApp);
|
|
11943
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"))));
|
|
11944
12709
|
app.route("/stripe", stripeApp);
|
|
11945
12710
|
if (!process.env.INNGEST_EVENT_KEY) {
|
|
11946
12711
|
startSiteAuditWorker();
|
|
@@ -12047,4 +12812,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
12047
12812
|
export {
|
|
12048
12813
|
app
|
|
12049
12814
|
};
|
|
12050
|
-
//# sourceMappingURL=server-
|
|
12815
|
+
//# sourceMappingURL=server-ASCMKUQ5.js.map
|