Files
ftb-project-management/apps/web/lib/wenfan-help-search.ts
2026-06-30 19:27:23 +08:00

106 lines
3.0 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 { WENFAN_HELP_ARTICLES, type HelpArticle } from './wenfan-help-articles';
export type HelpSearchResult = {
article: HelpArticle;
score: number;
matchedKeywords: string[];
relatedArticles: HelpArticle[];
};
const MIN_RESULT_SCORE = 15;
const FALLBACK_SUGGESTIONS = [
'怎么新建产品?',
'怎么把需求纳入版本?',
'开发任务怎么提测?',
'测试用例失败后怎么提 Bug',
'AI 拆解在哪里使用?',
'日志记录会记录哪些行为?',
];
export function normalizeHelpQuery(input: string): string {
return input
.toLowerCase()
.replace(/[,。!?、,.!?;:"'“”‘’()()【】[\]{}<>《》\s]/g, '')
.trim();
}
function isSubsequence(needle: string, haystack: string): boolean {
if (needle.length < 4) return false;
let index = 0;
for (const char of haystack) {
if (char === needle[index]) index += 1;
if (index === needle.length) return true;
}
return false;
}
function scoreText(query: string, text: string, exactScore: number, fuzzyScore: number): number {
const normalized = normalizeHelpQuery(text);
if (!normalized) return 0;
if (normalized.includes(query) || query.includes(normalized)) return exactScore;
if (isSubsequence(normalized, query) || isSubsequence(query, normalized)) return fuzzyScore;
return 0;
}
function resolveRelatedArticles(article: HelpArticle, articles: HelpArticle[]): HelpArticle[] {
return article.relatedIds
.map((id) => articles.find((item) => item.id === id))
.filter((item): item is HelpArticle => Boolean(item));
}
export function searchHelpArticles(
query: string,
articles: HelpArticle[] = WENFAN_HELP_ARTICLES,
): HelpSearchResult[] {
const normalizedQuery = normalizeHelpQuery(query);
if (normalizedQuery.length < 2) return [];
return articles
.map((article) => {
let score = 0;
const matchedKeywords: string[] = [];
score += scoreText(normalizedQuery, article.title, 20, 10);
score += scoreText(normalizedQuery, article.category, 12, 0);
for (const keyword of article.keywords) {
const keywordScore = scoreText(normalizedQuery, keyword, 10, 6);
if (keywordScore > 0) {
score += keywordScore;
matchedKeywords.push(keyword);
}
}
const secondaryTexts = [
article.scenario,
article.entry,
...article.steps,
...article.requiredFields,
...article.statusFlow,
...article.notes,
];
for (const text of secondaryTexts) {
score += scoreText(normalizedQuery, text, 3, 1);
}
return {
article,
score,
matchedKeywords,
relatedArticles: resolveRelatedArticles(article, articles),
};
})
.filter((result) => result.score >= MIN_RESULT_SCORE)
.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
return a.article.title.localeCompare(b.article.title, 'zh-CN');
});
}
export function getFallbackHelpSuggestions(): string[] {
return FALLBACK_SUGGESTIONS;
}