feat(问翻小宝): 接入静态帮助中心

This commit is contained in:
Script Generator
2026-06-30 19:27:23 +08:00
parent 846bf9e8d1
commit 8683b341e2
21 changed files with 1422 additions and 47 deletions

View 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;
}