smart-web-mcp 0.44.0 → 1.0.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/CHANGELOG.md +352 -1
- package/README.md +33 -566
- package/dist/assessment.d.ts +10 -3
- package/dist/assessment.js +21 -6
- package/dist/assessment.js.map +1 -1
- package/dist/body-export.d.ts +10 -0
- package/dist/body-export.js +28 -0
- package/dist/body-export.js.map +1 -0
- package/dist/browser-session.d.ts +32 -1
- package/dist/browser-session.js +373 -3
- package/dist/browser-session.js.map +1 -1
- package/dist/cli-fetch-options.d.ts +7 -0
- package/dist/cli-fetch-options.js +41 -0
- package/dist/cli-fetch-options.js.map +1 -0
- package/dist/cli.js +31 -12
- package/dist/cli.js.map +1 -1
- package/dist/comment-export.d.ts +10 -0
- package/dist/comment-export.js +39 -0
- package/dist/comment-export.js.map +1 -0
- package/dist/composition.d.ts +1 -22
- package/dist/composition.js +0 -16
- package/dist/composition.js.map +1 -1
- package/dist/extraction/budget.d.ts +3 -0
- package/dist/extraction/budget.js +88 -7
- package/dist/extraction/budget.js.map +1 -1
- package/dist/extraction/document.js +134 -9
- package/dist/extraction/document.js.map +1 -1
- package/dist/korea/coupang-filters.d.ts +5 -0
- package/dist/korea/coupang-filters.js +87 -0
- package/dist/korea/coupang-filters.js.map +1 -0
- package/dist/korea/coupang.d.ts +27 -6
- package/dist/korea/coupang.js +202 -82
- package/dist/korea/coupang.js.map +1 -1
- package/dist/korea/proxy-client.d.ts +1 -0
- package/dist/korea/proxy-client.js +6 -2
- package/dist/korea/proxy-client.js.map +1 -1
- package/dist/korea/smartvertical.d.ts +1 -0
- package/dist/korea/smartvertical.js +39 -25
- package/dist/korea/smartvertical.js.map +1 -1
- package/dist/lib.d.ts +4 -2
- package/dist/lib.js +2 -1
- package/dist/lib.js.map +1 -1
- package/dist/mcp-server.js +39 -15
- package/dist/mcp-server.js.map +1 -1
- package/dist/operator-browser-fixture.d.ts +7 -0
- package/dist/operator-browser-fixture.js +19 -0
- package/dist/operator-browser-fixture.js.map +1 -0
- package/dist/operator-canary-cli.d.ts +6 -0
- package/dist/operator-canary-cli.js +190 -0
- package/dist/operator-canary-cli.js.map +1 -0
- package/dist/operator-canary.d.ts +74 -0
- package/dist/operator-canary.js +135 -0
- package/dist/operator-canary.js.map +1 -0
- package/dist/operator-cdp.d.ts +15 -0
- package/dist/operator-cdp.js +195 -0
- package/dist/operator-cdp.js.map +1 -0
- package/dist/operator-frame.d.ts +3 -0
- package/dist/operator-frame.js +55 -0
- package/dist/operator-frame.js.map +1 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +1 -0
- package/dist/settings.js.map +1 -1
- package/dist/shared.d.ts +47 -10
- package/dist/shared.js +15 -9
- package/dist/shared.js.map +1 -1
- package/dist/smartbrowser.d.ts +169 -0
- package/dist/smartbrowser.js +288 -0
- package/dist/smartbrowser.js.map +1 -0
- package/dist/smartcrawl.js +16 -21
- package/dist/smartcrawl.js.map +1 -1
- package/dist/smartfetch/academic-fallback.js +3 -5
- package/dist/smartfetch/academic-fallback.js.map +1 -1
- package/dist/smartfetch/archive-fallback.js +2 -5
- package/dist/smartfetch/archive-fallback.js.map +1 -1
- package/dist/smartfetch/assets.d.ts +1 -1
- package/dist/smartfetch/assets.js +28 -1
- package/dist/smartfetch/assets.js.map +1 -1
- package/dist/smartfetch/jina-reader.d.ts +1 -0
- package/dist/smartfetch/jina-reader.js +17 -3
- package/dist/smartfetch/jina-reader.js.map +1 -1
- package/dist/smartfetch/pipeline.js +15 -25
- package/dist/smartfetch/pipeline.js.map +1 -1
- package/dist/smartfetch/provider-policy.d.ts +3 -0
- package/dist/smartfetch/provider-policy.js +45 -2
- package/dist/smartfetch/provider-policy.js.map +1 -1
- package/dist/smartfetch/provider-types.d.ts +8 -0
- package/dist/smartfetch/providers/article.js +76 -10
- package/dist/smartfetch/providers/article.js.map +1 -1
- package/dist/smartfetch/providers/blind-post.d.ts +2 -0
- package/dist/smartfetch/providers/blind-post.js +167 -0
- package/dist/smartfetch/providers/blind-post.js.map +1 -0
- package/dist/smartfetch/providers/clien.d.ts +2 -0
- package/dist/smartfetch/providers/clien.js +157 -0
- package/dist/smartfetch/providers/clien.js.map +1 -0
- package/dist/smartfetch/providers/commerce.d.ts +15 -0
- package/dist/smartfetch/providers/commerce.js +123 -7
- package/dist/smartfetch/providers/commerce.js.map +1 -1
- package/dist/smartfetch/providers/dcinside.js +313 -20
- package/dist/smartfetch/providers/dcinside.js.map +1 -1
- package/dist/smartfetch/providers/hackernews-html.d.ts +21 -0
- package/dist/smartfetch/providers/hackernews-html.js +148 -0
- package/dist/smartfetch/providers/hackernews-html.js.map +1 -0
- package/dist/smartfetch/providers/hackernews.js +76 -14
- package/dist/smartfetch/providers/hackernews.js.map +1 -1
- package/dist/smartfetch/providers/index.js +7 -1
- package/dist/smartfetch/providers/index.js.map +1 -1
- package/dist/smartfetch/providers/linkedin.js +2 -1
- package/dist/smartfetch/providers/linkedin.js.map +1 -1
- package/dist/smartfetch/providers/naver-blog.d.ts +2 -0
- package/dist/smartfetch/providers/naver-blog.js +87 -45
- package/dist/smartfetch/providers/naver-blog.js.map +1 -1
- package/dist/smartfetch/providers/naver-cafe-native.d.ts +7 -0
- package/dist/smartfetch/providers/naver-cafe-native.js +188 -0
- package/dist/smartfetch/providers/naver-cafe-native.js.map +1 -0
- package/dist/smartfetch/providers/naver-cafe.js +165 -4
- package/dist/smartfetch/providers/naver-cafe.js.map +1 -1
- package/dist/smartfetch/providers/naver-map-search.js +1 -1
- package/dist/smartfetch/providers/naver-map-search.js.map +1 -1
- package/dist/smartfetch/providers/ppomppu.d.ts +2 -0
- package/dist/smartfetch/providers/ppomppu.js +289 -0
- package/dist/smartfetch/providers/ppomppu.js.map +1 -0
- package/dist/smartfetch/providers/reddit.js +24 -3
- package/dist/smartfetch/providers/reddit.js.map +1 -1
- package/dist/smartfetch/providers/threads.js +1 -1
- package/dist/smartfetch/providers/threads.js.map +1 -1
- package/dist/smartfetch/providers/wikipedia.js +138 -0
- package/dist/smartfetch/providers/wikipedia.js.map +1 -1
- package/dist/smartfetch.d.ts +11 -2
- package/dist/smartfetch.js +369 -78
- package/dist/smartfetch.js.map +1 -1
- package/dist/smartsearch-brave.d.ts +2 -0
- package/dist/smartsearch-brave.js +43 -0
- package/dist/smartsearch-brave.js.map +1 -0
- package/dist/smartsearch-browser.d.ts +5 -0
- package/dist/smartsearch-browser.js +116 -0
- package/dist/smartsearch-browser.js.map +1 -0
- package/dist/smartsearch-ddg.d.ts +2 -0
- package/dist/smartsearch-ddg.js +46 -0
- package/dist/smartsearch-ddg.js.map +1 -0
- package/dist/smartsearch-node.d.ts +6 -0
- package/dist/smartsearch-node.js +21 -0
- package/dist/smartsearch-node.js.map +1 -0
- package/dist/smartsearch-postgresql.d.ts +7 -0
- package/dist/smartsearch-postgresql.js +44 -0
- package/dist/smartsearch-postgresql.js.map +1 -0
- package/dist/smartsearch-python.d.ts +8 -0
- package/dist/smartsearch-python.js +87 -0
- package/dist/smartsearch-python.js.map +1 -0
- package/dist/smartsearch-quality.d.ts +6 -0
- package/dist/smartsearch-quality.js +47 -0
- package/dist/smartsearch-quality.js.map +1 -0
- package/dist/smartsearch-youtube.d.ts +7 -0
- package/dist/smartsearch-youtube.js +76 -0
- package/dist/smartsearch-youtube.js.map +1 -0
- package/dist/smartsearch.d.ts +3 -0
- package/dist/smartsearch.js +617 -132
- package/dist/smartsearch.js.map +1 -1
- package/package.json +8 -5
- package/python/requirements-undetected-chromedriver.txt +2 -2
package/dist/smartfetch.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { absolutizeUrl, assetFromUrl, asString, cleanLinks, dedupeUrls, configFlag, extractAnchorHrefs, extractUrls, inferTarget, needsBotChallengeBrowserUpgrade, needsDynamicCrawl, needsSpaBrowserFallback, resolveValidatedUrl, validateOutboundUrl, } from "./shared.js";
|
|
2
|
-
import { createAssessment } from "./assessment.js";
|
|
2
|
+
import { createAssessment, hasTerminalSafetyBlock } from "./assessment.js";
|
|
3
3
|
import { runBrowserSession, resolveProfileDir } from "./browser-session.js";
|
|
4
4
|
import { projectSmartfetchOutput, resolveSmartfetchBudgetPolicy } from "./extraction/budget.js";
|
|
5
|
-
import { configText } from "./settings.js";
|
|
5
|
+
import { configText, configValue } from "./settings.js";
|
|
6
6
|
import { browserFetchHeaders, directFetchHeaders, impitFetchHeaders, shouldUseFastWait } from "./site-hints.js";
|
|
7
7
|
import { getImpitCache, getImpitLoader, setImpitCache } from "./smartfetch-internals.js";
|
|
8
8
|
import { planSmartfetchAcquire } from "./smartfetch/acquisition-plan.js";
|
|
@@ -14,6 +14,10 @@ import { maybeUseJinaReader } from "./smartfetch/jina-reader.js";
|
|
|
14
14
|
import { runCoupang } from "./korea/coupang.js";
|
|
15
15
|
import { assessSmartfetchOutput, buildAcquireStage, buildNormalizeStage, buildResolveStage } from "./smartfetch/pipeline.js";
|
|
16
16
|
import { resolveSmartfetchProvider } from "./smartfetch/providers/index.js";
|
|
17
|
+
import { naverBlogMobilePostUrl } from "./smartfetch/providers/naver-blog.js";
|
|
18
|
+
import { NAVER_CAFE_READY_SELECTOR, naverCafeMobilePostUrl } from "./smartfetch/providers/naver-cafe-native.js";
|
|
19
|
+
import { exportComments } from "./comment-export.js";
|
|
20
|
+
import { exportBody } from "./body-export.js";
|
|
17
21
|
const FAILURE_CACHE_TTL_MS = 30_000;
|
|
18
22
|
const IMPIT_MAX_REDIRECTS = 5;
|
|
19
23
|
const TEXT_RENDER_LIMITS = {
|
|
@@ -26,14 +30,42 @@ const TEXT_RENDER_LIMITS = {
|
|
|
26
30
|
handoffLinks: 8,
|
|
27
31
|
assets: 10,
|
|
28
32
|
};
|
|
29
|
-
/** Inflight coalescing: deduplicate concurrent smartfetch calls to the same URL. */
|
|
30
|
-
const inflightSmartfetch = new Map();
|
|
31
33
|
function isRedirect(status) {
|
|
32
34
|
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
33
35
|
}
|
|
34
36
|
function allowPrivateHosts() {
|
|
35
37
|
return configFlag("network.allowPrivateHosts");
|
|
36
38
|
}
|
|
39
|
+
function nativeNaverTarget(url) {
|
|
40
|
+
if (naverBlogMobilePostUrl(url))
|
|
41
|
+
return "naver_blog";
|
|
42
|
+
if (naverCafeMobilePostUrl(url))
|
|
43
|
+
return "naver_cafe_post";
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function operatorRecoveryReason(result) {
|
|
47
|
+
if (!configText("fetch.browserCdpUrl") || result.ok || hasTerminalSafetyBlock([result.error]))
|
|
48
|
+
return "";
|
|
49
|
+
// An explicit cooldown is not an invitation to retry in another client.
|
|
50
|
+
if (result.http_status === 429 || result.retry_after)
|
|
51
|
+
return "";
|
|
52
|
+
if (result.http_status === 403 || (result.http_status && result.http_status >= 500)) {
|
|
53
|
+
return `direct HTTP ${result.http_status} — continued in approved operator browser`;
|
|
54
|
+
}
|
|
55
|
+
if (["redirect_resolution_failed", "plain_fetch_exception", "impit_fetch_exception", "direct_acquisition_timeout"].includes(result.error?.code || "")) {
|
|
56
|
+
return "direct transport failed — continued in approved operator browser";
|
|
57
|
+
}
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
function preflightCooldown(result, method) {
|
|
61
|
+
if (result.http_status !== 429 && !result.retry_after)
|
|
62
|
+
return undefined;
|
|
63
|
+
return {
|
|
64
|
+
ok: false, method, content: "", links: [], final_url: result.url,
|
|
65
|
+
http_status: result.http_status, retry_after: result.retry_after,
|
|
66
|
+
error: { category: "block", code: "direct_retry_after", message: `status=${result.http_status}${result.retry_after ? `; Retry-After=${result.retry_after}` : ""}` },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
37
69
|
function compactText(value, maxChars) {
|
|
38
70
|
const text = String(value || "").trim();
|
|
39
71
|
if (text.length <= maxChars)
|
|
@@ -108,11 +140,36 @@ function redirectUrl(url, headers, body) {
|
|
|
108
140
|
}
|
|
109
141
|
return "";
|
|
110
142
|
}
|
|
111
|
-
|
|
143
|
+
class FetchDeadlineError extends Error {
|
|
144
|
+
constructor() { super("The original smartfetch deadline expired"); }
|
|
145
|
+
}
|
|
146
|
+
function remainingFetchTime(deadlineAt) {
|
|
147
|
+
const remaining = deadlineAt - Date.now();
|
|
148
|
+
if (remaining <= 0)
|
|
149
|
+
throw new FetchDeadlineError();
|
|
150
|
+
return remaining;
|
|
151
|
+
}
|
|
152
|
+
async function withinFetchDeadline(deadlineAt, work) {
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const remaining = remainingFetchTime(deadlineAt);
|
|
155
|
+
let timer;
|
|
156
|
+
const expiry = new Promise((_resolve, reject) => {
|
|
157
|
+
timer = setTimeout(() => { controller.abort(); reject(new FetchDeadlineError()); }, remaining);
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
return await Promise.race([work(controller.signal), expiry]);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
controller.abort();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function runNativeFetch(url, deadlineAt, signal) {
|
|
112
168
|
const headers = directFetchHeaders(url);
|
|
113
169
|
const resolved = await resolveValidatedUrl(url, {
|
|
114
170
|
allowPrivateHosts: allowPrivateHosts(),
|
|
115
|
-
timeoutMs,
|
|
171
|
+
timeoutMs: remainingFetchTime(deadlineAt),
|
|
172
|
+
signal,
|
|
116
173
|
headers,
|
|
117
174
|
});
|
|
118
175
|
if (!resolved.ok) {
|
|
@@ -128,18 +185,26 @@ async function runNativeFetch(url, timeoutMs) {
|
|
|
128
185
|
},
|
|
129
186
|
};
|
|
130
187
|
}
|
|
188
|
+
const cooldown = preflightCooldown(resolved, "plain_fetch");
|
|
189
|
+
if (cooldown)
|
|
190
|
+
return cooldown;
|
|
131
191
|
const controller = new AbortController();
|
|
132
|
-
const timer = setTimeout(() => controller.abort(),
|
|
192
|
+
const timer = setTimeout(() => controller.abort(), remainingFetchTime(deadlineAt));
|
|
133
193
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
194
|
+
const requestSignal = AbortSignal.any([signal, controller.signal]);
|
|
195
|
+
requestSignal.throwIfAborted();
|
|
196
|
+
const response = await fetch(resolved.url, { signal: requestSignal, headers, redirect: "error" });
|
|
136
197
|
if (!response.ok) {
|
|
198
|
+
// Cleanup must not delay publishing an observed terminal status.
|
|
199
|
+
void response.body?.cancel().catch(() => { });
|
|
137
200
|
return {
|
|
138
201
|
ok: false,
|
|
139
202
|
method: "plain_fetch",
|
|
140
203
|
content: "",
|
|
141
204
|
links: [],
|
|
142
205
|
final_url: resolved.url,
|
|
206
|
+
http_status: response.status,
|
|
207
|
+
retry_after: header(response.headers, "retry-after"),
|
|
143
208
|
error: {
|
|
144
209
|
category: response.status === 403 || response.status === 429 ? "block" : "unavailable",
|
|
145
210
|
code: "plain_fetch_failed",
|
|
@@ -147,6 +212,8 @@ async function runNativeFetch(url, timeoutMs) {
|
|
|
147
212
|
},
|
|
148
213
|
};
|
|
149
214
|
}
|
|
215
|
+
const body = await response.text();
|
|
216
|
+
remainingFetchTime(deadlineAt);
|
|
150
217
|
const links = [...extractAnchorHrefs(body), ...extractUrls(body)].map((item) => absolutizeUrl(item, resolved.url) || item).filter(Boolean);
|
|
151
218
|
return { ok: true, method: "plain_fetch", content: body, links: dedupeUrls(links), final_url: resolved.url };
|
|
152
219
|
}
|
|
@@ -169,14 +236,17 @@ async function runNativeFetch(url, timeoutMs) {
|
|
|
169
236
|
clearTimeout(timer);
|
|
170
237
|
}
|
|
171
238
|
}
|
|
172
|
-
async function runImpitFetch(url,
|
|
239
|
+
async function runImpitFetch(url, deadlineAt, signal) {
|
|
173
240
|
const Impit = await getImpit();
|
|
241
|
+
signal.throwIfAborted();
|
|
242
|
+
remainingFetchTime(deadlineAt);
|
|
174
243
|
if (!Impit)
|
|
175
|
-
return runNativeFetch(url,
|
|
244
|
+
return runNativeFetch(url, deadlineAt, signal);
|
|
176
245
|
const headers = impitFetchHeaders(url);
|
|
177
246
|
const resolved = await resolveValidatedUrl(url, {
|
|
178
247
|
allowPrivateHosts: allowPrivateHosts(),
|
|
179
|
-
timeoutMs,
|
|
248
|
+
timeoutMs: remainingFetchTime(deadlineAt),
|
|
249
|
+
signal,
|
|
180
250
|
headers,
|
|
181
251
|
});
|
|
182
252
|
if (!resolved.ok) {
|
|
@@ -192,12 +262,19 @@ async function runImpitFetch(url, timeoutMs) {
|
|
|
192
262
|
},
|
|
193
263
|
};
|
|
194
264
|
}
|
|
265
|
+
const cooldown = preflightCooldown(resolved, "impit_fetch");
|
|
266
|
+
if (cooldown)
|
|
267
|
+
return cooldown;
|
|
195
268
|
let currentUrl = resolved.url;
|
|
196
269
|
try {
|
|
197
|
-
const client = new Impit({ browser: "chrome", timeout:
|
|
270
|
+
const client = new Impit({ browser: "chrome", timeout: remainingFetchTime(deadlineAt) });
|
|
198
271
|
for (let hop = 0; hop <= IMPIT_MAX_REDIRECTS; hop++) {
|
|
199
|
-
|
|
200
|
-
const
|
|
272
|
+
signal.throwIfAborted();
|
|
273
|
+
const response = await client.fetch(currentUrl, { headers, redirect: "error", signal, timeout: remainingFetchTime(deadlineAt) });
|
|
274
|
+
// Preserve status/cooldown even if an error response body is interrupted.
|
|
275
|
+
const body = response.ok || isRedirect(response.status) ? await response.text() : "";
|
|
276
|
+
if (response.ok || isRedirect(response.status))
|
|
277
|
+
remainingFetchTime(deadlineAt);
|
|
201
278
|
if (response.ok) {
|
|
202
279
|
const finalUrl = asString(response.url).trim() || currentUrl;
|
|
203
280
|
const base = finalUrl || currentUrl;
|
|
@@ -211,12 +288,15 @@ async function runImpitFetch(url, timeoutMs) {
|
|
|
211
288
|
};
|
|
212
289
|
}
|
|
213
290
|
if (!isRedirect(response.status)) {
|
|
291
|
+
void response.body?.cancel().catch(() => { });
|
|
214
292
|
return {
|
|
215
293
|
ok: false,
|
|
216
294
|
method: "impit_fetch",
|
|
217
295
|
content: "",
|
|
218
296
|
links: [],
|
|
219
297
|
final_url: asString(response.url).trim() || currentUrl,
|
|
298
|
+
http_status: response.status,
|
|
299
|
+
retry_after: header(response.headers, "retry-after"),
|
|
220
300
|
error: {
|
|
221
301
|
category: response.status === 403 || response.status === 429 ? "block" : "unavailable",
|
|
222
302
|
code: "impit_fetch_failed",
|
|
@@ -316,6 +396,7 @@ async function runPlaywrightFetch(url, timeoutMs, target, profileDir, waitForSel
|
|
|
316
396
|
const result = await runBrowserSession({
|
|
317
397
|
url,
|
|
318
398
|
timeoutMs,
|
|
399
|
+
...(!profileDir && configText("fetch.browserCdpUrl") ? { cdpUrl: configText("fetch.browserCdpUrl") } : {}),
|
|
319
400
|
allowPrivateHosts: allowPrivateHosts(),
|
|
320
401
|
waitUntil: choosePlaywrightWaitUntil(target, url),
|
|
321
402
|
extraHeaders,
|
|
@@ -462,6 +543,8 @@ const HEADROOM_BALANCED_MIN = 60_000;
|
|
|
462
543
|
const PREFERRED_BALANCED_MIN = 30_000;
|
|
463
544
|
const PREFERRED_FULL_MIN = 120_000;
|
|
464
545
|
function asNonNegativeInteger(value) {
|
|
546
|
+
if (value === null || value === undefined)
|
|
547
|
+
return null;
|
|
465
548
|
const parsed = Number(value);
|
|
466
549
|
if (!Number.isFinite(parsed) || parsed < 0)
|
|
467
550
|
return null;
|
|
@@ -510,6 +593,15 @@ export function resolveSmartfetchOutputBudget(input = {}) {
|
|
|
510
593
|
reason: "context_mode used as backward-compatible alias (no numeric hint supplied)",
|
|
511
594
|
};
|
|
512
595
|
}
|
|
596
|
+
if (input.configuredOutputBudget !== undefined) {
|
|
597
|
+
const policy = resolveSmartfetchBudgetPolicy(input.configuredOutputBudget);
|
|
598
|
+
const preset = policy.name.replace(/^smartfetch\./, "");
|
|
599
|
+
return {
|
|
600
|
+
preset,
|
|
601
|
+
source: "settings",
|
|
602
|
+
reason: `fetch.outputBudget settings selected ${preset}`,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
513
605
|
return {
|
|
514
606
|
preset: "compact",
|
|
515
607
|
source: "default",
|
|
@@ -575,28 +667,32 @@ function isDocumentLikeAsset(asset) {
|
|
|
575
667
|
return asset?.kind === "document" || asset?.kind === "archive" || asset?.kind === "attachment";
|
|
576
668
|
}
|
|
577
669
|
export async function runSmartfetch(options, runtime = {}) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (coalesceKey) {
|
|
584
|
-
const inflight = inflightSmartfetch.get(coalesceKey);
|
|
585
|
-
if (inflight)
|
|
586
|
-
return inflight;
|
|
670
|
+
const timeoutMs = Math.max(3000, Math.floor(Number(options.timeoutMs || 25000)));
|
|
671
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
672
|
+
const output = baseOutput(options.url, inferTarget(options.url, options.target || "auto"));
|
|
673
|
+
try {
|
|
674
|
+
return await runSmartfetchTask(options, runtime, deadlineAt, output);
|
|
587
675
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (!(error instanceof FetchDeadlineError))
|
|
678
|
+
throw error;
|
|
679
|
+
output.partial = true;
|
|
680
|
+
output.errors.push({ category: "timeout", code: "smartfetch_deadline_exceeded", message: error.message });
|
|
681
|
+
output.assessment = createAssessment({ confidence: "low", blocked_reason: error.message, signals: ["retrieval:deadline_exceeded"] });
|
|
682
|
+
if (!output.pipeline.acquire.attempt_map) {
|
|
683
|
+
output.pipeline.acquire = buildAcquireStage({ attempts: output.retrieval_method, active: { ok: false, method: output.retrieval_method.at(-1) || "none", content: "", links: [] }, usedBrowser: output.evidence.playwright_attempted, usedArchive: false });
|
|
684
|
+
}
|
|
685
|
+
output.pipeline.normalize.status = "partial";
|
|
686
|
+
output.pipeline.assess = output.assessment;
|
|
687
|
+
return attachResearchHandoff(output);
|
|
592
688
|
}
|
|
593
|
-
return promise;
|
|
594
689
|
}
|
|
595
|
-
async function
|
|
690
|
+
async function runSmartfetchTask(options, runtime, deadlineAt, output) {
|
|
691
|
+
// A URL is not a task identity: callers can have different acquisition,
|
|
692
|
+
// session, runtime and projection requirements. Keep each task independent.
|
|
596
693
|
const requestedTarget = options.target || "auto";
|
|
597
|
-
const target = inferTarget(options.url, requestedTarget);
|
|
598
|
-
const
|
|
599
|
-
const output = baseOutput(options.url, target);
|
|
694
|
+
const target = (requestedTarget === "auto" ? nativeNaverTarget(options.url) : undefined) || inferTarget(options.url, requestedTarget);
|
|
695
|
+
const remaining = () => remainingFetchTime(deadlineAt);
|
|
600
696
|
output.pipeline.resolve = buildResolveStage({
|
|
601
697
|
url: options.url,
|
|
602
698
|
requestedTarget,
|
|
@@ -607,6 +703,7 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
607
703
|
const profileName = options.profile || configText("fetch.browserProfile");
|
|
608
704
|
const profileDir = profileName ? resolveProfileDir(profileName) : undefined;
|
|
609
705
|
const validation = await validateOutboundUrl(options.url, { allowPrivateHosts: allowPrivateHosts() });
|
|
706
|
+
remaining();
|
|
610
707
|
if (!validation.ok) {
|
|
611
708
|
return attachResearchHandoff({
|
|
612
709
|
...output,
|
|
@@ -721,16 +818,27 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
721
818
|
}
|
|
722
819
|
}
|
|
723
820
|
let active;
|
|
821
|
+
let expectedPostUrl = nativeNaverTarget(options.url) === target ? options.url : undefined;
|
|
724
822
|
if (acquirePlan.lane === "browser") {
|
|
725
|
-
|
|
823
|
+
const nativeUrl = target === "naver_blog" ? naverBlogMobilePostUrl(options.url)
|
|
824
|
+
: target === "naver_cafe_post" ? naverCafeMobilePostUrl(options.url) : null;
|
|
825
|
+
if (nativeUrl && nativeUrl !== options.url) {
|
|
826
|
+
effectiveReason += `; exact ${target === "naver_blog" ? "Naver Blog" : "Naver Cafe"} native mobile document`;
|
|
827
|
+
browserAttempt.reason = effectiveReason;
|
|
828
|
+
}
|
|
829
|
+
const nativeWait = nativeUrl && target === "naver_cafe_post"
|
|
830
|
+
? NAVER_CAFE_READY_SELECTOR
|
|
831
|
+
: undefined;
|
|
832
|
+
active = await runPlaywrightFetchWithRuntime(nativeUrl || options.url, remaining(), target, runtime, profileDir, options.wait_for || nativeWait);
|
|
726
833
|
browserAttempt.attempted = true;
|
|
727
834
|
browserAttempt.ok = active.ok;
|
|
728
835
|
browserAttempt.method = active.method;
|
|
729
836
|
output.evidence.playwright_attempted = true;
|
|
730
|
-
output.evidence.playwright_success = active.ok;
|
|
731
837
|
output.retrieval_method.push(active.method);
|
|
732
838
|
if (active.error)
|
|
733
839
|
output.errors.push(active.error);
|
|
840
|
+
remaining();
|
|
841
|
+
output.evidence.playwright_success = active.ok;
|
|
734
842
|
}
|
|
735
843
|
else if (acquirePlan.lane === "seed") {
|
|
736
844
|
active = {
|
|
@@ -752,65 +860,156 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
752
860
|
output.retrieval_method.push(active.method);
|
|
753
861
|
}
|
|
754
862
|
else {
|
|
755
|
-
|
|
863
|
+
// Leave time for the approved browser when direct transport stalls. Both
|
|
864
|
+
// phases still share the caller's original deadline.
|
|
865
|
+
const directDeadline = configText("fetch.browserCdpUrl") && !profileDir
|
|
866
|
+
? Math.min(deadlineAt, Date.now() + Math.max(1, Math.floor(remaining() * 0.4)))
|
|
867
|
+
: deadlineAt;
|
|
868
|
+
try {
|
|
869
|
+
active = await withinFetchDeadline(directDeadline, signal => (runtime.runImpitFetch || runImpitFetch)(options.url, directDeadline, signal));
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
if (!(error instanceof FetchDeadlineError))
|
|
873
|
+
throw error;
|
|
874
|
+
active = { ok: false, method: "direct_fetch", content: "", links: [], error: { category: "timeout", code: "direct_acquisition_timeout", message: "Direct acquisition exhausted its share of the original fetch budget" } };
|
|
875
|
+
}
|
|
756
876
|
directAttempt.attempted = true;
|
|
757
877
|
directAttempt.ok = active.ok;
|
|
758
878
|
directAttempt.method = active.method;
|
|
759
879
|
output.evidence.impit_attempted = true;
|
|
760
|
-
output.evidence.impit_success = active.ok;
|
|
761
880
|
output.retrieval_method.push(active.method);
|
|
762
881
|
if (active.error)
|
|
763
882
|
output.errors.push(active.error);
|
|
883
|
+
remaining();
|
|
884
|
+
output.evidence.impit_success = active.ok;
|
|
764
885
|
const redirectUrl = active.final_url && active.final_url !== options.url ? active.final_url : "";
|
|
765
886
|
const redirectTarget = redirectUrl
|
|
766
|
-
? inferTarget(redirectUrl, "auto")
|
|
887
|
+
? nativeNaverTarget(redirectUrl) || inferTarget(redirectUrl, "auto")
|
|
767
888
|
: target;
|
|
768
889
|
const redirectProvider = redirectUrl
|
|
769
890
|
? resolveSmartfetchProvider(redirectUrl, redirectTarget)
|
|
770
891
|
: plannedProvider;
|
|
771
892
|
const needsRedirectBrowserUpgrade = Boolean(redirectUrl)
|
|
772
893
|
&& (redirectProvider.capabilities.includes("requires_browser") || redirectProvider.capabilities.includes("interactive_surface"));
|
|
773
|
-
|
|
774
|
-
|
|
894
|
+
const mustNotRetry = hasTerminalSafetyBlock([active.error]) || active.http_status === 429 || Boolean(active.retry_after);
|
|
895
|
+
if (mustNotRetry) {
|
|
896
|
+
browserAttempt.reason = hasTerminalSafetyBlock([active.error])
|
|
897
|
+
? "terminal URL safety rejection — no browser continuation"
|
|
898
|
+
: `direct HTTP ${active.http_status}: cooldown${active.retry_after ? `; Retry-After=${active.retry_after}` : ""} — no browser continuation`;
|
|
899
|
+
}
|
|
900
|
+
else if (needsRedirectBrowserUpgrade) {
|
|
901
|
+
if (!expectedPostUrl && (requestedTarget === "auto" || target === redirectTarget)
|
|
902
|
+
&& (naverBlogMobilePostUrl(redirectUrl) || naverCafeMobilePostUrl(redirectUrl)))
|
|
903
|
+
expectedPostUrl = redirectUrl;
|
|
904
|
+
const browserResult = await runPlaywrightFetchWithRuntime(redirectUrl, remaining(), redirectTarget, runtime, profileDir, options.wait_for);
|
|
775
905
|
browserAttempt.attempted = true;
|
|
776
906
|
browserAttempt.ok = browserResult.ok;
|
|
777
907
|
browserAttempt.method = browserResult.method;
|
|
778
908
|
browserAttempt.reason = `redirected final provider:${redirectProvider.id} requires browser retrieval`;
|
|
779
909
|
output.evidence.playwright_attempted = true;
|
|
780
|
-
output.evidence.playwright_success = browserResult.ok;
|
|
781
910
|
output.retrieval_method.push(browserResult.method);
|
|
782
911
|
if (browserResult.error)
|
|
783
912
|
output.errors.push(browserResult.error);
|
|
913
|
+
remaining();
|
|
914
|
+
output.evidence.playwright_success = browserResult.ok;
|
|
784
915
|
effectiveLane = "browser";
|
|
785
916
|
effectiveReason = `redirected final provider:${redirectProvider.id} requires browser retrieval`;
|
|
786
917
|
redirectUpgrade = true;
|
|
787
|
-
if (browserResult.ok)
|
|
918
|
+
if (browserResult.ok || expectedPostUrl || hasTerminalSafetyBlock([browserResult.error]))
|
|
788
919
|
active = browserResult;
|
|
789
920
|
}
|
|
790
|
-
else if (needsBotChallengeBrowserUpgrade(active)) {
|
|
791
|
-
//
|
|
792
|
-
const browserResult = await runPlaywrightFetchWithRuntime(options.url,
|
|
921
|
+
else if (operatorRecoveryReason(active) || needsBotChallengeBrowserUpgrade(active)) {
|
|
922
|
+
// Direct-client failure does not prove the approved browser cannot read it.
|
|
923
|
+
const browserResult = await runPlaywrightFetchWithRuntime(options.url, remaining(), target, runtime, profileDir, options.wait_for);
|
|
793
924
|
browserAttempt.attempted = true;
|
|
794
925
|
browserAttempt.ok = browserResult.ok;
|
|
795
926
|
browserAttempt.method = browserResult.method;
|
|
796
|
-
browserAttempt.reason = "impit returned bot-challenge interstitial — upgraded to browser";
|
|
927
|
+
browserAttempt.reason = operatorRecoveryReason(active) || "impit returned bot-challenge interstitial — upgraded to browser";
|
|
797
928
|
output.evidence.playwright_attempted = true;
|
|
798
|
-
output.evidence.playwright_success = browserResult.ok;
|
|
799
929
|
output.retrieval_method.push(browserResult.method);
|
|
800
930
|
if (browserResult.error)
|
|
801
931
|
output.errors.push(browserResult.error);
|
|
932
|
+
remaining();
|
|
933
|
+
output.evidence.playwright_success = browserResult.ok;
|
|
802
934
|
effectiveLane = "browser";
|
|
803
|
-
effectiveReason =
|
|
935
|
+
effectiveReason = browserAttempt.reason;
|
|
804
936
|
if (browserResult.ok)
|
|
805
937
|
active = browserResult;
|
|
806
938
|
}
|
|
807
939
|
}
|
|
940
|
+
// Browser-resolved aliases must bind the observed article before continuing.
|
|
941
|
+
if (!expectedPostUrl && active.ok && active.final_url && !hasTerminalSafetyBlock([active.error]) && active.http_status !== 429 && !active.retry_after) {
|
|
942
|
+
const landedTarget = nativeNaverTarget(active.final_url);
|
|
943
|
+
if (landedTarget && (requestedTarget === "auto" || target === landedTarget)) {
|
|
944
|
+
expectedPostUrl = active.final_url;
|
|
945
|
+
if (!["m.blog.naver.com", "m.cafe.naver.com"].includes(new URL(active.final_url).hostname)) {
|
|
946
|
+
const browserResult = await runPlaywrightFetchWithRuntime(expectedPostUrl, remaining(), landedTarget, runtime, profileDir, options.wait_for);
|
|
947
|
+
browserAttempt.attempted = true;
|
|
948
|
+
browserAttempt.ok = browserResult.ok;
|
|
949
|
+
browserAttempt.method = browserResult.method;
|
|
950
|
+
browserAttempt.reason = "validated alias resolved to an exact Naver native article";
|
|
951
|
+
output.evidence.playwright_attempted = true;
|
|
952
|
+
output.retrieval_method.push(browserResult.method);
|
|
953
|
+
if (browserResult.error)
|
|
954
|
+
output.errors.push(browserResult.error);
|
|
955
|
+
remaining();
|
|
956
|
+
output.evidence.playwright_success = browserResult.ok;
|
|
957
|
+
effectiveLane = "browser";
|
|
958
|
+
effectiveReason = browserAttempt.reason;
|
|
959
|
+
redirectUpgrade = true;
|
|
960
|
+
active = browserResult;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
808
964
|
const archiveTarget = inferTarget(active.final_url || options.url, "auto");
|
|
809
965
|
const archiveProvider = resolveSmartfetchProvider(active.final_url || options.url, archiveTarget);
|
|
966
|
+
const suppressRetries = hasTerminalSafetyBlock([active.error]) || active.http_status === 429 || Boolean(active.retry_after);
|
|
967
|
+
// A visible review-to-unlock prompt is a participation boundary, not a
|
|
968
|
+
// weak article to be sent through third-party relay/archive retries.
|
|
969
|
+
const lockedBlindPreview = ["teamblind.com", "www.teamblind.com"].includes(new URL(options.url).hostname)
|
|
970
|
+
&& /^\/kr\/company\/[^/]+\/review\/[^/]+\/?$/.test(new URL(options.url).pathname)
|
|
971
|
+
&& /class=["'][^"']*\breview_unlock\b/.test(active.content);
|
|
972
|
+
// NamuWiki's weak anonymous shell can consume the entire deadline in the
|
|
973
|
+
// reader relay even when the approved local browser can read the original.
|
|
974
|
+
// Keep the direct result and existing relay fallback if browser recovery fails.
|
|
975
|
+
if (effectiveLane === "direct" && target === "namuwiki_article" && configText("fetch.browserCdpUrl")
|
|
976
|
+
&& !profileDir && !suppressRetries && needsDynamicCrawl(active) && !browserAttempt.attempted) {
|
|
977
|
+
const browserResult = await runPlaywrightFetchWithRuntime(options.url, remaining(), target, runtime, profileDir, options.wait_for);
|
|
978
|
+
const original = new URL(options.url);
|
|
979
|
+
let landed = null;
|
|
980
|
+
try {
|
|
981
|
+
if (browserResult.final_url)
|
|
982
|
+
landed = new URL(browserResult.final_url);
|
|
983
|
+
}
|
|
984
|
+
catch { /* Unusable browser identity. */ }
|
|
985
|
+
// A full browser DOM may contain unrelated CAPTCHA/help terms in chrome;
|
|
986
|
+
// use the isolated primary content when available, not a page-wide token.
|
|
987
|
+
const usable = browserResult.ok && ((browserResult.primary_content?.text?.length || 0) >= 200 || !needsDynamicCrawl(browserResult))
|
|
988
|
+
&& landed?.origin === original.origin && landed.pathname === original.pathname && landed.search === original.search;
|
|
989
|
+
browserAttempt.attempted = true;
|
|
990
|
+
browserAttempt.ok = Boolean(usable);
|
|
991
|
+
browserAttempt.method = browserResult.method;
|
|
992
|
+
browserAttempt.reason = "weak NamuWiki direct document — approved browser before optional relays";
|
|
993
|
+
output.evidence.playwright_attempted = true;
|
|
994
|
+
output.retrieval_method.push(browserResult.method);
|
|
995
|
+
if (browserResult.error)
|
|
996
|
+
output.errors.push(browserResult.error);
|
|
997
|
+
remaining();
|
|
998
|
+
output.evidence.playwright_success = Boolean(usable);
|
|
999
|
+
if (usable) {
|
|
1000
|
+
active = browserResult;
|
|
1001
|
+
effectiveLane = "browser";
|
|
1002
|
+
effectiveReason = browserAttempt.reason;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
810
1005
|
const allowArchiveFallback = acquirePlan.allowArchiveFallback
|
|
1006
|
+
&& !suppressRetries
|
|
1007
|
+
&& !lockedBlindPreview
|
|
811
1008
|
&& effectiveLane === "direct"
|
|
812
1009
|
&& Boolean(archiveProvider.policy.allowArchiveFallback);
|
|
813
1010
|
const allowJinaReader = acquirePlan.allowJinaReader
|
|
1011
|
+
&& !suppressRetries
|
|
1012
|
+
&& !lockedBlindPreview
|
|
814
1013
|
&& effectiveLane === "direct"
|
|
815
1014
|
&& Boolean(archiveProvider.policy.allowJinaReader);
|
|
816
1015
|
if (!allowJinaReader) {
|
|
@@ -823,9 +1022,10 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
823
1022
|
? "archive fallback was not eligible for the effective provider/lane"
|
|
824
1023
|
: "archive fallback was not allowed by plan, privacy profile, or settings";
|
|
825
1024
|
}
|
|
826
|
-
if (allowJinaReader) {
|
|
1025
|
+
if (allowJinaReader && effectiveLane === "direct") {
|
|
827
1026
|
const beforeMethod = active.method;
|
|
828
|
-
const relayed = await maybeUseJinaReader(active.final_url || options.url, active,
|
|
1027
|
+
const relayed = await maybeUseJinaReader(active.final_url || options.url, active, remaining());
|
|
1028
|
+
remaining();
|
|
829
1029
|
active = relayed.active;
|
|
830
1030
|
output.errors.push(...relayed.errors);
|
|
831
1031
|
const jinaErrors = relayed.errors.filter((error) => error.code.startsWith("jina_reader"));
|
|
@@ -844,9 +1044,10 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
844
1044
|
if (jinaSucceeded)
|
|
845
1045
|
output.retrieval_method.push(active.method);
|
|
846
1046
|
}
|
|
847
|
-
if (allowArchiveFallback) {
|
|
1047
|
+
if (allowArchiveFallback && effectiveLane === "direct") {
|
|
848
1048
|
const beforeMethod = active.method;
|
|
849
|
-
const archived = await maybeUseArchiveFallback(active.final_url || options.url, active,
|
|
1049
|
+
const archived = await maybeUseArchiveFallback(active.final_url || options.url, active, remaining());
|
|
1050
|
+
remaining();
|
|
850
1051
|
active = archived.active;
|
|
851
1052
|
output.errors.push(...archived.errors);
|
|
852
1053
|
const archiveErrors = archived.errors.filter((error) => /archive|wayback/i.test(error.code));
|
|
@@ -868,7 +1069,8 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
868
1069
|
}
|
|
869
1070
|
}
|
|
870
1071
|
if (effectiveLane === "direct" && (needsDynamicCrawl(active) || needsSpaBrowserFallback(active))) {
|
|
871
|
-
const scraped = await maybeUseAdaptiveScraper(active.final_url || options.url, active,
|
|
1072
|
+
const scraped = await maybeUseAdaptiveScraper(active.final_url || options.url, active, remaining());
|
|
1073
|
+
remaining();
|
|
872
1074
|
output.errors.push(...scraped.errors);
|
|
873
1075
|
adaptiveScraperAttempt.attempted = scraped.attempted;
|
|
874
1076
|
adaptiveScraperAttempt.ok = scraped.succeeded;
|
|
@@ -891,24 +1093,26 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
891
1093
|
// SPA auto-fallback: after cheap alternatives (Jina, archive, adaptive scraper) didn't help,
|
|
892
1094
|
// if the direct fetch result looks like an SPA shell, upgrade to Playwright.
|
|
893
1095
|
// This must come AFTER Jina/archive so those cheap paths get a chance first.
|
|
894
|
-
if (effectiveLane === "direct" && needsSpaBrowserFallback(active)) {
|
|
895
|
-
const browserResult = await runPlaywrightFetchWithRuntime(options.url,
|
|
1096
|
+
if (effectiveLane === "direct" && !browserAttempt.attempted && needsSpaBrowserFallback(active)) {
|
|
1097
|
+
const browserResult = await runPlaywrightFetchWithRuntime(options.url, remaining(), target, runtime, profileDir, options.wait_for);
|
|
896
1098
|
browserAttempt.attempted = true;
|
|
897
1099
|
browserAttempt.ok = browserResult.ok;
|
|
898
1100
|
browserAttempt.method = browserResult.method;
|
|
899
1101
|
browserAttempt.reason = "direct fetch returned SPA shell — auto-fallback to browser for JS rendering";
|
|
900
1102
|
output.evidence.playwright_attempted = true;
|
|
901
|
-
output.evidence.playwright_success = browserResult.ok;
|
|
902
1103
|
output.retrieval_method.push(browserResult.method);
|
|
903
1104
|
if (browserResult.error)
|
|
904
1105
|
output.errors.push(browserResult.error);
|
|
1106
|
+
remaining();
|
|
1107
|
+
output.evidence.playwright_success = browserResult.ok;
|
|
905
1108
|
effectiveLane = "browser";
|
|
906
1109
|
effectiveReason = "direct fetch returned SPA shell — auto-fallback to browser for JS rendering";
|
|
907
1110
|
if (browserResult.ok)
|
|
908
1111
|
active = browserResult;
|
|
909
1112
|
}
|
|
910
|
-
let effectiveTarget = target;
|
|
911
|
-
|
|
1113
|
+
let effectiveTarget = (expectedPostUrl ? nativeNaverTarget(expectedPostUrl) : undefined) || target;
|
|
1114
|
+
// The identity parser and selected normalizer must agree on accepted URLs.
|
|
1115
|
+
if ((options.target || "auto") === "auto" && !expectedPostUrl) {
|
|
912
1116
|
const inferred = inferTarget(active.final_url || options.url, "auto");
|
|
913
1117
|
if (inferred !== effectiveTarget) {
|
|
914
1118
|
effectiveTarget = inferred;
|
|
@@ -916,6 +1120,8 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
916
1120
|
output.risk_level = riskLevelForTarget(inferred);
|
|
917
1121
|
}
|
|
918
1122
|
}
|
|
1123
|
+
output.target = effectiveTarget;
|
|
1124
|
+
output.risk_level = riskLevelForTarget(effectiveTarget);
|
|
919
1125
|
output.pipeline.resolve = buildResolveStage({
|
|
920
1126
|
url: options.url,
|
|
921
1127
|
requestedTarget,
|
|
@@ -944,17 +1150,39 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
944
1150
|
],
|
|
945
1151
|
};
|
|
946
1152
|
const providerUrl = active.final_url || options.url;
|
|
947
|
-
|
|
1153
|
+
const exactNaverProviderUrl = expectedPostUrl || (target === "naver_blog" || target === "naver_cafe_post" ? options.url : undefined);
|
|
1154
|
+
let provider = resolveSmartfetchProvider(exactNaverProviderUrl || providerUrl, effectiveTarget);
|
|
948
1155
|
if (provider.id === "generic" && providerUrl !== options.url) {
|
|
949
1156
|
const originalProvider = resolveSmartfetchProvider(options.url, effectiveTarget);
|
|
950
1157
|
if (originalProvider.id !== "generic")
|
|
951
1158
|
provider = originalProvider;
|
|
952
1159
|
}
|
|
1160
|
+
if (suppressRetries) {
|
|
1161
|
+
// Normalizers can perform their own feed/API/search I/O. Do not let them
|
|
1162
|
+
// silently repeat a cooldown request or reopen a terminal safety rejection.
|
|
1163
|
+
output.partial = true;
|
|
1164
|
+
output.assessment = createAssessment({
|
|
1165
|
+
confidence: "low", blocked_reason: active.error?.message || "Retrieval stopped",
|
|
1166
|
+
signals: [hasTerminalSafetyBlock([active.error]) ? "resolve:blocked" : "retrieval:cooldown"],
|
|
1167
|
+
});
|
|
1168
|
+
output.pipeline.normalize = buildNormalizeStage({ provider, partial: true });
|
|
1169
|
+
output.pipeline.assess = output.assessment;
|
|
1170
|
+
return attachResearchHandoff(output);
|
|
1171
|
+
}
|
|
953
1172
|
const normalized = await provider.normalize({
|
|
954
1173
|
url: options.url,
|
|
955
1174
|
resolvedUrl: providerUrl,
|
|
1175
|
+
...(expectedPostUrl ? { expectedPostUrl } : {}),
|
|
956
1176
|
target: effectiveTarget,
|
|
957
|
-
timeoutMs,
|
|
1177
|
+
timeoutMs: remaining(),
|
|
1178
|
+
deadlineAt,
|
|
1179
|
+
browserSessionOptions: {
|
|
1180
|
+
...(!profileDir && configText("fetch.browserCdpUrl") ? { cdpUrl: configText("fetch.browserCdpUrl") } : {}),
|
|
1181
|
+
...(profileDir ? { profileDir } : {}),
|
|
1182
|
+
allowPrivateHosts: allowPrivateHosts(),
|
|
1183
|
+
waitUntil: choosePlaywrightWaitUntil(effectiveTarget, options.url),
|
|
1184
|
+
extraHeaders: browserFetchHeaders(options.url),
|
|
1185
|
+
},
|
|
958
1186
|
active,
|
|
959
1187
|
errors: output.errors,
|
|
960
1188
|
runBrowserSession: runtime.runBrowserSession || runBrowserSession,
|
|
@@ -963,15 +1191,17 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
963
1191
|
runCoupangSearch: runtime.runCoupangSearch || runCoupang,
|
|
964
1192
|
...(options.includeMarkdown ? { includeMarkdown: true } : {}),
|
|
965
1193
|
});
|
|
1194
|
+
remaining();
|
|
966
1195
|
const academic = await maybeUseAcademicFallback({
|
|
967
1196
|
url: options.url,
|
|
968
1197
|
resolvedUrl: providerUrl,
|
|
969
1198
|
target: effectiveTarget,
|
|
970
|
-
timeoutMs,
|
|
1199
|
+
timeoutMs: remaining(),
|
|
971
1200
|
active,
|
|
972
1201
|
errors: output.errors,
|
|
973
1202
|
...(runtime.runSmartsearch ? { runSmartsearch: runtime.runSmartsearch } : {}),
|
|
974
1203
|
}, normalized);
|
|
1204
|
+
remaining();
|
|
975
1205
|
const enriched = academic.normalized;
|
|
976
1206
|
output.retrieval_method.push(normalized.method);
|
|
977
1207
|
output.retrieval_method.push(...academic.retrievalMethods);
|
|
@@ -981,7 +1211,7 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
981
1211
|
output.outbound_links = Array.isArray(enriched.outbound_links) ? enriched.outbound_links : [];
|
|
982
1212
|
output.partial = enriched.partial;
|
|
983
1213
|
output.errors = enriched.errors;
|
|
984
|
-
output.assets = discoverAssets(options.url, active.content, output.post, output.outbound_links);
|
|
1214
|
+
output.assets = discoverAssets(options.url, normalized.assetHtml ?? active.content, output.post, output.outbound_links, active.method === "wayback_archive");
|
|
985
1215
|
if (isNaverCafeUnavailableFallback(provider.id, output) || isNaverMapSearchUnavailable(provider.id, output) || isThreadsJoinShellUnavailable(provider.id, output) || isInstagramUnavailable(provider.id, output) || isDcinsideUnavailable(provider.id, output) || isTelegramUnavailable(provider.id, output) || isXUnavailable(provider.id, output)) {
|
|
986
1216
|
output.assets = [];
|
|
987
1217
|
}
|
|
@@ -995,6 +1225,24 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
995
1225
|
output.assets = [];
|
|
996
1226
|
output.download = null;
|
|
997
1227
|
}
|
|
1228
|
+
if (options.exportComments) {
|
|
1229
|
+
try {
|
|
1230
|
+
output.comment_export = exportComments(output);
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
output.partial = true;
|
|
1234
|
+
output.errors.push({ category: "unavailable", code: "comment_export_failed", message: "Private comment export failed or exceeded its 5000-row/8 MiB bound; no export artifact is claimed" });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (options.exportBody) {
|
|
1238
|
+
try {
|
|
1239
|
+
output.body_export = exportBody(output);
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
output.partial = true;
|
|
1243
|
+
output.errors.push({ category: "unavailable", code: "body_export_failed", message: "Private body export failed, had no readable body, or exceeded its 1 MiB bound; no export artifact is claimed" });
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
998
1246
|
output.pipeline.normalize = buildNormalizeStage({ provider, partial: output.partial });
|
|
999
1247
|
output.assessment = assessSmartfetchOutput({
|
|
1000
1248
|
url: options.url,
|
|
@@ -1023,13 +1271,18 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
1023
1271
|
output.outbound_links = dedupeUrls(cleanLinks(dedupeUrls(Array.isArray(output.outbound_links) ? output.outbound_links : [])));
|
|
1024
1272
|
output.assets = output.assets.filter((asset, index, list) => asset.url && list.findIndex((item) => item.url === asset.url) === index);
|
|
1025
1273
|
attachResearchHandoff(output);
|
|
1274
|
+
const configuredOutputBudget = options.outputBudgetConfig ?? configValue("fetch.outputBudget");
|
|
1026
1275
|
const budgetDecision = resolveSmartfetchOutputBudget({
|
|
1027
1276
|
outputBudget: options.outputBudgetPreset,
|
|
1028
1277
|
preferredBudgetChars: options.preferredBudgetChars,
|
|
1029
1278
|
headroomTokens: options.headroomTokens,
|
|
1030
1279
|
contextMode: options.contextMode,
|
|
1280
|
+
configuredOutputBudget,
|
|
1031
1281
|
});
|
|
1032
|
-
const
|
|
1282
|
+
const budgetPolicy = budgetDecision.source === "settings"
|
|
1283
|
+
? resolveSmartfetchBudgetPolicy(configuredOutputBudget)
|
|
1284
|
+
: resolveSmartfetchBudgetPolicy(budgetDecision.preset);
|
|
1285
|
+
const projectedOutput = projectSmartfetchOutput(output, budgetPolicy);
|
|
1033
1286
|
if (options.strictSchema !== false) {
|
|
1034
1287
|
const error = validateOutput(projectedOutput);
|
|
1035
1288
|
if (error) {
|
|
@@ -1049,13 +1302,35 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
1049
1302
|
}
|
|
1050
1303
|
return projectedOutput;
|
|
1051
1304
|
}
|
|
1305
|
+
function renderCommentBody(comment) {
|
|
1306
|
+
if (!comment)
|
|
1307
|
+
return "";
|
|
1308
|
+
const images = Array.isArray(comment.image_urls) ? comment.image_urls.filter((url) => typeof url === "string") : [];
|
|
1309
|
+
return [asString(comment.body) || asString(comment.text), ...images].filter(Boolean).join("\n").trim();
|
|
1310
|
+
}
|
|
1052
1311
|
export function renderSmartfetch(output, format) {
|
|
1053
1312
|
const projected = output.budget ? output : projectSmartfetchOutput(output);
|
|
1054
1313
|
const assessment = projected.assessment || createAssessment();
|
|
1055
1314
|
const post = projected.post || {};
|
|
1056
1315
|
const title = String(post?.title || "").trim();
|
|
1057
1316
|
const text = String(post?.text || post?.selftext || "").trim();
|
|
1317
|
+
const exported = projected.comment_export;
|
|
1318
|
+
const exportReceipt = exported ? `Comment export: ${exported.path}\ncomments=${exported.comments_count} complete=${exported.complete} bytes=${exported.bytes} sha256=${exported.sha256}` : "";
|
|
1319
|
+
const bodyExported = projected.body_export;
|
|
1320
|
+
const bodyExportReceipt = bodyExported ? `Body export: ${bodyExported.path}\nchars=${bodyExported.chars} complete=${bodyExported.complete} bytes=${bodyExported.bytes} sha256=${bodyExported.sha256}` : "";
|
|
1058
1321
|
if (format === "text") {
|
|
1322
|
+
// Explicit full output must not hide projected comments behind the compact text view.
|
|
1323
|
+
// The projection policy still bounds each field and the total response.
|
|
1324
|
+
const fullPolicy = projected.budget?.policy === "smartfetch.full" ? resolveSmartfetchBudgetPolicy("full") : null;
|
|
1325
|
+
const textLimits = fullPolicy ? {
|
|
1326
|
+
...TEXT_RENDER_LIMITS,
|
|
1327
|
+
bodyChars: fullPolicy.maxPostTextChars,
|
|
1328
|
+
itemChars: fullPolicy.maxItemTextChars,
|
|
1329
|
+
results: fullPolicy.maxThreadItems,
|
|
1330
|
+
comments: fullPolicy.maxCommentItems,
|
|
1331
|
+
links: fullPolicy.maxOutboundLinks,
|
|
1332
|
+
assets: fullPolicy.maxAssets,
|
|
1333
|
+
} : TEXT_RENDER_LIMITS;
|
|
1059
1334
|
const assessmentLines = [
|
|
1060
1335
|
`confidence=${assessment.confidence}`,
|
|
1061
1336
|
assessment.blocked_reason ? `blocked=${assessment.blocked_reason}` : "",
|
|
@@ -1072,12 +1347,13 @@ export function renderSmartfetch(output, format) {
|
|
|
1072
1347
|
post?.availability ? `availability=${post.availability}` : "",
|
|
1073
1348
|
assessmentLines,
|
|
1074
1349
|
].filter(Boolean).join(" ");
|
|
1075
|
-
const resultItems = Array.isArray(projected.thread) ? limitedItems(projected.thread,
|
|
1076
|
-
const commentItems = Array.isArray(projected.comments) ? limitedItems(projected.comments,
|
|
1077
|
-
const hasHandoff = Boolean(assessment.recommended_handoff || assessment.auth_required_likely || assessment.interactive_flow_likely
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
1350
|
+
const resultItems = Array.isArray(projected.thread) ? limitedItems(projected.thread, textLimits.results) : { visible: [], omitted: 0 };
|
|
1351
|
+
const commentItems = Array.isArray(projected.comments) ? limitedItems(projected.comments, textLimits.comments) : { visible: [], omitted: 0 };
|
|
1352
|
+
const hasHandoff = Boolean(assessment.recommended_handoff || assessment.auth_required_likely || assessment.interactive_flow_likely
|
|
1353
|
+
|| (assessment.continuation && (assessment.continuation.status !== "unavailable" || assessment.confidence === "low")));
|
|
1354
|
+
const linkItems = limitedItems(cleanLinks(projected.outbound_links || []), hasHandoff ? TEXT_RENDER_LIMITS.handoffLinks : textLimits.links);
|
|
1355
|
+
const assetItems = Array.isArray(projected.assets) ? limitedItems(projected.assets, textLimits.assets) : { visible: [], omitted: 0 };
|
|
1356
|
+
const textBody = compactText(text, hasHandoff ? TEXT_RENDER_LIMITS.handoffBodyChars : textLimits.bodyChars);
|
|
1081
1357
|
const renderBudget = [
|
|
1082
1358
|
textBody.truncated ? `text_chars_omitted=${textBody.omittedChars}` : "",
|
|
1083
1359
|
resultItems.omitted ? `results_omitted=${resultItems.omitted}` : "",
|
|
@@ -1089,21 +1365,22 @@ export function renderSmartfetch(output, format) {
|
|
|
1089
1365
|
const title = String(item?.title || "").trim();
|
|
1090
1366
|
const price = String(item?.price || "").trim();
|
|
1091
1367
|
const url = String(item?.url || "").trim();
|
|
1092
|
-
return compactText([title, price, url].filter(Boolean).join(" — "),
|
|
1368
|
+
return compactText([title, price, url].filter(Boolean).join(" — "), textLimits.itemChars).text;
|
|
1093
1369
|
}).filter(Boolean);
|
|
1094
1370
|
const comments = commentItems.visible.map((comment) => {
|
|
1095
|
-
const body =
|
|
1371
|
+
const body = renderCommentBody(comment);
|
|
1096
1372
|
const author = String(comment?.author || "unknown");
|
|
1097
|
-
return body ? compactText(`${author}: ${body}`,
|
|
1373
|
+
return body ? compactText(`${author}: ${body}`, textLimits.itemChars).text : "";
|
|
1098
1374
|
}).filter(Boolean);
|
|
1099
1375
|
const assets = assetItems.visible.map((asset) => `${asset.kind}: ${asset.download_url}`);
|
|
1100
1376
|
const download = projected.download ? `download: ${projected.download.download_url}` : "";
|
|
1101
1377
|
const handoff = assessment.recommended_handoff
|
|
1102
|
-
? `handoff: ${assessment.recommended_handoff.tool} — ${assessment.recommended_handoff.reason}
|
|
1378
|
+
? `handoff: ${assessment.recommended_handoff.tool} — ${assessment.recommended_handoff.reason}`
|
|
1103
1379
|
: "";
|
|
1380
|
+
const continuation = assessment.continuation ? `continuation: ${assessment.continuation.status} — ${assessment.continuation.reason}` : "";
|
|
1104
1381
|
const budget = projected.budget?.truncated ? `budget: truncated fields=${Object.keys(projected.budget.fields).join(",")}` : "";
|
|
1105
|
-
const compactBudget = renderBudget ? `text_view: compact renderer omitted ${renderBudget}` : "";
|
|
1106
|
-
return [title, meta, textBody.text, results.join("\n"), comments.join("\n"), linkItems.visible.join("\n"), download, assets.join("\n"), handoff, budget, compactBudget].filter(Boolean).join("\n\n").trim();
|
|
1382
|
+
const compactBudget = renderBudget ? `text_view: ${fullPolicy ? "full" : "compact"} renderer omitted ${renderBudget}` : "";
|
|
1383
|
+
return [title, meta, exportReceipt, bodyExportReceipt, textBody.text, results.join("\n"), comments.join("\n"), linkItems.visible.join("\n"), download, assets.join("\n"), handoff, continuation, budget, compactBudget].filter(Boolean).join("\n\n").trim();
|
|
1107
1384
|
}
|
|
1108
1385
|
const parts = [];
|
|
1109
1386
|
const url = String(post?.url || projected.url || "").trim();
|
|
@@ -1126,6 +1403,10 @@ export function renderSmartfetch(output, format) {
|
|
|
1126
1403
|
parts.push(`# ${title}`);
|
|
1127
1404
|
if (url)
|
|
1128
1405
|
parts.push(`Source: ${url}`);
|
|
1406
|
+
if (exportReceipt)
|
|
1407
|
+
parts.push(exportReceipt);
|
|
1408
|
+
if (bodyExportReceipt)
|
|
1409
|
+
parts.push(bodyExportReceipt);
|
|
1129
1410
|
parts.push(`Confidence: ${assessment.confidence}`);
|
|
1130
1411
|
if (assessment.blocked_reason)
|
|
1131
1412
|
parts.push(`Blocked reason: ${assessment.blocked_reason}`);
|
|
@@ -1140,7 +1421,7 @@ export function renderSmartfetch(output, format) {
|
|
|
1140
1421
|
if (Array.isArray(projected.comments) && projected.comments.length > 0) {
|
|
1141
1422
|
const comments = projected.comments.map((comment) => {
|
|
1142
1423
|
const author = String(comment?.author || "unknown");
|
|
1143
|
-
const body =
|
|
1424
|
+
const body = renderCommentBody(comment);
|
|
1144
1425
|
return body ? `- ${author}: ${body}` : "";
|
|
1145
1426
|
}).filter(Boolean);
|
|
1146
1427
|
if (comments.length > 0)
|
|
@@ -1175,8 +1456,11 @@ export function renderSmartfetch(output, format) {
|
|
|
1175
1456
|
parts.push(`## Assets\n\n${assets.join("\n")}`);
|
|
1176
1457
|
if (assessment.recommended_handoff) {
|
|
1177
1458
|
const handoff = assessment.recommended_handoff;
|
|
1178
|
-
|
|
1179
|
-
|
|
1459
|
+
parts.push(`## Recommended handoff\n\n- tool: ${handoff.tool}\n- mode: ${handoff.mode}\n- url: ${handoff.url}\n- reason: ${handoff.reason}\n- goal: ${handoff.goal}`);
|
|
1460
|
+
}
|
|
1461
|
+
if (assessment.continuation) {
|
|
1462
|
+
const continuation = assessment.continuation;
|
|
1463
|
+
parts.push(`## Continuation\n\n- status: ${continuation.status}\n- reason: ${continuation.reason}\n- url: ${continuation.url}`);
|
|
1180
1464
|
}
|
|
1181
1465
|
if (projected.budget?.truncated) {
|
|
1182
1466
|
const fields = Object.entries(projected.budget.fields).map(([field, item]) => {
|
|
@@ -1201,12 +1485,19 @@ export async function renderSmartfetchUrl(url, format, timeoutMs) {
|
|
|
1201
1485
|
return renderSmartfetch(output, format);
|
|
1202
1486
|
}
|
|
1203
1487
|
async function runPlaywrightFetchWithRuntime(url, timeoutMs, target, runtime, profileDir, waitForSelector) {
|
|
1488
|
+
const nativeUrl = target === "naver_blog" ? naverBlogMobilePostUrl(url) : target === "naver_cafe_post" ? naverCafeMobilePostUrl(url) : null;
|
|
1489
|
+
if (nativeUrl) {
|
|
1490
|
+
url = nativeUrl;
|
|
1491
|
+
if (!waitForSelector && target === "naver_cafe_post")
|
|
1492
|
+
waitForSelector = NAVER_CAFE_READY_SELECTOR;
|
|
1493
|
+
}
|
|
1204
1494
|
if (!runtime.runBrowserSession)
|
|
1205
1495
|
return runPlaywrightFetch(url, timeoutMs, target, profileDir, waitForSelector);
|
|
1206
1496
|
const extraHeaders = browserFetchHeaders(url);
|
|
1207
1497
|
const result = await runtime.runBrowserSession({
|
|
1208
1498
|
url,
|
|
1209
1499
|
timeoutMs,
|
|
1500
|
+
...(!profileDir && configText("fetch.browserCdpUrl") ? { cdpUrl: configText("fetch.browserCdpUrl") } : {}),
|
|
1210
1501
|
allowPrivateHosts: allowPrivateHosts(),
|
|
1211
1502
|
waitUntil: choosePlaywrightWaitUntil(target, url),
|
|
1212
1503
|
extraHeaders,
|