haixunMaster/lib/search/brave-execute.ts

174 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { cacheGet, cacheSet } from "./cache";
import { getSearchConfig } from "./config";
import { extractThreadId } from "./dedupe";
import { logSearchEvent } from "./logger";
import { canUseDailyQuota, getDailyQuotaUsed, incrementDailyQuota } from "./quota";
import { buildBraveThreadsKeywordQuery } from "./query-builders";
import type { KeywordPriority, SearchResult } from "./types";
export interface BraveExecuteOptions {
keyword: string;
limit: number;
/** 直接指定查詢字串(略過 keyword query builder */
query?: string;
priority?: KeywordPriority;
/** 巡邏模式:檢查 high priority + 每日額度(預設 true */
patrolMode?: boolean;
/** 僅保留 threads.com 結果(預設 truefact-check 等可設 false */
threadsOnly?: boolean;
}
export interface BraveExecuteResult {
results: SearchResult[];
status: "success" | "unavailable" | "skipped";
skipReason?: string;
quotaUsed?: number;
quotaLimit?: number;
fromCache?: boolean;
}
function mapBraveItems(
items: Array<{ title?: string; description?: string; url?: string }>,
limit: number,
threadsOnly: boolean
): SearchResult[] {
return items
.filter((item) => item.url && (!threadsOnly || /threads\.(?:com|net)/i.test(item.url)))
.slice(0, limit)
.map((item) => ({
title: item.title ?? "",
snippet: item.description ?? "",
url: item.url ?? "",
author: "",
source: "brave" as const,
threadId: extractThreadId(item.url ?? ""),
}));
}
export async function executeBraveSearch(opts: BraveExecuteOptions): Promise<BraveExecuteResult> {
const cfg = getSearchConfig().brave;
const patrolMode = opts.patrolMode !== false;
const threadsOnly = opts.threadsOnly !== false;
const quotaLimit = cfg.dailyLimit;
if (!cfg.enabled || !cfg.apiKey) {
return { results: [], status: "unavailable" };
}
if (patrolMode && opts.priority && opts.priority !== "high") {
logSearchEvent({
kind: "provider",
provider: "brave",
status: "skipped",
keyword: opts.keyword,
reason: "keyword_priority_not_high",
});
return {
results: [],
status: "skipped",
skipReason: "keyword_priority_not_high",
};
}
if (patrolMode && !canUseDailyQuota("brave", quotaLimit)) {
logSearchEvent({
kind: "provider",
provider: "brave",
status: "skipped",
keyword: opts.keyword,
reason: "daily_limit_reached",
quotaUsed: getDailyQuotaUsed("brave"),
quotaLimit,
});
return {
results: [],
status: "skipped",
skipReason: "daily_limit_reached",
quotaUsed: getDailyQuotaUsed("brave"),
quotaLimit,
};
}
const query = opts.query?.trim() || buildBraveThreadsKeywordQuery(opts.keyword);
const cacheKey = `search:brave:${query}`;
const cached = cacheGet<SearchResult[]>("brave", cacheKey);
if (cached) {
logSearchEvent({
kind: "provider",
provider: "brave",
status: "skipped",
keyword: opts.keyword,
reason: "cache_hit",
count: cached.length,
quotaUsed: getDailyQuotaUsed("brave"),
quotaLimit,
});
return {
results: cached.slice(0, opts.limit),
status: "success",
skipReason: "cache_hit",
quotaUsed: getDailyQuotaUsed("brave"),
quotaLimit,
fromCache: true,
};
}
try {
const url = new URL(cfg.baseUrl);
url.searchParams.set("q", query);
url.searchParams.set("count", String(Math.min(opts.limit, cfg.resultLimit, 20)));
url.searchParams.set("country", "tw");
url.searchParams.set("search_lang", "zh-hant");
const res = await fetch(url.toString(), {
headers: {
Accept: "application/json",
"X-Subscription-Token": cfg.apiKey,
},
signal: AbortSignal.timeout(20_000),
});
if (!res.ok) {
logSearchEvent({
kind: "provider",
provider: "brave",
status: "unavailable",
keyword: opts.keyword,
});
return { results: [], status: "unavailable" };
}
const data = (await res.json()) as {
web?: { results?: Array<{ title?: string; description?: string; url?: string }> };
};
const results = mapBraveItems(data.web?.results ?? [], opts.limit, threadsOnly);
const quotaUsed = patrolMode ? incrementDailyQuota("brave") : getDailyQuotaUsed("brave");
cacheSet("brave", cacheKey, results, cfg.cacheTtlMs);
logSearchEvent({
kind: "provider",
provider: "brave",
status: "success",
keyword: opts.keyword,
count: results.length,
quotaUsed,
quotaLimit,
});
return {
results,
status: "success",
quotaUsed,
quotaLimit,
};
} catch {
logSearchEvent({
kind: "provider",
provider: "brave",
status: "unavailable",
keyword: opts.keyword,
});
return { results: [], status: "unavailable" };
}
}