repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/web/search.ts
Rawimport { setTimeout as delay } from "node:timers/promises";
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { codexSearch, isCodexAvailable } from "./codex.js";
import type { SearchResult } from "./constants.js";
import {
BROWSER_HEADERS,
DDG_HTML_URL,
DEFAULT_TIMEOUT_MS,
} from "./constants.js";
import { markSearchTime, parseDdgResults, throttleWait } from "./ddg.js";
import { fetchSearchHtml } from "./fetch.js";
import { googleSearch } from "./google.js";
import { isMinimaxAvailable, minimaxSearch } from "./minimax.js";
import { parallelSearch } from "./parallel.js";
import { isZaiAvailable, zaiSearch } from "./zai.js";
export type SearchSource =
| "codex"
| "minimax"
| "parallel"
| "google"
| "ddg"
| "zai";
interface SearchProvider {
source: SearchSource;
label: "Codex" | "Z.AI" | "MiniMax" | "Parallel" | "Google WML" | "DDG";
search(): Promise<SearchResult[]>;
}
interface SearchFailure {
label: SearchProvider["label"];
err: unknown;
message: string;
}
const MAX_ERROR_TEXT = 240;
function displayError(err: unknown): string {
const message = (err instanceof Error ? err.message : String(err))
.replace(/\s+/g, " ")
.trim();
if (!message) return "unknown error";
if (message.length <= MAX_ERROR_TEXT) return message;
return `${message.slice(0, MAX_ERROR_TEXT - 1)}…`;
}
export interface WebSearchOptions {
query: string;
count: number;
timeout: number;
signal?: AbortSignal;
ctx?: ExtensionContext;
}
export interface WebSearchOutcome {
results: SearchResult[];
source: SearchSource;
}
export async function searchWeb(
options: WebSearchOptions,
): Promise<WebSearchOutcome> {
options.signal?.throwIfAborted();
const providers = await buildProviders(options);
const failures: SearchFailure[] = [];
for (const provider of providers) {
options.signal?.throwIfAborted();
try {
return { results: await provider.search(), source: provider.source };
} catch (err) {
options.signal?.throwIfAborted();
failures.push({
label: provider.label,
err,
message: displayError(err),
});
}
}
const summary = failures
.map((failure) => `${failure.label}: ${failure.message}`)
.join(" | ");
throw new AggregateError(
failures.map((failure) => failure.err),
`Search unavailable: ${summary}`,
);
}
export async function buildProviders(
options: WebSearchOptions,
): Promise<SearchProvider[]> {
const providers: SearchProvider[] = [
{
source: "google",
label: "Google WML",
search: () =>
googleSearch(
options.query,
options.signal,
options.count,
options.timeout,
),
},
{
source: "parallel",
label: "Parallel",
search: () =>
parallelSearch(
options.query,
options.signal,
options.count,
options.timeout,
),
},
];
const ctx = options.ctx;
if (ctx && (await isCodexAvailable(ctx))) {
providers.push({
source: "codex",
label: "Codex",
search: () =>
codexSearch(options.query, ctx, options.signal, options.count),
});
}
if (ctx && (await isMinimaxAvailable(ctx))) {
providers.push({
source: "minimax",
label: "MiniMax",
search: () => searchMinimax(options.query, ctx, options.signal),
});
}
providers.push({
source: "ddg",
label: "DDG",
search: () =>
searchDdg(options.query, options.signal, options.timeout, options.ctx),
});
if (await isZaiAvailable(options.ctx)) {
providers.push({
source: "zai",
label: "Z.AI",
search: () =>
zaiSearch(options.query, options.ctx, options.signal, {
count: options.count,
timeoutMs: options.timeout,
}),
});
}
return providers;
}
async function searchMinimax(
query: string,
ctx: ExtensionContext,
signal?: AbortSignal,
): Promise<SearchResult[]> {
const response = await minimaxSearch(query, ctx, signal);
return (response.organic ?? []).map((r) => ({
title: r.title,
url: r.link,
snippet: r.snippet,
date: r.date,
}));
}
async function searchDdg(
query: string,
signal?: AbortSignal,
timeout?: number,
ctx?: ExtensionContext,
): Promise<SearchResult[]> {
const deadline = AbortSignal.timeout(timeout ?? DEFAULT_TIMEOUT_MS);
const requestSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
requestSignal.throwIfAborted();
const waitMs = throttleWait();
if (waitMs > 0) {
ctx?.ui?.setStatus(
"web",
`⏳ DDG rate limit: ${(waitMs / 1000).toFixed(1)}s`,
);
try {
await delay(waitMs, undefined, { signal: requestSignal });
} finally {
ctx?.ui?.setStatus("web", undefined);
}
}
const html = await fetchSearchHtml(DDG_HTML_URL, requestSignal, timeout, {
method: "POST",
headers: {
...BROWSER_HEADERS,
"Content-Type": "application/x-www-form-urlencoded",
Origin: new URL(DDG_HTML_URL).origin,
Referer: new URL("/", DDG_HTML_URL).href,
},
body: new URLSearchParams({ q: query, b: "", kl: "wt-wt" }),
});
markSearchTime();
const parsed = parseDdgResults(html);
if (parsed.error) throw new Error(parsed.error);
return parsed.results.map((r) => ({
title: r.title,
url: r.url,
snippet: r.snippet,
}));
}