@melaya/runner 1.1.49 → 1.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistantHost.py +6 -1
- package/dist/connection.js +119 -0
- package/dist/localBacklinkFetch.py +138 -0
- package/localBacklinkFetch.py +138 -0
- package/package.json +3 -2
package/dist/assistantHost.py
CHANGED
|
@@ -170,6 +170,8 @@ def _config_hash() -> str:
|
|
|
170
170
|
pinned_tools = ",".join(sorted(t.lower() for t in raw_pinned.split(",") if t.strip()))
|
|
171
171
|
if pinned_tools:
|
|
172
172
|
fields.append(pinned_tools)
|
|
173
|
+
if os.environ.get("MEL_ASSISTANT_TELEMETRY_SURFACE") == "marketing":
|
|
174
|
+
fields.append("marketing-scoped-tools-v1")
|
|
173
175
|
canon = "|".join(fields)
|
|
174
176
|
return hashlib.sha256(canon.encode("utf-8")).hexdigest()[:32]
|
|
175
177
|
|
|
@@ -607,6 +609,7 @@ def _build_agent():
|
|
|
607
609
|
# names are case-sensitive Python identifiers (unlike connector_services,
|
|
608
610
|
# which are lowercased service ids). Registered ACTIVE at boot below so the
|
|
609
611
|
# model calls them directly instead of search_tools/activate_tool.
|
|
612
|
+
marketing_tool_scope = os.environ.get("MEL_ASSISTANT_TELEMETRY_SURFACE") == "marketing"
|
|
610
613
|
pinned_tool_names = [t.strip() for t in os.environ.get("MEL_ASSISTANT_PINNED_TOOLS", "").split(",") if t.strip()]
|
|
611
614
|
# Self-arm the write-approval gate whenever connectors are active, so it can't
|
|
612
615
|
# be silently OFF on an older runner build that didn't set this env. Writes
|
|
@@ -659,11 +662,12 @@ def _build_agent():
|
|
|
659
662
|
include_categories=categories + core_categories + connector_services,
|
|
660
663
|
budget=_budget,
|
|
661
664
|
pinned_names=pinned_tool_names or None,
|
|
665
|
+
allowed_names=pinned_tool_names if marketing_tool_scope else None,
|
|
662
666
|
)
|
|
663
667
|
except Exception as exc:
|
|
664
668
|
_log(f"toolkit build failed (connectors={connector_services}, core): {exc}; retrying melaya_agent only")
|
|
665
669
|
try:
|
|
666
|
-
toolkit = build_toolkit(categories=["melaya_agent"])
|
|
670
|
+
toolkit = build_toolkit(names=[]) if marketing_tool_scope else build_toolkit(categories=["melaya_agent"])
|
|
667
671
|
except Exception:
|
|
668
672
|
toolkit = build_toolkit(names=[])
|
|
669
673
|
|
|
@@ -1302,6 +1306,7 @@ def main() -> int:
|
|
|
1302
1306
|
except Exception:
|
|
1303
1307
|
_log("restore failed:\n" + traceback.format_exc())
|
|
1304
1308
|
continue
|
|
1309
|
+
os.environ["MEL_APPROVAL_CONVERSATION_ID"] = str(req.get("conversation_id") or "")
|
|
1305
1310
|
turn_id = str(req.get("turnId") or "")
|
|
1306
1311
|
message = str(req.get("message") or "")
|
|
1307
1312
|
# Vision: base64 image blocks for THIS turn (validated server-side). Used
|
package/dist/connection.js
CHANGED
|
@@ -1884,6 +1884,7 @@ export async function connect(opts) {
|
|
|
1884
1884
|
message: payload.message,
|
|
1885
1885
|
hitl_mode: hitlMode,
|
|
1886
1886
|
static_context: typeof payload.staticContext === "string" ? payload.staticContext : "",
|
|
1887
|
+
conversation_id: String(payload.sessionId || "").split(":").slice(1).join(":"),
|
|
1887
1888
|
telemetry_surface: typeof payload.telemetrySurface === "string" ? payload.telemetrySurface : "assistant",
|
|
1888
1889
|
// Per-turn browser grant: forwarded to the host as the raw compact
|
|
1889
1890
|
// JWS string so the Python browser toolkit can verify it again
|
|
@@ -2473,6 +2474,124 @@ export async function connect(opts) {
|
|
|
2473
2474
|
reply(false, undefined, e?.message || String(e));
|
|
2474
2475
|
}
|
|
2475
2476
|
});
|
|
2477
|
+
// ── Residential-IP page fetch (Melaya backlink presence check) ──────────────
|
|
2478
|
+
// The Melaya cloud runs from a datacenter IP that Cloudflare "managed" Turnstile
|
|
2479
|
+
// forces into an interactive challenge no headless engine clears. THIS machine
|
|
2480
|
+
// is on a residential IP, where the same real-browser fetch passes in seconds.
|
|
2481
|
+
// The server hands us ONE public https url; we fetch it with the local Scrapling
|
|
2482
|
+
// ladder (localBacklinkFetch.py) and return the HTML for the server's presence
|
|
2483
|
+
// scan. We receive only the url — no secrets, no internal hosts, no catalog data.
|
|
2484
|
+
socket.on("runner:backlink-fetch", async (payload, ack) => {
|
|
2485
|
+
const rid = payload?.requestId;
|
|
2486
|
+
if (!rid)
|
|
2487
|
+
return;
|
|
2488
|
+
ack?.({ ok: true });
|
|
2489
|
+
const reply = (r) => socket.emit("runner:backlink-fetch-result", {
|
|
2490
|
+
requestId: rid,
|
|
2491
|
+
ok: !r.error && !!r.html && !r.challenged,
|
|
2492
|
+
status: r.status ?? 0,
|
|
2493
|
+
engine: r.engine ?? null,
|
|
2494
|
+
challenged: !!r.challenged,
|
|
2495
|
+
html: r.html ?? "",
|
|
2496
|
+
finalUrl: r.finalUrl ?? payload.url,
|
|
2497
|
+
error: r.error,
|
|
2498
|
+
});
|
|
2499
|
+
// HTTPS-only, public url — defence in depth (the server already validated it).
|
|
2500
|
+
let u = null;
|
|
2501
|
+
try {
|
|
2502
|
+
u = new URL(payload.url);
|
|
2503
|
+
}
|
|
2504
|
+
catch {
|
|
2505
|
+
u = null;
|
|
2506
|
+
}
|
|
2507
|
+
if (!u || u.protocol !== "https:") {
|
|
2508
|
+
reply({ error: "invalid_url" });
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
try {
|
|
2512
|
+
const { ensurePythonEnv } = await import("./pythonEnv.js");
|
|
2513
|
+
const { getLocalSharedVersion } = await import("./sharedVendor.js");
|
|
2514
|
+
const localVersion = getLocalSharedVersion();
|
|
2515
|
+
if (!localVersion) {
|
|
2516
|
+
reply({ error: "runner python runtime still setting up" });
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const env = await ensurePythonEnv(opts.pythonPath, localVersion, (m) => { if (opts.verbose)
|
|
2520
|
+
console.log(chalk.gray(` [backlink-fetch venv] ${m}`)); });
|
|
2521
|
+
if (!env.ok) {
|
|
2522
|
+
reply({ error: `venv bootstrap failed: ${env.reason}` });
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
const { existsSync, copyFileSync, mkdirSync } = await import("fs");
|
|
2526
|
+
const workDir = join(tmpdir(), `melaya-blf-${Date.now()}`);
|
|
2527
|
+
mkdirSync(workDir, { recursive: true });
|
|
2528
|
+
const candidates = [
|
|
2529
|
+
join(__dirname, "localBacklinkFetch.py"),
|
|
2530
|
+
join(__dirname, "..", "localBacklinkFetch.py"),
|
|
2531
|
+
];
|
|
2532
|
+
let found = "";
|
|
2533
|
+
for (const c of candidates) {
|
|
2534
|
+
if (existsSync(c)) {
|
|
2535
|
+
found = c;
|
|
2536
|
+
break;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
if (!found) {
|
|
2540
|
+
reply({ error: "localBacklinkFetch.py not found — update @melaya/runner" });
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const staged = join(workDir, "localBacklinkFetch.py");
|
|
2544
|
+
copyFileSync(found, staged);
|
|
2545
|
+
const certBundle = (await import("./pythonEnv.js")).getCertBundlePath();
|
|
2546
|
+
const sslEnv = certBundle
|
|
2547
|
+
? { SSL_CERT_FILE: certBundle, REQUESTS_CA_BUNDLE: certBundle } : {};
|
|
2548
|
+
const budget = Math.max(6, Math.min(75, Math.floor(payload.budgetSeconds || 40)));
|
|
2549
|
+
const proc = spawn(env.pythonPath, ["-u", staged], {
|
|
2550
|
+
env: { ...process.env, ...sslEnv },
|
|
2551
|
+
cwd: workDir,
|
|
2552
|
+
});
|
|
2553
|
+
let stdout = "";
|
|
2554
|
+
let stderr = "";
|
|
2555
|
+
let done = false;
|
|
2556
|
+
const finish = (r) => { if (done)
|
|
2557
|
+
return; done = true; reply(r); };
|
|
2558
|
+
// Hard wall-clock cap slightly above the fetch budget so a wedged browser
|
|
2559
|
+
// can't hold the session; the server has its own (budget+8s) timeout too.
|
|
2560
|
+
const killTimer = setTimeout(() => {
|
|
2561
|
+
try {
|
|
2562
|
+
proc.kill();
|
|
2563
|
+
}
|
|
2564
|
+
catch { /* already gone */ }
|
|
2565
|
+
finish({ error: "fetch timed out" });
|
|
2566
|
+
}, (budget + 10) * 1000);
|
|
2567
|
+
proc.stdout.on("data", (d) => { stdout += d.toString("utf-8"); });
|
|
2568
|
+
proc.stderr.on("data", (d) => { stderr += d.toString("utf-8"); });
|
|
2569
|
+
proc.on("error", (err) => { clearTimeout(killTimer); finish({ error: err?.message || String(err) }); });
|
|
2570
|
+
proc.on("close", (code) => {
|
|
2571
|
+
clearTimeout(killTimer);
|
|
2572
|
+
const jsonLine = stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean).reverse()[0] || "";
|
|
2573
|
+
let parsed = null;
|
|
2574
|
+
try {
|
|
2575
|
+
parsed = JSON.parse(jsonLine);
|
|
2576
|
+
}
|
|
2577
|
+
catch { /* handled below */ }
|
|
2578
|
+
if (parsed && typeof parsed === "object") {
|
|
2579
|
+
finish({ status: parsed.status, engine: parsed.engine, challenged: !!parsed.challenged, html: parsed.html || "", finalUrl: parsed.finalUrl });
|
|
2580
|
+
}
|
|
2581
|
+
else {
|
|
2582
|
+
finish({ error: `fetch exited ${code ?? "?"}${stderr ? `: ${stderr.slice(-200)}` : ""}` });
|
|
2583
|
+
}
|
|
2584
|
+
});
|
|
2585
|
+
try {
|
|
2586
|
+
proc.stdin.write(JSON.stringify({ url: u.href, budget_seconds: budget }));
|
|
2587
|
+
proc.stdin.end();
|
|
2588
|
+
}
|
|
2589
|
+
catch { /* proc.on('error') handles it */ }
|
|
2590
|
+
}
|
|
2591
|
+
catch (e) {
|
|
2592
|
+
reply({ error: e?.message || String(e) });
|
|
2593
|
+
}
|
|
2594
|
+
});
|
|
2476
2595
|
socket.on("linkedin:start-login", async (req) => {
|
|
2477
2596
|
const sid = req?.session_id;
|
|
2478
2597
|
if (!sid)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Residential-IP page fetch for the Melaya backlink presence check.
|
|
3
|
+
|
|
4
|
+
The Melaya cloud runs from a datacenter IP that Cloudflare "managed" Turnstile
|
|
5
|
+
forces into an interactive challenge no headless engine clears. The user's
|
|
6
|
+
runner is on a RESIDENTIAL IP, where the same real-browser fetch passes in
|
|
7
|
+
seconds. So the server hands us ONE public https url; we fetch it with the
|
|
8
|
+
local Scrapling stack and print the result as a single JSON line on stdout:
|
|
9
|
+
|
|
10
|
+
{"status": int, "engine": str|null, "challenged": bool,
|
|
11
|
+
"finalUrl": str, "html": str} # html capped
|
|
12
|
+
|
|
13
|
+
Input (stdin, JSON): {"url": "...", "budget_seconds": 40}
|
|
14
|
+
|
|
15
|
+
Self-contained on purpose (mirrors localDbProbe.py): it imports only Scrapling
|
|
16
|
+
from the runner venv, never the shared tool bundle. HTTPS-only; the server
|
|
17
|
+
already validated the url, this is defence in depth. Never fetches anything but
|
|
18
|
+
the url it is given — no internal hosts, no secrets, no catalog data.
|
|
19
|
+
"""
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
_HTML_CAP = 600_000
|
|
25
|
+
_CHALLENGE_MARKERS = (
|
|
26
|
+
"just a moment", "cf_chl", "cf-challenge", "checking your browser",
|
|
27
|
+
"enable javascript and cookies", "attention required",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _html_of(page) -> str:
|
|
32
|
+
for attr in ("html_content", "body", "content"):
|
|
33
|
+
try:
|
|
34
|
+
v = getattr(page, attr)
|
|
35
|
+
if callable(v):
|
|
36
|
+
v = v()
|
|
37
|
+
if v:
|
|
38
|
+
return v if isinstance(v, str) else str(v)
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
try:
|
|
42
|
+
return str(page)
|
|
43
|
+
except Exception:
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _challenged(h: str) -> bool:
|
|
48
|
+
h = (h or "").lower()
|
|
49
|
+
return any(m in h for m in _CHALLENGE_MARKERS)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _fetch(url: str, budget: float) -> dict:
|
|
53
|
+
ms = int(max(6.0, min(75.0, budget)) * 1000)
|
|
54
|
+
# ── Tier 1: TLS-impersonating async GET (no browser) ──
|
|
55
|
+
try:
|
|
56
|
+
from scrapling.fetchers import AsyncFetcher
|
|
57
|
+
p = await AsyncFetcher.get(url, timeout=min(ms, 15000), impersonate="chrome",
|
|
58
|
+
stealthy_headers=True, follow_redirects=True)
|
|
59
|
+
st = int(getattr(p, "status", 0) or 0)
|
|
60
|
+
h = _html_of(p)
|
|
61
|
+
final = str(getattr(p, "url", "") or url)
|
|
62
|
+
if 200 <= st < 300 and h and not _challenged(h):
|
|
63
|
+
return {"status": st, "engine": "fast", "challenged": False,
|
|
64
|
+
"finalUrl": final, "html": h[:_HTML_CAP]}
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# ── Tier 2/3: stealth browser (real Chrome), plain then solve_cloudflare ──
|
|
69
|
+
try:
|
|
70
|
+
from scrapling.fetchers import StealthyFetcher
|
|
71
|
+
except Exception:
|
|
72
|
+
return {"status": 0, "engine": None, "challenged": False, "finalUrl": url, "html": ""}
|
|
73
|
+
fn = getattr(StealthyFetcher, "async_fetch", None)
|
|
74
|
+
if fn is None:
|
|
75
|
+
return {"status": 0, "engine": None, "challenged": False, "finalUrl": url, "html": ""}
|
|
76
|
+
|
|
77
|
+
last = {"status": 0, "engine": "stealth", "challenged": True, "finalUrl": url, "html": ""}
|
|
78
|
+
for solve in (False, True):
|
|
79
|
+
base = {"headless": True, "timeout": 60000 if solve else min(ms, 25000)}
|
|
80
|
+
if solve:
|
|
81
|
+
base.update(solve_cloudflare=True, block_webrtc=True, disable_resources=False)
|
|
82
|
+
page = None
|
|
83
|
+
engine = ("real_chrome_cf" if solve else "real_chrome")
|
|
84
|
+
for use_real in (True, False):
|
|
85
|
+
kw = dict(base)
|
|
86
|
+
if use_real:
|
|
87
|
+
kw["real_chrome"] = True
|
|
88
|
+
else:
|
|
89
|
+
engine = ("chromium_cf" if solve else "chromium")
|
|
90
|
+
try:
|
|
91
|
+
page = await fn(url, **kw)
|
|
92
|
+
break
|
|
93
|
+
except Exception:
|
|
94
|
+
page = None
|
|
95
|
+
continue
|
|
96
|
+
if page is None:
|
|
97
|
+
continue
|
|
98
|
+
h = _html_of(page)
|
|
99
|
+
st = int(getattr(page, "status", 0) or 0)
|
|
100
|
+
final = str(getattr(page, "url", "") or url)
|
|
101
|
+
last = {"status": st, "engine": engine, "challenged": _challenged(h),
|
|
102
|
+
"finalUrl": final, "html": (h or "")[:_HTML_CAP]}
|
|
103
|
+
if h and not _challenged(h):
|
|
104
|
+
return last
|
|
105
|
+
if not solve and not _challenged(h):
|
|
106
|
+
break # plain stealth returned something non-challenge-y but empty
|
|
107
|
+
return last
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _main() -> None:
|
|
111
|
+
try:
|
|
112
|
+
data = json.loads(sys.stdin.read() or "{}")
|
|
113
|
+
except Exception:
|
|
114
|
+
data = {}
|
|
115
|
+
url = str(data.get("url") or "").strip()
|
|
116
|
+
try:
|
|
117
|
+
budget = float(data.get("budget_seconds") or 40)
|
|
118
|
+
except Exception:
|
|
119
|
+
budget = 40.0
|
|
120
|
+
from urllib.parse import urlsplit
|
|
121
|
+
if not url or urlsplit(url).scheme != "https":
|
|
122
|
+
print(json.dumps({"status": 0, "engine": None, "challenged": False,
|
|
123
|
+
"finalUrl": url, "html": "", "error": "https_only"}))
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
out = await _fetch(url, budget)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
out = {"status": 0, "engine": None, "challenged": False,
|
|
129
|
+
"finalUrl": url, "html": "", "error": str(e)[:200]}
|
|
130
|
+
print(json.dumps(out))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if sys.platform.startswith("win"):
|
|
134
|
+
try:
|
|
135
|
+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
asyncio.run(_main())
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Residential-IP page fetch for the Melaya backlink presence check.
|
|
3
|
+
|
|
4
|
+
The Melaya cloud runs from a datacenter IP that Cloudflare "managed" Turnstile
|
|
5
|
+
forces into an interactive challenge no headless engine clears. The user's
|
|
6
|
+
runner is on a RESIDENTIAL IP, where the same real-browser fetch passes in
|
|
7
|
+
seconds. So the server hands us ONE public https url; we fetch it with the
|
|
8
|
+
local Scrapling stack and print the result as a single JSON line on stdout:
|
|
9
|
+
|
|
10
|
+
{"status": int, "engine": str|null, "challenged": bool,
|
|
11
|
+
"finalUrl": str, "html": str} # html capped
|
|
12
|
+
|
|
13
|
+
Input (stdin, JSON): {"url": "...", "budget_seconds": 40}
|
|
14
|
+
|
|
15
|
+
Self-contained on purpose (mirrors localDbProbe.py): it imports only Scrapling
|
|
16
|
+
from the runner venv, never the shared tool bundle. HTTPS-only; the server
|
|
17
|
+
already validated the url, this is defence in depth. Never fetches anything but
|
|
18
|
+
the url it is given — no internal hosts, no secrets, no catalog data.
|
|
19
|
+
"""
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
_HTML_CAP = 600_000
|
|
25
|
+
_CHALLENGE_MARKERS = (
|
|
26
|
+
"just a moment", "cf_chl", "cf-challenge", "checking your browser",
|
|
27
|
+
"enable javascript and cookies", "attention required",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _html_of(page) -> str:
|
|
32
|
+
for attr in ("html_content", "body", "content"):
|
|
33
|
+
try:
|
|
34
|
+
v = getattr(page, attr)
|
|
35
|
+
if callable(v):
|
|
36
|
+
v = v()
|
|
37
|
+
if v:
|
|
38
|
+
return v if isinstance(v, str) else str(v)
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
try:
|
|
42
|
+
return str(page)
|
|
43
|
+
except Exception:
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _challenged(h: str) -> bool:
|
|
48
|
+
h = (h or "").lower()
|
|
49
|
+
return any(m in h for m in _CHALLENGE_MARKERS)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _fetch(url: str, budget: float) -> dict:
|
|
53
|
+
ms = int(max(6.0, min(75.0, budget)) * 1000)
|
|
54
|
+
# ── Tier 1: TLS-impersonating async GET (no browser) ──
|
|
55
|
+
try:
|
|
56
|
+
from scrapling.fetchers import AsyncFetcher
|
|
57
|
+
p = await AsyncFetcher.get(url, timeout=min(ms, 15000), impersonate="chrome",
|
|
58
|
+
stealthy_headers=True, follow_redirects=True)
|
|
59
|
+
st = int(getattr(p, "status", 0) or 0)
|
|
60
|
+
h = _html_of(p)
|
|
61
|
+
final = str(getattr(p, "url", "") or url)
|
|
62
|
+
if 200 <= st < 300 and h and not _challenged(h):
|
|
63
|
+
return {"status": st, "engine": "fast", "challenged": False,
|
|
64
|
+
"finalUrl": final, "html": h[:_HTML_CAP]}
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# ── Tier 2/3: stealth browser (real Chrome), plain then solve_cloudflare ──
|
|
69
|
+
try:
|
|
70
|
+
from scrapling.fetchers import StealthyFetcher
|
|
71
|
+
except Exception:
|
|
72
|
+
return {"status": 0, "engine": None, "challenged": False, "finalUrl": url, "html": ""}
|
|
73
|
+
fn = getattr(StealthyFetcher, "async_fetch", None)
|
|
74
|
+
if fn is None:
|
|
75
|
+
return {"status": 0, "engine": None, "challenged": False, "finalUrl": url, "html": ""}
|
|
76
|
+
|
|
77
|
+
last = {"status": 0, "engine": "stealth", "challenged": True, "finalUrl": url, "html": ""}
|
|
78
|
+
for solve in (False, True):
|
|
79
|
+
base = {"headless": True, "timeout": 60000 if solve else min(ms, 25000)}
|
|
80
|
+
if solve:
|
|
81
|
+
base.update(solve_cloudflare=True, block_webrtc=True, disable_resources=False)
|
|
82
|
+
page = None
|
|
83
|
+
engine = ("real_chrome_cf" if solve else "real_chrome")
|
|
84
|
+
for use_real in (True, False):
|
|
85
|
+
kw = dict(base)
|
|
86
|
+
if use_real:
|
|
87
|
+
kw["real_chrome"] = True
|
|
88
|
+
else:
|
|
89
|
+
engine = ("chromium_cf" if solve else "chromium")
|
|
90
|
+
try:
|
|
91
|
+
page = await fn(url, **kw)
|
|
92
|
+
break
|
|
93
|
+
except Exception:
|
|
94
|
+
page = None
|
|
95
|
+
continue
|
|
96
|
+
if page is None:
|
|
97
|
+
continue
|
|
98
|
+
h = _html_of(page)
|
|
99
|
+
st = int(getattr(page, "status", 0) or 0)
|
|
100
|
+
final = str(getattr(page, "url", "") or url)
|
|
101
|
+
last = {"status": st, "engine": engine, "challenged": _challenged(h),
|
|
102
|
+
"finalUrl": final, "html": (h or "")[:_HTML_CAP]}
|
|
103
|
+
if h and not _challenged(h):
|
|
104
|
+
return last
|
|
105
|
+
if not solve and not _challenged(h):
|
|
106
|
+
break # plain stealth returned something non-challenge-y but empty
|
|
107
|
+
return last
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _main() -> None:
|
|
111
|
+
try:
|
|
112
|
+
data = json.loads(sys.stdin.read() or "{}")
|
|
113
|
+
except Exception:
|
|
114
|
+
data = {}
|
|
115
|
+
url = str(data.get("url") or "").strip()
|
|
116
|
+
try:
|
|
117
|
+
budget = float(data.get("budget_seconds") or 40)
|
|
118
|
+
except Exception:
|
|
119
|
+
budget = 40.0
|
|
120
|
+
from urllib.parse import urlsplit
|
|
121
|
+
if not url or urlsplit(url).scheme != "https":
|
|
122
|
+
print(json.dumps({"status": 0, "engine": None, "challenged": False,
|
|
123
|
+
"finalUrl": url, "html": "", "error": "https_only"}))
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
out = await _fetch(url, budget)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
out = {"status": 0, "engine": None, "challenged": False,
|
|
129
|
+
"finalUrl": url, "html": "", "error": str(e)[:200]}
|
|
130
|
+
print(json.dumps(out))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if sys.platform.startswith("win"):
|
|
134
|
+
try:
|
|
135
|
+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
asyncio.run(_main())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melaya/runner",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.50",
|
|
4
4
|
"description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
"localRagIngest.py",
|
|
17
17
|
"localRagRetrieve.py",
|
|
18
18
|
"localDbProbe.py",
|
|
19
|
+
"localBacklinkFetch.py",
|
|
19
20
|
"nltk_data/**",
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
|
-
"build": "tsc && node -e \"const fs=require('fs'); fs.copyFileSync('localRagIngest.py','dist/localRagIngest.py'); fs.copyFileSync('localRagRetrieve.py','dist/localRagRetrieve.py'); fs.copyFileSync('localDbProbe.py','dist/localDbProbe.py'); fs.copyFileSync('src/assistantHost.py','dist/assistantHost.py')\"",
|
|
24
|
+
"build": "tsc && node -e \"const fs=require('fs'); fs.copyFileSync('localRagIngest.py','dist/localRagIngest.py'); fs.copyFileSync('localRagRetrieve.py','dist/localRagRetrieve.py'); fs.copyFileSync('localDbProbe.py','dist/localDbProbe.py'); fs.copyFileSync('localBacklinkFetch.py','dist/localBacklinkFetch.py'); fs.copyFileSync('src/assistantHost.py','dist/assistantHost.py')\"",
|
|
24
25
|
"test": "node --test --import tsx \"src/**/*.test.ts\"",
|
|
25
26
|
"prepublishOnly": "npm run build"
|
|
26
27
|
},
|