smart-web-mcp 0.44.1 → 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 +342 -0
- package/README.md +33 -568
- 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 +28 -11
- 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 +30 -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 +33 -8
- 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 +7 -1
- package/dist/smartfetch.js +353 -76
- 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 +2 -0
- package/dist/smartsearch.js +506 -84
- 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,5 +1,5 @@
|
|
|
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
5
|
import { configText, configValue } from "./settings.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;
|
|
@@ -584,28 +667,32 @@ function isDocumentLikeAsset(asset) {
|
|
|
584
667
|
return asset?.kind === "document" || asset?.kind === "archive" || asset?.kind === "attachment";
|
|
585
668
|
}
|
|
586
669
|
export async function runSmartfetch(options, runtime = {}) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (coalesceKey) {
|
|
593
|
-
const inflight = inflightSmartfetch.get(coalesceKey);
|
|
594
|
-
if (inflight)
|
|
595
|
-
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);
|
|
596
675
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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);
|
|
601
688
|
}
|
|
602
|
-
return promise;
|
|
603
689
|
}
|
|
604
|
-
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.
|
|
605
693
|
const requestedTarget = options.target || "auto";
|
|
606
|
-
const target = inferTarget(options.url, requestedTarget);
|
|
607
|
-
const
|
|
608
|
-
const output = baseOutput(options.url, target);
|
|
694
|
+
const target = (requestedTarget === "auto" ? nativeNaverTarget(options.url) : undefined) || inferTarget(options.url, requestedTarget);
|
|
695
|
+
const remaining = () => remainingFetchTime(deadlineAt);
|
|
609
696
|
output.pipeline.resolve = buildResolveStage({
|
|
610
697
|
url: options.url,
|
|
611
698
|
requestedTarget,
|
|
@@ -616,6 +703,7 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
616
703
|
const profileName = options.profile || configText("fetch.browserProfile");
|
|
617
704
|
const profileDir = profileName ? resolveProfileDir(profileName) : undefined;
|
|
618
705
|
const validation = await validateOutboundUrl(options.url, { allowPrivateHosts: allowPrivateHosts() });
|
|
706
|
+
remaining();
|
|
619
707
|
if (!validation.ok) {
|
|
620
708
|
return attachResearchHandoff({
|
|
621
709
|
...output,
|
|
@@ -730,16 +818,27 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
730
818
|
}
|
|
731
819
|
}
|
|
732
820
|
let active;
|
|
821
|
+
let expectedPostUrl = nativeNaverTarget(options.url) === target ? options.url : undefined;
|
|
733
822
|
if (acquirePlan.lane === "browser") {
|
|
734
|
-
|
|
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);
|
|
735
833
|
browserAttempt.attempted = true;
|
|
736
834
|
browserAttempt.ok = active.ok;
|
|
737
835
|
browserAttempt.method = active.method;
|
|
738
836
|
output.evidence.playwright_attempted = true;
|
|
739
|
-
output.evidence.playwright_success = active.ok;
|
|
740
837
|
output.retrieval_method.push(active.method);
|
|
741
838
|
if (active.error)
|
|
742
839
|
output.errors.push(active.error);
|
|
840
|
+
remaining();
|
|
841
|
+
output.evidence.playwright_success = active.ok;
|
|
743
842
|
}
|
|
744
843
|
else if (acquirePlan.lane === "seed") {
|
|
745
844
|
active = {
|
|
@@ -761,65 +860,156 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
761
860
|
output.retrieval_method.push(active.method);
|
|
762
861
|
}
|
|
763
862
|
else {
|
|
764
|
-
|
|
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
|
+
}
|
|
765
876
|
directAttempt.attempted = true;
|
|
766
877
|
directAttempt.ok = active.ok;
|
|
767
878
|
directAttempt.method = active.method;
|
|
768
879
|
output.evidence.impit_attempted = true;
|
|
769
|
-
output.evidence.impit_success = active.ok;
|
|
770
880
|
output.retrieval_method.push(active.method);
|
|
771
881
|
if (active.error)
|
|
772
882
|
output.errors.push(active.error);
|
|
883
|
+
remaining();
|
|
884
|
+
output.evidence.impit_success = active.ok;
|
|
773
885
|
const redirectUrl = active.final_url && active.final_url !== options.url ? active.final_url : "";
|
|
774
886
|
const redirectTarget = redirectUrl
|
|
775
|
-
? inferTarget(redirectUrl, "auto")
|
|
887
|
+
? nativeNaverTarget(redirectUrl) || inferTarget(redirectUrl, "auto")
|
|
776
888
|
: target;
|
|
777
889
|
const redirectProvider = redirectUrl
|
|
778
890
|
? resolveSmartfetchProvider(redirectUrl, redirectTarget)
|
|
779
891
|
: plannedProvider;
|
|
780
892
|
const needsRedirectBrowserUpgrade = Boolean(redirectUrl)
|
|
781
893
|
&& (redirectProvider.capabilities.includes("requires_browser") || redirectProvider.capabilities.includes("interactive_surface"));
|
|
782
|
-
|
|
783
|
-
|
|
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);
|
|
784
905
|
browserAttempt.attempted = true;
|
|
785
906
|
browserAttempt.ok = browserResult.ok;
|
|
786
907
|
browserAttempt.method = browserResult.method;
|
|
787
908
|
browserAttempt.reason = `redirected final provider:${redirectProvider.id} requires browser retrieval`;
|
|
788
909
|
output.evidence.playwright_attempted = true;
|
|
789
|
-
output.evidence.playwright_success = browserResult.ok;
|
|
790
910
|
output.retrieval_method.push(browserResult.method);
|
|
791
911
|
if (browserResult.error)
|
|
792
912
|
output.errors.push(browserResult.error);
|
|
913
|
+
remaining();
|
|
914
|
+
output.evidence.playwright_success = browserResult.ok;
|
|
793
915
|
effectiveLane = "browser";
|
|
794
916
|
effectiveReason = `redirected final provider:${redirectProvider.id} requires browser retrieval`;
|
|
795
917
|
redirectUpgrade = true;
|
|
796
|
-
if (browserResult.ok)
|
|
918
|
+
if (browserResult.ok || expectedPostUrl || hasTerminalSafetyBlock([browserResult.error]))
|
|
797
919
|
active = browserResult;
|
|
798
920
|
}
|
|
799
|
-
else if (needsBotChallengeBrowserUpgrade(active)) {
|
|
800
|
-
//
|
|
801
|
-
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);
|
|
802
924
|
browserAttempt.attempted = true;
|
|
803
925
|
browserAttempt.ok = browserResult.ok;
|
|
804
926
|
browserAttempt.method = browserResult.method;
|
|
805
|
-
browserAttempt.reason = "impit returned bot-challenge interstitial — upgraded to browser";
|
|
927
|
+
browserAttempt.reason = operatorRecoveryReason(active) || "impit returned bot-challenge interstitial — upgraded to browser";
|
|
806
928
|
output.evidence.playwright_attempted = true;
|
|
807
|
-
output.evidence.playwright_success = browserResult.ok;
|
|
808
929
|
output.retrieval_method.push(browserResult.method);
|
|
809
930
|
if (browserResult.error)
|
|
810
931
|
output.errors.push(browserResult.error);
|
|
932
|
+
remaining();
|
|
933
|
+
output.evidence.playwright_success = browserResult.ok;
|
|
811
934
|
effectiveLane = "browser";
|
|
812
|
-
effectiveReason =
|
|
935
|
+
effectiveReason = browserAttempt.reason;
|
|
813
936
|
if (browserResult.ok)
|
|
814
937
|
active = browserResult;
|
|
815
938
|
}
|
|
816
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
|
+
}
|
|
817
964
|
const archiveTarget = inferTarget(active.final_url || options.url, "auto");
|
|
818
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
|
+
}
|
|
819
1005
|
const allowArchiveFallback = acquirePlan.allowArchiveFallback
|
|
1006
|
+
&& !suppressRetries
|
|
1007
|
+
&& !lockedBlindPreview
|
|
820
1008
|
&& effectiveLane === "direct"
|
|
821
1009
|
&& Boolean(archiveProvider.policy.allowArchiveFallback);
|
|
822
1010
|
const allowJinaReader = acquirePlan.allowJinaReader
|
|
1011
|
+
&& !suppressRetries
|
|
1012
|
+
&& !lockedBlindPreview
|
|
823
1013
|
&& effectiveLane === "direct"
|
|
824
1014
|
&& Boolean(archiveProvider.policy.allowJinaReader);
|
|
825
1015
|
if (!allowJinaReader) {
|
|
@@ -832,9 +1022,10 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
832
1022
|
? "archive fallback was not eligible for the effective provider/lane"
|
|
833
1023
|
: "archive fallback was not allowed by plan, privacy profile, or settings";
|
|
834
1024
|
}
|
|
835
|
-
if (allowJinaReader) {
|
|
1025
|
+
if (allowJinaReader && effectiveLane === "direct") {
|
|
836
1026
|
const beforeMethod = active.method;
|
|
837
|
-
const relayed = await maybeUseJinaReader(active.final_url || options.url, active,
|
|
1027
|
+
const relayed = await maybeUseJinaReader(active.final_url || options.url, active, remaining());
|
|
1028
|
+
remaining();
|
|
838
1029
|
active = relayed.active;
|
|
839
1030
|
output.errors.push(...relayed.errors);
|
|
840
1031
|
const jinaErrors = relayed.errors.filter((error) => error.code.startsWith("jina_reader"));
|
|
@@ -853,9 +1044,10 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
853
1044
|
if (jinaSucceeded)
|
|
854
1045
|
output.retrieval_method.push(active.method);
|
|
855
1046
|
}
|
|
856
|
-
if (allowArchiveFallback) {
|
|
1047
|
+
if (allowArchiveFallback && effectiveLane === "direct") {
|
|
857
1048
|
const beforeMethod = active.method;
|
|
858
|
-
const archived = await maybeUseArchiveFallback(active.final_url || options.url, active,
|
|
1049
|
+
const archived = await maybeUseArchiveFallback(active.final_url || options.url, active, remaining());
|
|
1050
|
+
remaining();
|
|
859
1051
|
active = archived.active;
|
|
860
1052
|
output.errors.push(...archived.errors);
|
|
861
1053
|
const archiveErrors = archived.errors.filter((error) => /archive|wayback/i.test(error.code));
|
|
@@ -877,7 +1069,8 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
877
1069
|
}
|
|
878
1070
|
}
|
|
879
1071
|
if (effectiveLane === "direct" && (needsDynamicCrawl(active) || needsSpaBrowserFallback(active))) {
|
|
880
|
-
const scraped = await maybeUseAdaptiveScraper(active.final_url || options.url, active,
|
|
1072
|
+
const scraped = await maybeUseAdaptiveScraper(active.final_url || options.url, active, remaining());
|
|
1073
|
+
remaining();
|
|
881
1074
|
output.errors.push(...scraped.errors);
|
|
882
1075
|
adaptiveScraperAttempt.attempted = scraped.attempted;
|
|
883
1076
|
adaptiveScraperAttempt.ok = scraped.succeeded;
|
|
@@ -900,24 +1093,26 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
900
1093
|
// SPA auto-fallback: after cheap alternatives (Jina, archive, adaptive scraper) didn't help,
|
|
901
1094
|
// if the direct fetch result looks like an SPA shell, upgrade to Playwright.
|
|
902
1095
|
// This must come AFTER Jina/archive so those cheap paths get a chance first.
|
|
903
|
-
if (effectiveLane === "direct" && needsSpaBrowserFallback(active)) {
|
|
904
|
-
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);
|
|
905
1098
|
browserAttempt.attempted = true;
|
|
906
1099
|
browserAttempt.ok = browserResult.ok;
|
|
907
1100
|
browserAttempt.method = browserResult.method;
|
|
908
1101
|
browserAttempt.reason = "direct fetch returned SPA shell — auto-fallback to browser for JS rendering";
|
|
909
1102
|
output.evidence.playwright_attempted = true;
|
|
910
|
-
output.evidence.playwright_success = browserResult.ok;
|
|
911
1103
|
output.retrieval_method.push(browserResult.method);
|
|
912
1104
|
if (browserResult.error)
|
|
913
1105
|
output.errors.push(browserResult.error);
|
|
1106
|
+
remaining();
|
|
1107
|
+
output.evidence.playwright_success = browserResult.ok;
|
|
914
1108
|
effectiveLane = "browser";
|
|
915
1109
|
effectiveReason = "direct fetch returned SPA shell — auto-fallback to browser for JS rendering";
|
|
916
1110
|
if (browserResult.ok)
|
|
917
1111
|
active = browserResult;
|
|
918
1112
|
}
|
|
919
|
-
let effectiveTarget = target;
|
|
920
|
-
|
|
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) {
|
|
921
1116
|
const inferred = inferTarget(active.final_url || options.url, "auto");
|
|
922
1117
|
if (inferred !== effectiveTarget) {
|
|
923
1118
|
effectiveTarget = inferred;
|
|
@@ -925,6 +1120,8 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
925
1120
|
output.risk_level = riskLevelForTarget(inferred);
|
|
926
1121
|
}
|
|
927
1122
|
}
|
|
1123
|
+
output.target = effectiveTarget;
|
|
1124
|
+
output.risk_level = riskLevelForTarget(effectiveTarget);
|
|
928
1125
|
output.pipeline.resolve = buildResolveStage({
|
|
929
1126
|
url: options.url,
|
|
930
1127
|
requestedTarget,
|
|
@@ -953,17 +1150,39 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
953
1150
|
],
|
|
954
1151
|
};
|
|
955
1152
|
const providerUrl = active.final_url || options.url;
|
|
956
|
-
|
|
1153
|
+
const exactNaverProviderUrl = expectedPostUrl || (target === "naver_blog" || target === "naver_cafe_post" ? options.url : undefined);
|
|
1154
|
+
let provider = resolveSmartfetchProvider(exactNaverProviderUrl || providerUrl, effectiveTarget);
|
|
957
1155
|
if (provider.id === "generic" && providerUrl !== options.url) {
|
|
958
1156
|
const originalProvider = resolveSmartfetchProvider(options.url, effectiveTarget);
|
|
959
1157
|
if (originalProvider.id !== "generic")
|
|
960
1158
|
provider = originalProvider;
|
|
961
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
|
+
}
|
|
962
1172
|
const normalized = await provider.normalize({
|
|
963
1173
|
url: options.url,
|
|
964
1174
|
resolvedUrl: providerUrl,
|
|
1175
|
+
...(expectedPostUrl ? { expectedPostUrl } : {}),
|
|
965
1176
|
target: effectiveTarget,
|
|
966
|
-
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
|
+
},
|
|
967
1186
|
active,
|
|
968
1187
|
errors: output.errors,
|
|
969
1188
|
runBrowserSession: runtime.runBrowserSession || runBrowserSession,
|
|
@@ -972,15 +1191,17 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
972
1191
|
runCoupangSearch: runtime.runCoupangSearch || runCoupang,
|
|
973
1192
|
...(options.includeMarkdown ? { includeMarkdown: true } : {}),
|
|
974
1193
|
});
|
|
1194
|
+
remaining();
|
|
975
1195
|
const academic = await maybeUseAcademicFallback({
|
|
976
1196
|
url: options.url,
|
|
977
1197
|
resolvedUrl: providerUrl,
|
|
978
1198
|
target: effectiveTarget,
|
|
979
|
-
timeoutMs,
|
|
1199
|
+
timeoutMs: remaining(),
|
|
980
1200
|
active,
|
|
981
1201
|
errors: output.errors,
|
|
982
1202
|
...(runtime.runSmartsearch ? { runSmartsearch: runtime.runSmartsearch } : {}),
|
|
983
1203
|
}, normalized);
|
|
1204
|
+
remaining();
|
|
984
1205
|
const enriched = academic.normalized;
|
|
985
1206
|
output.retrieval_method.push(normalized.method);
|
|
986
1207
|
output.retrieval_method.push(...academic.retrievalMethods);
|
|
@@ -990,7 +1211,7 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
990
1211
|
output.outbound_links = Array.isArray(enriched.outbound_links) ? enriched.outbound_links : [];
|
|
991
1212
|
output.partial = enriched.partial;
|
|
992
1213
|
output.errors = enriched.errors;
|
|
993
|
-
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");
|
|
994
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)) {
|
|
995
1216
|
output.assets = [];
|
|
996
1217
|
}
|
|
@@ -1004,6 +1225,24 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
1004
1225
|
output.assets = [];
|
|
1005
1226
|
output.download = null;
|
|
1006
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
|
+
}
|
|
1007
1246
|
output.pipeline.normalize = buildNormalizeStage({ provider, partial: output.partial });
|
|
1008
1247
|
output.assessment = assessSmartfetchOutput({
|
|
1009
1248
|
url: options.url,
|
|
@@ -1063,13 +1302,35 @@ async function runSmartfetchInner(options, runtime = {}) {
|
|
|
1063
1302
|
}
|
|
1064
1303
|
return projectedOutput;
|
|
1065
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
|
+
}
|
|
1066
1311
|
export function renderSmartfetch(output, format) {
|
|
1067
1312
|
const projected = output.budget ? output : projectSmartfetchOutput(output);
|
|
1068
1313
|
const assessment = projected.assessment || createAssessment();
|
|
1069
1314
|
const post = projected.post || {};
|
|
1070
1315
|
const title = String(post?.title || "").trim();
|
|
1071
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}` : "";
|
|
1072
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;
|
|
1073
1334
|
const assessmentLines = [
|
|
1074
1335
|
`confidence=${assessment.confidence}`,
|
|
1075
1336
|
assessment.blocked_reason ? `blocked=${assessment.blocked_reason}` : "",
|
|
@@ -1086,12 +1347,13 @@ export function renderSmartfetch(output, format) {
|
|
|
1086
1347
|
post?.availability ? `availability=${post.availability}` : "",
|
|
1087
1348
|
assessmentLines,
|
|
1088
1349
|
].filter(Boolean).join(" ");
|
|
1089
|
-
const resultItems = Array.isArray(projected.thread) ? limitedItems(projected.thread,
|
|
1090
|
-
const commentItems = Array.isArray(projected.comments) ? limitedItems(projected.comments,
|
|
1091
|
-
const hasHandoff = Boolean(assessment.recommended_handoff || assessment.auth_required_likely || assessment.interactive_flow_likely
|
|
1092
|
-
|
|
1093
|
-
const
|
|
1094
|
-
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);
|
|
1095
1357
|
const renderBudget = [
|
|
1096
1358
|
textBody.truncated ? `text_chars_omitted=${textBody.omittedChars}` : "",
|
|
1097
1359
|
resultItems.omitted ? `results_omitted=${resultItems.omitted}` : "",
|
|
@@ -1103,21 +1365,22 @@ export function renderSmartfetch(output, format) {
|
|
|
1103
1365
|
const title = String(item?.title || "").trim();
|
|
1104
1366
|
const price = String(item?.price || "").trim();
|
|
1105
1367
|
const url = String(item?.url || "").trim();
|
|
1106
|
-
return compactText([title, price, url].filter(Boolean).join(" — "),
|
|
1368
|
+
return compactText([title, price, url].filter(Boolean).join(" — "), textLimits.itemChars).text;
|
|
1107
1369
|
}).filter(Boolean);
|
|
1108
1370
|
const comments = commentItems.visible.map((comment) => {
|
|
1109
|
-
const body =
|
|
1371
|
+
const body = renderCommentBody(comment);
|
|
1110
1372
|
const author = String(comment?.author || "unknown");
|
|
1111
|
-
return body ? compactText(`${author}: ${body}`,
|
|
1373
|
+
return body ? compactText(`${author}: ${body}`, textLimits.itemChars).text : "";
|
|
1112
1374
|
}).filter(Boolean);
|
|
1113
1375
|
const assets = assetItems.visible.map((asset) => `${asset.kind}: ${asset.download_url}`);
|
|
1114
1376
|
const download = projected.download ? `download: ${projected.download.download_url}` : "";
|
|
1115
1377
|
const handoff = assessment.recommended_handoff
|
|
1116
|
-
? `handoff: ${assessment.recommended_handoff.tool} — ${assessment.recommended_handoff.reason}
|
|
1378
|
+
? `handoff: ${assessment.recommended_handoff.tool} — ${assessment.recommended_handoff.reason}`
|
|
1117
1379
|
: "";
|
|
1380
|
+
const continuation = assessment.continuation ? `continuation: ${assessment.continuation.status} — ${assessment.continuation.reason}` : "";
|
|
1118
1381
|
const budget = projected.budget?.truncated ? `budget: truncated fields=${Object.keys(projected.budget.fields).join(",")}` : "";
|
|
1119
|
-
const compactBudget = renderBudget ? `text_view: compact renderer omitted ${renderBudget}` : "";
|
|
1120
|
-
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();
|
|
1121
1384
|
}
|
|
1122
1385
|
const parts = [];
|
|
1123
1386
|
const url = String(post?.url || projected.url || "").trim();
|
|
@@ -1140,6 +1403,10 @@ export function renderSmartfetch(output, format) {
|
|
|
1140
1403
|
parts.push(`# ${title}`);
|
|
1141
1404
|
if (url)
|
|
1142
1405
|
parts.push(`Source: ${url}`);
|
|
1406
|
+
if (exportReceipt)
|
|
1407
|
+
parts.push(exportReceipt);
|
|
1408
|
+
if (bodyExportReceipt)
|
|
1409
|
+
parts.push(bodyExportReceipt);
|
|
1143
1410
|
parts.push(`Confidence: ${assessment.confidence}`);
|
|
1144
1411
|
if (assessment.blocked_reason)
|
|
1145
1412
|
parts.push(`Blocked reason: ${assessment.blocked_reason}`);
|
|
@@ -1154,7 +1421,7 @@ export function renderSmartfetch(output, format) {
|
|
|
1154
1421
|
if (Array.isArray(projected.comments) && projected.comments.length > 0) {
|
|
1155
1422
|
const comments = projected.comments.map((comment) => {
|
|
1156
1423
|
const author = String(comment?.author || "unknown");
|
|
1157
|
-
const body =
|
|
1424
|
+
const body = renderCommentBody(comment);
|
|
1158
1425
|
return body ? `- ${author}: ${body}` : "";
|
|
1159
1426
|
}).filter(Boolean);
|
|
1160
1427
|
if (comments.length > 0)
|
|
@@ -1189,8 +1456,11 @@ export function renderSmartfetch(output, format) {
|
|
|
1189
1456
|
parts.push(`## Assets\n\n${assets.join("\n")}`);
|
|
1190
1457
|
if (assessment.recommended_handoff) {
|
|
1191
1458
|
const handoff = assessment.recommended_handoff;
|
|
1192
|
-
|
|
1193
|
-
|
|
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}`);
|
|
1194
1464
|
}
|
|
1195
1465
|
if (projected.budget?.truncated) {
|
|
1196
1466
|
const fields = Object.entries(projected.budget.fields).map(([field, item]) => {
|
|
@@ -1215,12 +1485,19 @@ export async function renderSmartfetchUrl(url, format, timeoutMs) {
|
|
|
1215
1485
|
return renderSmartfetch(output, format);
|
|
1216
1486
|
}
|
|
1217
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
|
+
}
|
|
1218
1494
|
if (!runtime.runBrowserSession)
|
|
1219
1495
|
return runPlaywrightFetch(url, timeoutMs, target, profileDir, waitForSelector);
|
|
1220
1496
|
const extraHeaders = browserFetchHeaders(url);
|
|
1221
1497
|
const result = await runtime.runBrowserSession({
|
|
1222
1498
|
url,
|
|
1223
1499
|
timeoutMs,
|
|
1500
|
+
...(!profileDir && configText("fetch.browserCdpUrl") ? { cdpUrl: configText("fetch.browserCdpUrl") } : {}),
|
|
1224
1501
|
allowPrivateHosts: allowPrivateHosts(),
|
|
1225
1502
|
waitUntil: choosePlaywrightWaitUntil(target, url),
|
|
1226
1503
|
extraHeaders,
|