2026-07-15 13:46:17 +08:00
|
|
|
export const SEARCH_ERROR_CODES = Object.freeze({ CAPABILITY_DISABLED: 'CAPABILITY_DISABLED', INVALID_REQUEST: 'INVALID_REQUEST', PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE', UNSAFE_URL: 'UNSAFE_URL' });
|
|
|
|
|
|
|
|
|
|
export function validateSearchRequest(input = {}) {
|
|
|
|
|
const query = String(input.query ?? '').trim();
|
|
|
|
|
if (!query || query.length > 500) return { ok: false, code: SEARCH_ERROR_CODES.INVALID_REQUEST, message: 'query must be 1-500 characters' };
|
|
|
|
|
const limit = Math.max(1, Math.min(20, Number(input.limit ?? 10) || 10));
|
|
|
|
|
return { ok: true, value: { query, type: String(input.type ?? 'web'), limit } };
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 09:19:41 +08:00
|
|
|
/** provider 返回的配图字段名不统一,统一取第一个安全的 https 地址。 */
|
|
|
|
|
function pickResultImage(result = {}) {
|
|
|
|
|
for (const candidate of [result.image, result.img_src, result.thumbnail_src, result.thumbnail]) {
|
|
|
|
|
const value = String(candidate ?? '').trim();
|
|
|
|
|
if (value.startsWith('https://') && isSafeHttpUrl(value)) return value.slice(0, 1000);
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 13:46:17 +08:00
|
|
|
export function normalizeSearchResult(result = {}, index = 0) {
|
2026-09-21 09:19:41 +08:00
|
|
|
const image = pickResultImage(result);
|
|
|
|
|
const publishedAt = String(result.publishedAt ?? result.publishedDate ?? '').trim().slice(0, 40);
|
|
|
|
|
return {
|
|
|
|
|
title: String(result.title ?? '').trim(),
|
|
|
|
|
url: String(result.url ?? '').trim(),
|
|
|
|
|
snippet: String(result.snippet ?? result.content ?? '').trim().slice(0, 2000),
|
|
|
|
|
source: String(result.source ?? 'unknown'),
|
|
|
|
|
rank: index + 1,
|
|
|
|
|
...(image ? { image } : {}),
|
|
|
|
|
...(publishedAt ? { publishedAt } : {}),
|
|
|
|
|
};
|
2026-07-15 13:46:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildCitations(results = []) {
|
|
|
|
|
return results.filter((item) => item.url).map((item, index) => ({ id: `[${index + 1}]`, title: item.title, url: item.url, source: item.source }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isSafeHttpUrl(value) {
|
|
|
|
|
try { const url = new URL(value); return ['http:', 'https:'].includes(url.protocol) && !['localhost', '127.0.0.1', '::1'].includes(url.hostname); } catch { return false; }
|
|
|
|
|
}
|