+29
-4
@@ -11,6 +11,9 @@ const CHAR_PER_TOKEN = 3;
|
||||
const THRESHOLD = 0.68;
|
||||
const MIN_SUMMARY = 80;
|
||||
const MIN_COMPACT_MESSAGES = 4;
|
||||
// Spoken sessions only need the last few exchanges. Prefill cost tracks the
|
||||
// prompt, so a 32k model must not keep a 20k-token voice transcript.
|
||||
const VOICE_KEEP_TOKENS = 1024;
|
||||
const COMPACT_PROMPT =
|
||||
'Summarize this coding-agent conversation. Use exactly these sections:\n' +
|
||||
'1. Goal\n' +
|
||||
@@ -75,19 +78,38 @@ function estimateTokens(messages, tools) {
|
||||
return conversationTokens(messages) + toolTokens(tools);
|
||||
}
|
||||
|
||||
function historyBudget(ctxSize, tools, attempt, extraTokens = 0) {
|
||||
function dialogTokens(messages) {
|
||||
return conversationTokens((messages || []).filter((m) => m && m.role !== 'system'));
|
||||
}
|
||||
|
||||
function voiceKeepTokens(ctxSize) {
|
||||
const cap = ctxSize > 0 ? Number(ctxSize) : 8192;
|
||||
return Math.min(VOICE_KEEP_TOKENS, Math.max(480, Math.floor(cap * 0.28)));
|
||||
}
|
||||
|
||||
function historyBudget(ctxSize, tools, attempt, extraTokens = 0, opts = {}) {
|
||||
const cap = ctxSize > 0 ? Number(ctxSize) : 8192;
|
||||
const toolTok = toolTokens(tools);
|
||||
const reserve = Math.max(384, Math.floor(cap * (0.18 + (Number(attempt) || 0) * 0.08)));
|
||||
return Math.max(240, Math.floor(cap * 0.72) - toolTok - reserve - extraTokens);
|
||||
let budget = Math.max(240, Math.floor(cap * 0.72) - toolTok - reserve - extraTokens);
|
||||
if (opts && opts.voice) {
|
||||
const sysTok = Math.max(0, Number(opts.systemTokens) || 0);
|
||||
budget = Math.min(budget, Math.max(240, sysTok + voiceKeepTokens(cap) - extraTokens));
|
||||
}
|
||||
return Math.max(240, budget);
|
||||
}
|
||||
|
||||
function shouldCompact(messages, tools, ctxSize, extraTokens = 0) {
|
||||
function shouldCompact(messages, tools, ctxSize, extraTokens = 0, opts = {}) {
|
||||
const dialog = dialogTokens(messages);
|
||||
const enough = nonSystemCount(messages) >= MIN_COMPACT_MESSAGES || dialog > 1024;
|
||||
if (opts && opts.voice) {
|
||||
return dialog > voiceKeepTokens(ctxSize) && enough;
|
||||
}
|
||||
const budget = historyBudget(ctxSize, tools, 0, extraTokens);
|
||||
const tokens = conversationTokens(messages);
|
||||
// Large first requests or tool results can overflow before four messages.
|
||||
// A small greeting must not be discarded merely because schemas are large.
|
||||
return tokens > budget && (nonSystemCount(messages) >= MIN_COMPACT_MESSAGES || conversationTokens((messages || []).filter((m) => m.role !== 'system')) > 1024);
|
||||
return tokens > budget && enough;
|
||||
}
|
||||
|
||||
function isOverflowError(err) {
|
||||
@@ -329,11 +351,14 @@ module.exports = {
|
||||
THRESHOLD,
|
||||
MIN_SUMMARY,
|
||||
MIN_COMPACT_MESSAGES,
|
||||
VOICE_KEEP_TOKENS,
|
||||
COMPACT_PROMPT,
|
||||
VOICE_COMPACT_PROMPT,
|
||||
conversationTokens,
|
||||
dialogTokens,
|
||||
toolTokens,
|
||||
estimateTokens,
|
||||
voiceKeepTokens,
|
||||
historyBudget,
|
||||
shouldCompact,
|
||||
nonSystemCount,
|
||||
|
||||
Vendored
+27
-11
@@ -72,6 +72,22 @@ function loadedCtxSize() {
|
||||
return (loaded && loaded.ctxSize) || 8192;
|
||||
}
|
||||
|
||||
function systemTokens(history) {
|
||||
if (history && history[0] && history[0].role === 'system') return compaction.conversationTokens([history[0]]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function historyCompactOpts(session, toolDefs, ctxSize, sidecarTokens, budget, attempt) {
|
||||
return {
|
||||
budgetTokens: compaction.historyBudget(ctxSize, toolDefs, attempt || 0, sidecarTokens, {
|
||||
voice: !!budget.voice,
|
||||
systemTokens: systemTokens(session.history),
|
||||
}),
|
||||
tools: toolDefs,
|
||||
voice: !!budget.voice,
|
||||
};
|
||||
}
|
||||
|
||||
function toolResultCap(budget) {
|
||||
const ctx = loadedCtxSize();
|
||||
const voice = !!(budget && budget.voice);
|
||||
@@ -608,22 +624,20 @@ async function runTurn(ctx) {
|
||||
const sidecarTokens = compaction.conversationTokens(turnSidecars) + 256;
|
||||
const beforeUsage = compaction.usage(session.history, toolDefs, ctxSize);
|
||||
emitLive(emit, session.id, jobId, Object.assign({ type: 'context' }, beforeUsage));
|
||||
if (compaction.shouldCompact(session.history, toolDefs, ctxSize, sidecarTokens)) {
|
||||
if (compaction.shouldCompact(session.history, toolDefs, ctxSize, sidecarTokens, { voice: !!budget.voice })) {
|
||||
emitUpdate(emit, session.id, jobId, {
|
||||
type: 'compaction',
|
||||
status: 'start',
|
||||
method: 'llm',
|
||||
method: budget.voice ? 'heuristic' : 'llm',
|
||||
used: beforeUsage.used,
|
||||
limit: beforeUsage.limit,
|
||||
pct: beforeUsage.pct,
|
||||
threshold: beforeUsage.threshold,
|
||||
});
|
||||
const compactOpts = {
|
||||
budgetTokens: compaction.historyBudget(ctxSize, toolDefs, 0, sidecarTokens),
|
||||
tools: toolDefs,
|
||||
voice: !!budget.voice,
|
||||
};
|
||||
session.history = await compaction.compactWithLlm(session.history, Object.assign({}, compactOpts, {
|
||||
const compactOpts = historyCompactOpts(session, toolDefs, ctxSize, sidecarTokens, budget, 0);
|
||||
session.history = budget.voice
|
||||
? compaction.compact(session.history, compactOpts)
|
||||
: await compaction.compactWithLlm(session.history, Object.assign({}, compactOpts, {
|
||||
complete: (opts) => engine.complete(Object.assign({}, opts, {
|
||||
desktopVision: false,
|
||||
timeoutMs: budget.completeTimeoutMs,
|
||||
@@ -643,7 +657,7 @@ async function runTurn(ctx) {
|
||||
emitUpdate(emit, session.id, jobId, {
|
||||
type: 'compaction',
|
||||
status: 'done',
|
||||
method: 'llm',
|
||||
method: budget.voice ? 'heuristic' : 'llm',
|
||||
used: afterUsage.used,
|
||||
limit: afterUsage.limit,
|
||||
pct: afterUsage.pct,
|
||||
@@ -715,7 +729,7 @@ async function runTurn(ctx) {
|
||||
// The estimate can be lower than the model's tokenizer count.
|
||||
// Every overflow retry must shrink even an apparently small history.
|
||||
budgetTokens: Math.min(
|
||||
compaction.historyBudget(ctxSize, toolDefs, overflowTry + 1, sidecarTokens),
|
||||
historyCompactOpts(session, toolDefs, ctxSize, sidecarTokens, budget, overflowTry + 1).budgetTokens,
|
||||
Math.max(1, Math.floor(compaction.conversationTokens(session.history) * 0.85))
|
||||
),
|
||||
tools: toolDefs,
|
||||
@@ -863,7 +877,9 @@ async function runTurn(ctx) {
|
||||
}
|
||||
if (stopEarly) break;
|
||||
}
|
||||
if (prepared.length) toolBudget.markToolRound(budget);
|
||||
if (prepared.some((item) => item.name !== 'todo_write' && item.name !== 'update_goal')) {
|
||||
toolBudget.markToolRound(budget);
|
||||
}
|
||||
if (stopEarly) {
|
||||
return endTurn(emit, session, jobId, tracker, stopEarly);
|
||||
}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ function fromPayload(payload, origin) {
|
||||
voice,
|
||||
maxTurns: num(payload.maxTurns, voice ? 6 : 24),
|
||||
maxShellCalls: unlimitedShell ? 0 : num(payload.maxShellCalls, voice ? 1 : 0),
|
||||
maxToolRounds: num(payload.maxToolRounds, voice ? 4 : 0),
|
||||
maxToolRounds: num(payload.maxToolRounds, voice ? 6 : 0),
|
||||
completeTimeoutMs: voice ? 45000 : 0,
|
||||
completeIdleMs: voice ? 10000 : 0,
|
||||
shellCalls: 0,
|
||||
|
||||
+161
-68
@@ -7,8 +7,10 @@
|
||||
const net = require('../lib/net.js');
|
||||
const truncate = require('./truncate.js');
|
||||
|
||||
const WEB_TIMEOUT_MS = 12000;
|
||||
const PAGE_TIMEOUT_MS = 20000;
|
||||
const WEB_TIMEOUT_MS = 3500;
|
||||
const PAGE_TIMEOUT_MS = 8000;
|
||||
const SEARCH_BUDGET_MS = 8000;
|
||||
const ENGINE_TIMEOUT_MS = 3000;
|
||||
const BROWSER_UA =
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||
const GOOGLE_UA =
|
||||
@@ -34,14 +36,14 @@ const ENGINE_NAMES = [
|
||||
];
|
||||
|
||||
const AUTO_ENGINES = [
|
||||
'duckduckgo',
|
||||
'ddg_lite',
|
||||
'jina',
|
||||
'bing',
|
||||
'bing_rss',
|
||||
'google',
|
||||
'wikipedia',
|
||||
'ddg_instant',
|
||||
'ddg_lite',
|
||||
'bing_rss',
|
||||
'duckduckgo',
|
||||
'bing',
|
||||
'google',
|
||||
];
|
||||
|
||||
const ENGINE_ALIASES = {
|
||||
@@ -61,13 +63,64 @@ function abortError(timeoutMs) {
|
||||
return err;
|
||||
}
|
||||
|
||||
function remainingMs(deadline) {
|
||||
return Math.max(0, Number(deadline) - Date.now());
|
||||
}
|
||||
|
||||
function timeoutErrorResult(ms, extra) {
|
||||
return Object.assign({ error: 'timed out after ' + ms + 'ms' }, extra || {});
|
||||
}
|
||||
|
||||
function budgetMs(timeoutMs, fallback, max) {
|
||||
const fallbackMs = Number(fallback) > 0 ? Number(fallback) : SEARCH_BUDGET_MS;
|
||||
const cap = Number(max) > 0 ? Number(max) : fallbackMs;
|
||||
const n = Number(timeoutMs);
|
||||
if (!(n > 0)) return fallbackMs;
|
||||
return n > cap ? cap : n;
|
||||
}
|
||||
|
||||
function withDeadline(work, deadline, fallback) {
|
||||
const left = remainingMs(deadline);
|
||||
if (left <= 0) return Promise.resolve(typeof fallback === 'function' ? fallback() : fallback);
|
||||
let timer;
|
||||
const timeout = new Promise((resolve) => {
|
||||
timer = setTimeout(() => resolve(typeof fallback === 'function' ? fallback() : fallback), left);
|
||||
});
|
||||
return Promise.race([Promise.resolve().then(work), timeout]).finally(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
|
||||
function linkAbort(parent, child) {
|
||||
if (!parent || !child) return;
|
||||
if (parent.aborted) {
|
||||
try {
|
||||
child.abort();
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
parent.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
try {
|
||||
child.abort();
|
||||
} catch (_) {}
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
|
||||
function fetchWithTimeout(url, opts, timeoutMs) {
|
||||
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
|
||||
if (!(ms > 0)) return Promise.reject(abortError(0));
|
||||
const headers = Object.assign({ 'user-agent': BROWSER_UA }, (opts && opts.headers) || {});
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||||
let timer;
|
||||
const init = Object.assign({}, opts || {}, { headers });
|
||||
if (controller) init.signal = controller.signal;
|
||||
if (controller) {
|
||||
init.signal = controller.signal;
|
||||
if (opts && opts.signal) linkAbort(opts.signal, controller);
|
||||
}
|
||||
const timeout = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
try {
|
||||
@@ -84,7 +137,8 @@ function fetchWithTimeout(url, opts, timeoutMs) {
|
||||
}
|
||||
|
||||
function readBodyWithTimeout(res, timeoutMs) {
|
||||
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
|
||||
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : 0;
|
||||
if (!(ms > 0)) return Promise.reject(abortError(0));
|
||||
if (!res || typeof res.text !== 'function') return Promise.resolve('');
|
||||
let timer;
|
||||
const timeout = new Promise((_, reject) => {
|
||||
@@ -238,14 +292,16 @@ function pushHit(hits, seen, href, title, snippet, limit) {
|
||||
}
|
||||
|
||||
async function fetchText(url, timeoutMs, opts) {
|
||||
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
|
||||
const deadline = Date.now() + ms;
|
||||
try {
|
||||
net.assertPublicHttpUrl(url);
|
||||
} catch (err) {
|
||||
return { error: String(err && err.message || err), url };
|
||||
}
|
||||
try {
|
||||
const res = await fetchWithTimeout(url, opts || {}, timeoutMs);
|
||||
const text = await readBodyWithTimeout(res, timeoutMs);
|
||||
const res = await fetchWithTimeout(url, opts || {}, remainingMs(deadline));
|
||||
const text = await readBodyWithTimeout(res, remainingMs(deadline));
|
||||
if (res.status >= 400) {
|
||||
return { error: 'HTTP ' + res.status, url: String(res.url || url), status: res.status, text };
|
||||
}
|
||||
@@ -381,9 +437,11 @@ function isDdgChallenge(html) {
|
||||
}
|
||||
|
||||
async function duckDuckGoSearch(query, timeoutMs, limit) {
|
||||
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
|
||||
const deadline = Date.now() + ms;
|
||||
const url = 'https://html.duckduckgo.com/html/';
|
||||
const body = 'q=' + encodeURIComponent(query) + '&b=&kl=us-en';
|
||||
let page = await fetchText(url, timeoutMs, {
|
||||
let page = await fetchText(url, remainingMs(deadline), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
@@ -397,7 +455,8 @@ async function duckDuckGoSearch(query, timeoutMs, limit) {
|
||||
const posted = parseDdgHtmlHits(page.text, limit);
|
||||
if (searchHasHits(posted)) return posted;
|
||||
}
|
||||
page = await fetchText(url + '?q=' + encodeURIComponent(query), timeoutMs);
|
||||
if (remainingMs(deadline) <= 0) return { error: 'timed out after ' + ms + 'ms', url };
|
||||
page = await fetchText(url + '?q=' + encodeURIComponent(query), remainingMs(deadline));
|
||||
if (page.error) return page;
|
||||
if (isDdgChallenge(page.text)) return { error: 'duckduckgo bot challenge', url: page.url, status: page.status };
|
||||
return parseDdgHtmlHits(page.text, limit);
|
||||
@@ -633,32 +692,48 @@ async function runWebSearch(query, opts) {
|
||||
const q = String(query || '').trim();
|
||||
if (!q) return { error: 'query required' };
|
||||
const limit = clampLimit(opts.limit);
|
||||
const timeoutMs = opts.timeoutMs;
|
||||
const budget = budgetMs(opts.timeoutMs, SEARCH_BUDGET_MS, SEARCH_BUDGET_MS);
|
||||
const deadline = Date.now() + budget;
|
||||
const engine = resolveEngine(opts.engine);
|
||||
if (engine !== 'auto') {
|
||||
const fn = SEARCH_ENGINES[engine];
|
||||
if (!fn) return { error: 'unknown engine', engine: opts.engine, engines: ENGINE_NAMES };
|
||||
const result = await fn(q, timeoutMs, limit);
|
||||
if (searchHasHits(result)) return tagSearchHits(result, engine).slice(0, limit);
|
||||
return {
|
||||
error: (result && result.error) || 'no search results',
|
||||
url: result && result.url,
|
||||
tried: [engine],
|
||||
engines: ENGINE_NAMES,
|
||||
};
|
||||
}
|
||||
const prefer = Array.isArray(opts.prefer) ? opts.prefer.map(resolveEngine).filter((n) => SEARCH_ENGINES[n]) : [];
|
||||
const chain = prefer.concat(AUTO_ENGINES.filter((name) => prefer.indexOf(name) < 0));
|
||||
const tried = [];
|
||||
const errors = {};
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const name = chain[i];
|
||||
tried.push(name);
|
||||
const result = await SEARCH_ENGINES[name](q, timeoutMs, limit);
|
||||
if (searchHasHits(result)) return tagSearchHits(result, name).slice(0, limit);
|
||||
errors[name] = result && result.error ? result.error : 'no results';
|
||||
}
|
||||
return { error: 'no search results', tried, errors, engines: ENGINE_NAMES };
|
||||
const timedOut = () => timeoutErrorResult(budget, {
|
||||
tried: tried.slice(),
|
||||
errors: Object.assign({}, errors),
|
||||
engines: ENGINE_NAMES,
|
||||
});
|
||||
return withDeadline(async () => {
|
||||
try {
|
||||
if (engine !== 'auto') {
|
||||
const fn = SEARCH_ENGINES[engine];
|
||||
if (!fn) return { error: 'unknown engine', engine: opts.engine, engines: ENGINE_NAMES };
|
||||
tried.push(engine);
|
||||
const result = await fn(q, remainingMs(deadline), limit);
|
||||
if (searchHasHits(result)) return tagSearchHits(result, engine).slice(0, limit);
|
||||
return {
|
||||
error: (result && result.error) || 'no search results',
|
||||
url: result && result.url,
|
||||
tried: [engine],
|
||||
engines: ENGINE_NAMES,
|
||||
};
|
||||
}
|
||||
const prefer = Array.isArray(opts.prefer) ? opts.prefer.map(resolveEngine).filter((n) => SEARCH_ENGINES[n]) : [];
|
||||
const chain = prefer.concat(AUTO_ENGINES.filter((name) => prefer.indexOf(name) < 0));
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const left = remainingMs(deadline);
|
||||
if (left <= 0) return timedOut();
|
||||
if (tried.length && left < 50) break;
|
||||
const name = chain[i];
|
||||
tried.push(name);
|
||||
const result = await SEARCH_ENGINES[name](q, Math.min(ENGINE_TIMEOUT_MS, left), limit);
|
||||
if (searchHasHits(result)) return tagSearchHits(result, name).slice(0, limit);
|
||||
errors[name] = result && result.error ? result.error : 'no results';
|
||||
}
|
||||
return { error: 'no search results', tried, errors, engines: ENGINE_NAMES };
|
||||
} catch (err) {
|
||||
return { error: String(err && err.message || err), tried, errors, engines: ENGINE_NAMES };
|
||||
}
|
||||
}, deadline, timedOut);
|
||||
}
|
||||
|
||||
async function googleSearchWithFallback(query, timeoutMs) {
|
||||
@@ -672,22 +747,27 @@ async function webSearch(query, timeoutMs) {
|
||||
async function codeSearch(query, timeoutMs, limit) {
|
||||
const q = String(query || '').trim();
|
||||
if (!q) return { error: 'query required' };
|
||||
const [github, npm, mdn] = await Promise.all([
|
||||
githubSearch(q, timeoutMs, limit),
|
||||
npmSearch(q, timeoutMs, limit),
|
||||
mdnSearch(q, timeoutMs, limit),
|
||||
]);
|
||||
const out = { github: [], npm: [], mdn: [] };
|
||||
if (searchHasHits(github)) out.github = tagSearchHits(github, 'github');
|
||||
else if (github && github.error) out.github_error = github.error;
|
||||
if (searchHasHits(npm)) out.npm = tagSearchHits(npm, 'npm');
|
||||
else if (npm && npm.error) out.npm_error = npm.error;
|
||||
if (searchHasHits(mdn)) out.mdn = tagSearchHits(mdn, 'mdn');
|
||||
else if (mdn && mdn.error) out.mdn_error = mdn.error;
|
||||
if (!out.github.length && !out.npm.length && !out.mdn.length) {
|
||||
return { error: 'no code search results', github_error: out.github_error, npm_error: out.npm_error, mdn_error: out.mdn_error };
|
||||
}
|
||||
return out;
|
||||
const budget = budgetMs(timeoutMs, SEARCH_BUDGET_MS, SEARCH_BUDGET_MS);
|
||||
const deadline = Date.now() + budget;
|
||||
return withDeadline(async () => {
|
||||
const slice = remainingMs(deadline);
|
||||
const [github, npm, mdn] = await Promise.all([
|
||||
githubSearch(q, slice, limit),
|
||||
npmSearch(q, slice, limit),
|
||||
mdnSearch(q, slice, limit),
|
||||
]);
|
||||
const out = { github: [], npm: [], mdn: [] };
|
||||
if (searchHasHits(github)) out.github = tagSearchHits(github, 'github');
|
||||
else if (github && github.error) out.github_error = github.error;
|
||||
if (searchHasHits(npm)) out.npm = tagSearchHits(npm, 'npm');
|
||||
else if (npm && npm.error) out.npm_error = npm.error;
|
||||
if (searchHasHits(mdn)) out.mdn = tagSearchHits(mdn, 'mdn');
|
||||
else if (mdn && mdn.error) out.mdn_error = mdn.error;
|
||||
if (!out.github.length && !out.npm.length && !out.mdn.length) {
|
||||
return { error: 'no code search results', github_error: out.github_error, npm_error: out.npm_error, mdn_error: out.mdn_error };
|
||||
}
|
||||
return out;
|
||||
}, deadline, () => timeoutErrorResult(budget));
|
||||
}
|
||||
|
||||
async function webFetch(url, timeoutMs) {
|
||||
@@ -696,9 +776,11 @@ async function webFetch(url, timeoutMs) {
|
||||
} catch (err) {
|
||||
return { error: String(err && err.message || err), url: String(url || '') };
|
||||
}
|
||||
const ms = budgetMs(timeoutMs, WEB_TIMEOUT_MS, SEARCH_BUDGET_MS);
|
||||
const deadline = Date.now() + ms;
|
||||
try {
|
||||
const res = await fetchWithTimeout(url, {}, timeoutMs);
|
||||
let text = htmlToText(await readBodyWithTimeout(res, timeoutMs));
|
||||
const res = await fetchWithTimeout(url, {}, remainingMs(deadline));
|
||||
let text = htmlToText(await readBodyWithTimeout(res, remainingMs(deadline)));
|
||||
text = truncate.truncateWithMarker(text, 12000);
|
||||
const href = String(res.url || url);
|
||||
if (res.status >= 400) {
|
||||
@@ -718,26 +800,37 @@ async function fetchPage(url, timeoutMs) {
|
||||
}
|
||||
const target = String(url);
|
||||
const jinaUrl = 'https://r.jina.ai/' + target;
|
||||
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : PAGE_TIMEOUT_MS;
|
||||
try {
|
||||
net.assertPublicHttpUrl(jinaUrl);
|
||||
const res = await fetchWithTimeout(jinaUrl, { headers: { accept: 'text/plain', 'user-agent': AGENT_UA } }, ms);
|
||||
if (res.status < 400) {
|
||||
let text = await readBodyWithTimeout(res, ms);
|
||||
text = truncate.truncateWithMarker(text, 12000);
|
||||
if (text && text.length > 24 && !/verifying you are (a )?human/i.test(text)) {
|
||||
return { status: res.status, url: target, text, via: 'jina' };
|
||||
const ms = budgetMs(timeoutMs, PAGE_TIMEOUT_MS, SEARCH_BUDGET_MS);
|
||||
const deadline = Date.now() + ms;
|
||||
const timedOut = () => timeoutErrorResult(ms, { url: target });
|
||||
return withDeadline(async () => {
|
||||
try {
|
||||
net.assertPublicHttpUrl(jinaUrl);
|
||||
const left = remainingMs(deadline);
|
||||
if (left > 0) {
|
||||
const res = await fetchWithTimeout(jinaUrl, { headers: { accept: 'text/plain', 'user-agent': AGENT_UA } }, left);
|
||||
if (res.status < 400) {
|
||||
let text = await readBodyWithTimeout(res, remainingMs(deadline));
|
||||
text = truncate.truncateWithMarker(text, 12000);
|
||||
if (text && text.length > 24 && !/verifying you are (a )?human/i.test(text)) {
|
||||
return { status: res.status, url: target, text, via: 'jina' };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
const raw = await webFetch(target, timeoutMs);
|
||||
if (raw && !raw.via) raw.via = 'raw';
|
||||
return raw;
|
||||
} catch (_) {}
|
||||
const left = remainingMs(deadline);
|
||||
if (left <= 0) return timedOut();
|
||||
const raw = await webFetch(target, left);
|
||||
if (raw && !raw.via) raw.via = 'raw';
|
||||
return raw;
|
||||
}, deadline, timedOut);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WEB_TIMEOUT_MS,
|
||||
PAGE_TIMEOUT_MS,
|
||||
SEARCH_BUDGET_MS,
|
||||
ENGINE_TIMEOUT_MS,
|
||||
BROWSER_UA,
|
||||
GOOGLE_UA,
|
||||
AGENT_UA,
|
||||
|
||||
Reference in New Issue
Block a user