feat: add fastCRW search request, mapping and error handling
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b80b7eb87d
commit
cc3814deeb
2 files changed
+249
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import type { Config } from "./config.ts"
|
||||
|
||||
export type SearchResult = {
|
||||
url: string
|
||||
title: string
|
||||
content: string
|
||||
time: Record<string, never>
|
||||
}
|
||||
|
||||
const TIMEOUT_MS = 30_000
|
||||
|
||||
const str = (value: unknown) => (typeof value === "string" ? value : "")
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null
|
||||
|
||||
function hint(status: number, code: string | undefined): string {
|
||||
if (status === 401) return " (check apiKey or CRW_API_KEY)"
|
||||
if (code === "search_disabled") return " (the server has no search backend configured)"
|
||||
return ""
|
||||
}
|
||||
|
||||
// Hosted fastCRW returns `data` as an array, self-hosted as `{ results: [...] }`.
|
||||
function items(data: unknown): unknown[] {
|
||||
const list = isObject(data) && !Array.isArray(data) ? data.results : data
|
||||
if (!Array.isArray(list)) throw new Error("fastCRW: unexpected response shape")
|
||||
return list
|
||||
}
|
||||
|
||||
export async function search(
|
||||
config: Config,
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
fetchFn: typeof fetch = fetch,
|
||||
timeoutMs = TIMEOUT_MS,
|
||||
): Promise<SearchResult[]> {
|
||||
const timeout = AbortSignal.timeout(timeoutMs)
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" }
|
||||
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`
|
||||
|
||||
let res: Response
|
||||
let text: string
|
||||
try {
|
||||
res = await fetchFn(`${config.baseURL}/v1/search`, {
|
||||
method: "POST",
|
||||
signal: AbortSignal.any([signal, timeout]),
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
limit: config.limit,
|
||||
...(config.lang && { lang: config.lang }),
|
||||
...(config.tbs && { tbs: config.tbs }),
|
||||
}),
|
||||
})
|
||||
text = await res.text()
|
||||
} catch (cause) {
|
||||
if (signal.aborted) throw cause
|
||||
if (timeout.aborted) throw new Error(`fastCRW: timed out after ${timeoutMs / 1000}s`, { cause })
|
||||
throw new Error(`fastCRW: cannot reach ${config.baseURL}`, { cause })
|
||||
}
|
||||
|
||||
let body: unknown
|
||||
try {
|
||||
body = JSON.parse(text)
|
||||
} catch {
|
||||
throw new Error(`fastCRW ${res.status}: non-JSON response: ${text.slice(0, 200)}`)
|
||||
}
|
||||
const parsed = isObject(body) ? body : {}
|
||||
|
||||
if (!res.ok || parsed.success === false) {
|
||||
const code = str(parsed.error_code) || undefined
|
||||
const message = str(parsed.error) || res.statusText || "request failed"
|
||||
throw new Error(
|
||||
`fastCRW ${res.status}${code ? ` ${code}` : ""}: ${message}${hint(res.status, code)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return items(parsed.data).flatMap((item) => {
|
||||
if (!isObject(item) || !str(item.url)) return []
|
||||
const url = str(item.url)
|
||||
return [
|
||||
{
|
||||
url,
|
||||
title: str(item.title) || url,
|
||||
content: str(item.snippet) || str(item.description),
|
||||
time: {},
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user