feat(问翻小宝): 接入静态帮助中心
This commit is contained in:
105
apps/web/lib/wenfan-help-search.ts
Normal file
105
apps/web/lib/wenfan-help-search.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user