/** * Shared types and utilities for web-fetch handlers */ import { scheduler } from "node:timers/promises"; import { ptree } from "@oh-my-pi/pi-utils"; import { decodeEntities } from "@oh-my-pi/pi-utils/dom/parser"; import type TurndownService from "@oh-my-pi/pi-utils/turndown"; import type { ModelRegistry } from "../../config/model-registry"; import type { AgentStorageApi } from "../../session/agent-storage"; import { ToolAbortError } from "../../tools/tool-errors"; import { decodeBody, readBodyCapped } from "../response-body"; export { formatNumber } from "@oh-my-pi/pi-utils"; export interface RenderResult { url: string; finalUrl: string; contentType: string; method: string; content: string; fetchedAt: string; /** True when the response body was cut mid-stream at {@link MAX_BYTES}. */ truncated: boolean; notes: string[]; } /** * Site-specific URL renderer: returns `null` for URLs it does not own. * `modelRegistry` is the session's, for handlers that read through a model (X via Grok). */ export type SpecialHandler = ( url: string, timeout: number, signal?: AbortSignal, storage?: AgentStorageApi | null, modelRegistry?: ModelRegistry, ) => Promise; /** Network read bound for a fetched body; the only size limit on rendered URL content. */ export const MAX_BYTES = 50 * 1024 * 1024; const USER_AGENTS = [ "curl/8.0", "Mozilla/5.0 (compatible; TextBot/1.0)", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", ]; /** * Statuses sites answer a user agent they refuse with (999 is LinkedIn's), whatever the body * says: a bare nginx "403 Forbidden" page, SEC's "Undeclared Automated Tool" page or a 429 * keyed on the agent all serve the page to the next user agent. */ const BLOCKED_STATUSES = new Set([403, 429, 503, 999]); /** A challenge interstitial carries no content (Reddit's is about 8 KB); a larger page is no wall. */ const CHALLENGE_PAGE_MAX_CHARS = 64 * 1024; /** * True when a page small enough to be an interstitial holds an `` field * outside `