haixunMaster/lib/search/notify-rules.ts

87 lines
2.1 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
import type { SearchResult } from "./types";
const NEGATIVE_WORDS = [
"不好用",
"雷",
"踩雷",
"失望",
"問題",
"壞掉",
"客服",
"退款",
"詐騙",
"不推",
"有人遇過嗎",
"怎麼辦",
];
const QUESTION_WORDS = [
"有人用過",
"好用嗎",
"推薦嗎",
"值得買嗎",
"怎麼選",
"求推薦",
"請問",
"疑問",
];
export interface NotifyRuleContext {
brandTerms?: string[];
productTerms?: string[];
accountTerms?: string[];
competitorTerms?: string[];
manualHighPriority?: boolean;
}
export interface NotifyMatch {
matched: boolean;
rules: string[];
}
function blob(result: SearchResult): string {
return `${result.title} ${result.snippet} ${result.url}`.toLowerCase();
}
export function evaluateNotifyRules(
result: SearchResult,
ctx: NotifyRuleContext
): NotifyMatch {
const text = blob(result);
const rules: string[] = [];
if (ctx.manualHighPriority) rules.push("high_priority_keyword");
for (const term of ctx.brandTerms ?? []) {
if (term && text.includes(term.toLowerCase())) rules.push("brand");
}
for (const term of ctx.productTerms ?? []) {
if (term && text.includes(term.toLowerCase())) rules.push("product");
}
for (const term of ctx.accountTerms ?? []) {
if (term && text.includes(term.toLowerCase().replace(/^@/, ""))) rules.push("account");
}
for (const term of ctx.competitorTerms ?? []) {
if (term && text.includes(term.toLowerCase())) rules.push("competitor");
}
for (const w of NEGATIVE_WORDS) {
if (text.includes(w)) rules.push("negative_word");
}
for (const w of QUESTION_WORDS) {
if (text.includes(w)) rules.push("question_word");
}
return { matched: rules.length > 0, rules: [...new Set(rules)] };
}
export function filterNotifiableResults(
results: SearchResult[],
ctx: NotifyRuleContext
): Array<SearchResult & { matchedRules: string[] }> {
return results
.map((r) => {
const verdict = evaluateNotifyRules(r, ctx);
return verdict.matched ? { ...r, matchedRules: verdict.rules } : null;
})
.filter((r): r is SearchResult & { matchedRules: string[] } => r !== null);
}