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,541 @@
export type HelpImage = {
src: string;
alt: string;
caption: string;
};
export type HelpArticle = {
id: string;
title: string;
category: string;
keywords: string[];
scenario: string;
entry: string;
steps: string[];
requiredFields: string[];
statusFlow: string[];
notes: string[];
images: HelpImage[];
relatedIds: string[];
};
function helpImage(name: string, alt: string, caption: string): HelpImage {
return {
src: `/help/wenfan-xiaobao/${name}.png`,
alt,
caption,
};
}
export const WENFAN_HELP_ARTICLES: HelpArticle[] = [
{
id: 'product-create',
title: '产品如何新建',
category: '产品',
keywords: [
'产品',
'产品管理',
'新建产品',
'创建产品',
'新增产品',
'产品怎么建',
'怎么新建产品',
'产品要填什么',
'产品字段',
'产品名称',
'产品负责人',
'顶层容器',
],
scenario: '当你要把一个业务系统、客户项目或产品线作为顶层容器管理时,先创建产品。',
entry: '左侧导航栏 -> 产品 -> 新建产品',
steps: [
'进入“产品”页面。',
'点击页面右上角的新建产品按钮。',
'填写产品名称、负责人、说明等信息。',
'保存后,产品会出现在产品列表中,后续项目、版本和需求都围绕产品展开。',
],
requiredFields: ['产品名称', '负责人', '产品描述或备注(建议填写,方便团队识别)'],
statusFlow: ['产品本身没有复杂状态流转,主要作为项目、版本和需求的顶层归属。'],
notes: [
'产品不要和项目混用:产品是顶层容器,项目是产品下的执行范围。',
'产品名称建议稳定,后续项目和版本会引用它。',
],
images: [helpImage('product-create', '产品页面截图', '在产品页面创建产品。')],
relatedIds: ['project-create', 'requirement-pool', 'version-create'],
},
{
id: 'project-create',
title: '项目如何新建',
category: '项目',
keywords: [
'项目',
'项目管理',
'新建项目',
'创建项目',
'新增项目',
'项目怎么建',
'项目归属产品',
'项目字段',
'项目负责人',
'项目成员',
'项目和产品',
],
scenario: '当一个产品下需要拆出具体交付范围、团队或客户实施范围时,创建项目。',
entry: '左侧导航栏 -> 项目 -> 新建项目',
steps: [
'进入“项目”页面。',
'点击新建项目按钮。',
'选择归属产品。',
'填写项目名称、负责人、项目类型、说明等信息。',
'保存后,可以继续在项目下创建版本和关联需求。',
],
requiredFields: ['归属产品', '项目名称', '项目负责人', '项目类型'],
statusFlow: ['项目作为执行容器,版本和任务状态会在项目下汇总。'],
notes: [
'一个产品可以有多个项目。',
'项目创建后,需求纳入版本时会按当前项目范围筛选候选需求。',
],
images: [helpImage('project-create', '项目页面截图', '在项目页面创建项目并选择归属产品。')],
relatedIds: ['product-create', 'version-create', 'version-requirement-include'],
},
{
id: 'version-create',
title: '版本如何新建',
category: '版本',
keywords: [
'版本',
'版本号',
'新建版本',
'创建版本',
'新增版本',
'版本怎么建',
'版本字段',
'预期发布日期',
'发版日期',
'版本成员',
'项目开发类型',
'版本状态',
],
scenario: '当项目进入一次明确的交付或发版周期时,创建版本承载计划、开发、测试和发布风险。',
entry: '左侧导航栏 -> 版本 -> 新建版本',
steps: [
'进入“版本”页面。',
'点击新建版本按钮。',
'选择产品和项目。',
'填写版本号、版本名称、项目开发类型、预期发布日期。',
'添加版本成员,保存版本。',
],
requiredFields: ['产品', '项目', '版本号', '版本名称', '项目开发类型', '预期发布日期', '版本成员'],
statusFlow: ['计划中', '开发中', '已发布', '已关闭', '已暂停'],
notes: [
'版本是执行主线,需求、计划、开发任务、测试用例和 Bug 都围绕版本聚合。',
'预期发布日期会影响小宝预警的风险判断。',
],
images: [helpImage('version-create', '版本页面截图', '在版本页面创建版本并维护版本信息。')],
relatedIds: ['project-create', 'version-requirement-include', 'xiaobao-warning'],
},
{
id: 'requirement-pool',
title: '需求池如何新建和采纳需求',
category: '需求池',
keywords: [
'需求',
'需求池',
'新建需求',
'创建需求',
'新增需求',
'需求怎么建',
'需求字段',
'需求采纳',
'采纳需求',
'通过需求',
'需求状态',
'需求评审',
'需求进入版本',
],
scenario: '当产品或项目有新的业务诉求时,先放进需求池,再经过采纳后进入版本执行。',
entry: '左侧导航栏 -> 需求池 -> 新建需求',
steps: [
'进入“需求池”页面。',
'点击新建需求。',
'填写需求标题、归属产品/项目、需求描述、优先级等信息。',
'保存后,需求进入待评审或待采纳状态。',
'评审通过后点击采纳,需求才会成为版本可纳入的候选项。',
],
requiredFields: ['需求标题', '归属产品', '归属项目', '需求描述', '优先级', '提出人'],
statusFlow: ['待评审', '已采纳', '已规划', '开发中', '测试中', '已发布', '已关闭', '已拒绝'],
notes: [
'只有已采纳需求才应该进入版本。',
'需求解释为什么要做,版本和任务负责怎么执行。',
],
images: [helpImage('requirement-pool', '需求池页面截图', '在需求池中新建、评审和采纳需求。')],
relatedIds: ['version-requirement-include', 'version-create', 'dev-task-workflow'],
},
{
id: 'version-requirement-include',
title: '版本里如何从需求池纳入需求',
category: '版本',
keywords: [
'版本需求',
'需求纳入版本',
'需求怎么纳入版本',
'需求怎么纳入版本号',
'纳入需求',
'纳入版本',
'纳入版本号',
'加入版本',
'关联需求',
'需求关联版本',
'需求进入版本',
'从需求池纳入',
'版本号里需求',
'版本号里面的需求',
'版本号需求',
'需求怎么放进版本',
'需求怎么加入版本号',
'已采纳需求',
],
scenario: '当需求池里的需求已采纳,并且本次版本要实现它时,把需求纳入版本。',
entry: '左侧导航栏 -> 版本 -> 打开版本详情 -> 需求区域',
steps: [
'打开目标版本详情。',
'进入需求区域或需求关联入口。',
'从当前项目下已采纳需求中选择要纳入本版本的需求。',
'保存关联关系。',
'后续产品方案、AI 拆解、开发任务和测试用例都围绕这些版本需求展开。',
],
requiredFields: ['目标版本', '至少一条已采纳需求'],
statusFlow: ['需求:已采纳 -> 已规划 -> 开发中 -> 测试中 -> 已发布'],
notes: [
'候选需求来自当前项目下已采纳需求,不从全量需求池随意选择。',
'如果找不到需求,先回需求池确认是否已采纳、是否属于当前项目。',
],
images: [helpImage('version-requirement-include', '版本需求区域截图', '在版本详情中把已采纳需求纳入版本。')],
relatedIds: ['requirement-pool', 'product-plan', 'ai-decompose'],
},
{
id: 'research-plan',
title: '调研计划如何创建和管理进度',
category: '调研',
keywords: [
'调研',
'调研计划',
'创建调研',
'新建调研',
'调研方向',
'调研进度',
'方向进度',
'调研完成',
'计划日志',
'VersionPlan',
],
scenario: '当版本前期需要先做业务、技术或方案调研时,创建调研计划并记录方向进展。',
entry: '版本详情 -> 计划区域 -> 新建调研计划',
steps: [
'打开版本详情。',
'在计划区域选择新建调研计划。',
'填写计划标题、负责人、计划起止时间。',
'维护调研方向和每个方向的完成情况。',
'所有调研方向完成并提交成果后,计划可完成。',
],
requiredFields: ['计划标题', '负责人', '计划开始时间', '计划结束时间', '调研方向'],
statusFlow: ['未开始', '进行中', '已完成'],
notes: [
'调研计划看的是方向完成度,不以需求覆盖作为完成条件。',
'方向进度更新会进入计划日志和工作活动记录。',
],
images: [helpImage('research-plan', '调研计划截图', '在版本计划中创建调研计划并更新方向进度。')],
relatedIds: ['product-plan', 'activity-logs', 'version-create'],
},
{
id: 'product-plan',
title: '产品方案如何创建和提交成果',
category: '产品方案',
keywords: [
'产品方案',
'方案',
'原型',
'成果链接',
'创建产品方案',
'新建产品方案',
'需求覆盖',
'覆盖需求',
'提交成果',
'产品计划',
'方案进度',
],
scenario: '当版本需求需要先沉淀产品方案或原型成果时,创建产品方案计划。',
entry: '版本详情 -> 计划区域 -> 新建产品方案',
steps: [
'打开版本详情。',
'在计划区域创建产品方案计划。',
'选择或确认本方案覆盖的版本需求。',
'按需求记录未开始、部分完成、已完成的覆盖状态。',
'提交成果链接,通常是原型链接或方案文档。',
'覆盖需求全部完成并提交成果后,计划可完成。',
],
requiredFields: ['计划标题', '负责人', '计划起止时间', '覆盖需求', '成果标题', '成果链接'],
statusFlow: ['计划:未开始 -> 进行中 -> 已完成', '需求覆盖:未开始 -> 部分完成 -> 已完成'],
notes: [
'AI 拆解会优先使用已完成产品方案的成果链接作为原型输入。',
'部分完成只记录进度,不满足成果提交门槛。',
],
images: [helpImage('product-plan', '产品方案计划截图', '在产品方案计划中维护需求覆盖并提交成果。')],
relatedIds: ['version-requirement-include', 'ai-decompose', 'activity-logs'],
},
{
id: 'ai-decompose',
title: 'AI 拆解开发任务和测试用例如何使用',
category: 'AI 拆解',
keywords: [
'AI拆解',
'ai拆解',
'AI 拆解',
'任务拆解',
'开发任务拆解',
'测试用例拆解',
'AI生成任务',
'AI生成用例',
'采纳草稿',
'对账报告',
'原型拆解',
],
scenario: '当版本已有已完成产品方案和已纳入需求时,可以让 AI 生成开发任务或测试用例草稿。',
entry: '版本详情 -> 产品方案计划 -> AI 拆解开发任务 / AI 拆解测试用例',
steps: [
'确认版本中已纳入至少一条需求。',
'确认至少一个产品方案计划已完成,并提交了成果链接。',
'在产品方案区域点击 AI 拆解开发任务或 AI 拆解测试用例。',
'查看对账报告,确认需求和原型是否匹配。',
'选择要采纳的草稿。',
'采纳后,草稿进入开发任务或测试用例列表,并带有 AI 草稿标记。',
],
requiredFields: ['已完成产品方案成果链接', '版本已纳入需求', '版本成员'],
statusFlow: ['AI 草稿 -> 用户采纳 -> 任务/用例列表 -> 用户编辑后转为普通记录'],
notes: [
'AI 只生成草稿,不代表负责人已确认排期。',
'AI 估时写入 aiEstimateHours执行估时仍需人工确认。',
'第一阶段问翻小宝不会调用 AI这里说明的是业务系统里的 AI 拆解功能。',
],
images: [helpImage('ai-decompose', 'AI 拆解入口截图', '在产品方案计划中触发 AI 拆解。')],
relatedIds: ['product-plan', 'dev-task-workflow', 'test-case-workflow'],
},
{
id: 'ui-design-plan',
title: 'UI 设计计划如何创建和管理进度',
category: 'UI 设计',
keywords: [
'UI',
'ui',
'UI设计',
'ui设计',
'新建设计',
'创建UI设计',
'UI计划',
'设计进度',
'提交设计',
'设计成果',
],
scenario: '当版本需要设计稿、交互稿或视觉稿交付时,创建 UI 设计计划。',
entry: '版本详情 -> 计划区域 -> 新建 UI 设计',
steps: [
'打开版本详情。',
'在计划区域创建 UI 设计计划。',
'填写负责人和计划起止时间。',
'按需求维护设计覆盖进度。',
'提交设计稿链接或成果说明。',
'需求覆盖完成并提交成果后,计划可完成。',
],
requiredFields: ['计划标题', '负责人', '计划起止时间', '覆盖需求', '设计成果链接'],
statusFlow: ['计划:未开始 -> 进行中 -> 已完成', '需求覆盖:未开始 -> 部分完成 -> 已完成'],
notes: [
'UI 设计和产品方案一样,需要记录需求覆盖状态。',
'提交成果后,日志会记录计划完成和成果提交行为。',
],
images: [helpImage('ui-design-plan', 'UI 设计计划截图', '在版本计划中创建 UI 设计计划并提交成果。')],
relatedIds: ['product-plan', 'activity-logs', 'dev-task-workflow'],
},
{
id: 'dev-task-workflow',
title: '开发任务如何新建、开发中和提测',
category: '开发任务',
keywords: [
'开发任务',
'任务',
'DevTask',
'新建开发任务',
'创建开发任务',
'任务怎么建',
'领取任务',
'填写计划',
'开发中',
'自测',
'提测',
'提交测试',
'任务状态',
'阻塞',
],
scenario: '当版本需求已经明确,需要拆成具体研发工作项时,创建开发任务并推进状态。',
entry: '版本详情 -> 开发任务 Tab -> 新建开发任务',
steps: [
'打开版本详情并进入开发任务 Tab。',
'点击新建开发任务。',
'选择关联需求,填写任务标题、类型、负责人、计划起止时间、估时等信息。',
'负责人领取或确认计划后,将任务切换到开发中。',
'开发完成后进入自测。',
'自测通过且没有阻塞时,切换为已提测。',
],
requiredFields: ['关联需求或原型批注', '任务标题', '任务类型', '负责人', '计划开始时间', '计划结束时间', '执行估时'],
statusFlow: ['待开发', '开发中', '自测', '已提测'],
notes: [
'已提测是开发任务终态,后续测试失败不会回写开发任务状态。',
'任务阻塞时不能提测,必须先解除阻塞。',
'AI 草稿任务需要人工编辑或确认后再进入正式执行。',
],
images: [helpImage('dev-task-workflow', '开发任务页面截图', '在开发任务 Tab 新建任务并推进到提测。')],
relatedIds: ['ai-decompose', 'test-case-workflow', 'activity-logs'],
},
{
id: 'test-case-workflow',
title: '测试用例如何新建和执行',
category: '测试用例',
keywords: [
'测试用例',
'用例',
'TestCase',
'新建测试用例',
'创建测试用例',
'用例怎么建',
'开始测试',
'测试通过',
'测试失败',
'测试阻塞',
'用例状态',
'测试轮次',
],
scenario: '当版本进入验收或测试阶段,需要按需求和测试点建立测试用例并执行。',
entry: '版本详情 -> 测试用例 Tab -> 新建测试用例',
steps: [
'打开版本详情并进入测试用例 Tab。',
'点击新建测试用例。',
'选择关联需求、测试类型、负责人和计划测试时间。',
'填写测试标题、步骤、预期结果。',
'开始测试后,将用例切换为测试中。',
'根据执行结果标记通过、失败或阻塞。',
],
requiredFields: ['用例标题', '测试类型', '负责人', '计划测试时间', '计划结束时间', '测试步骤', '预期结果'],
statusFlow: ['待测试', '测试中', '通过', '失败', '阻塞'],
notes: [
'测试用例主归属是版本,关联需求用于语义分组和覆盖说明。',
'失败用例可以直接创建 Bug。',
'只有最新轮次用例全部完成后,才能开启下一轮测试。',
],
images: [helpImage('test-case-workflow', '测试用例页面截图', '在测试用例 Tab 新建和执行用例。')],
relatedIds: ['bug-workflow', 'dev-task-workflow', 'ai-decompose'],
},
{
id: 'bug-workflow',
title: 'Bug 如何新建和流转',
category: 'Bug',
keywords: [
'Bug',
'BUG',
'bug',
'缺陷',
'问题',
'新建Bug',
'创建Bug',
'提Bug',
'怎么提bug',
'测试失败提bug',
'修复Bug',
'验证Bug',
'关闭Bug',
'Bug状态',
],
scenario: '当测试用例失败或线上发现问题时,创建 Bug 并跟踪修复、验证和关闭。',
entry: '版本详情 -> Bug Tab -> 新建 Bug或测试用例失败后点击提 Bug',
steps: [
'从 Bug Tab 点击新建 Bug或在失败测试用例里点击提 Bug。',
'填写 Bug 标题、严重程度、关联版本、关联测试用例、复现步骤。',
'分配修复负责人和计划修复时间。',
'负责人将 Bug 切换到修复中。',
'修复完成后切换为待验证。',
'测试验证通过后关闭;不通过时可退回修复。',
],
requiredFields: ['Bug 标题', '严重程度', '所属版本', '复现步骤', '负责人', '计划修复时间'],
statusFlow: ['待修复', '修复中', '待验证', '已关闭', '已拒绝'],
notes: [
'Bug 直接挂在版本上,测试用例只是来源追踪。',
'关键 Bug 会影响小宝预警风险等级。',
'关闭前应确认修复结果和验证记录。',
],
images: [helpImage('bug-workflow', 'Bug 页面截图', '在 Bug Tab 新建并跟踪 Bug。')],
relatedIds: ['test-case-workflow', 'xiaobao-warning', 'activity-logs'],
},
{
id: 'activity-logs',
title: '日志记录会记录哪些行为',
category: '日志记录',
keywords: [
'日志',
'记录',
'行为记录',
'活动记录',
'工作活动',
'计划日志',
'日报',
'谁操作了',
'记录哪些行为',
'状态变更记录',
'work activity',
'activity',
],
scenario: '当你需要追踪计划、任务、测试、Bug 的关键操作时,查看日志和活动记录。',
entry: '版本详情计划抽屉、任务详情、工作台日报、小宝预警证据区域',
steps: [
'在计划详情里查看计划日志了解需求覆盖、成果提交、AI 拆解等计划行为。',
'在开发任务、测试用例、Bug 详情里查看对应实体的状态变更记录。',
'在工作台日报里查看当前用户当天的交付、进展、创建和风险记录。',
'在小宝预警里,系统会把日志和活动作为风险证据之一。',
],
requiredFields: ['无需手工填写;系统在关键业务动作成功后自动记录。'],
statusFlow: ['日志不单独流转,跟随业务动作追加。'],
notes: [
'计划日志记录计划内部行为,例如需求覆盖更新和成果提交。',
'工作活动记录跨模块关键动作例如任务开始、提测、Bug 修复、测试失败。',
'手动工作日志仍可补充说明多日进行中的工作。',
],
images: [helpImage('activity-logs', '日志记录页面截图', '在详情或工作台查看系统自动记录的行为。')],
relatedIds: ['research-plan', 'product-plan', 'dev-task-workflow'],
},
{
id: 'xiaobao-warning',
title: '小宝预警和问翻小宝有什么区别',
category: '小宝预警',
keywords: [
'小宝预警',
'预警',
'风险',
'红点',
'发布风险',
'能不能发版',
'问翻小宝和小宝预警',
],
scenario: '当你想判断版本能不能按期发布时,看小宝预警;当你不知道系统怎么用时,问问翻小宝。',
entry: '左侧导航栏 -> 小宝预警',
steps: [
'进入小宝预警页面。',
'查看有风险的未完成版本。',
'打开预警详情,查看风险分、预计发版日、风险原因和建议动作。',
],
requiredFields: ['无需手工填写风险来自版本、任务、测试、Bug、日报和活动数据。'],
statusFlow: ['正常', '关注', '有风险', '大概率延期', '阻塞'],
notes: [
'小宝预警用于版本发布风险,不回答系统怎么使用。',
'问翻小宝第一阶段只回答帮助中心内容,不分析版本风险。',
],
images: [helpImage('xiaobao-warning', '小宝预警页面截图', '小宝预警用于查看版本发布风险。')],
relatedIds: ['version-create', 'activity-logs', 'bug-workflow'],
},
];

View File

@@ -0,0 +1,48 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { getFallbackHelpSuggestions, searchHelpArticles } from './wenfan-help-search';
import { WENFAN_HELP_ARTICLES } from './wenfan-help-articles';
test('searchHelpArticles matches common product creation phrasing', () => {
const result = searchHelpArticles('产品怎么新建,要填什么字段', WENFAN_HELP_ARTICLES);
assert.equal(result[0]?.article.id, 'product-create');
});
test('searchHelpArticles matches demand adoption and version inclusion synonyms', () => {
const result = searchHelpArticles('需求怎么纳入版本号里面', WENFAN_HELP_ARTICLES);
assert.equal(result[0]?.article.id, 'version-requirement-include');
});
test('searchHelpArticles matches dev task test submission phrasing', () => {
const result = searchHelpArticles('开发任务怎么提测', WENFAN_HELP_ARTICLES);
assert.equal(result[0]?.article.id, 'dev-task-workflow');
});
test('searchHelpArticles matches bug creation from failed test cases', () => {
const result = searchHelpArticles('测试用例失败了怎么提bug', WENFAN_HELP_ARTICLES);
assert.equal(result[0]?.article.id, 'bug-workflow');
});
test('searchHelpArticles keeps related article ids from the matched article', () => {
const result = searchHelpArticles('AI拆解测试用例在哪里', WENFAN_HELP_ARTICLES);
assert.equal(result[0]?.article.id, 'ai-decompose');
assert.ok(result[0]?.relatedArticles.some((article) => article.id === 'product-plan'));
});
test('searchHelpArticles returns no article for short or unknown input', () => {
assert.deepEqual(searchHelpArticles('?', WENFAN_HELP_ARTICLES), []);
assert.deepEqual(searchHelpArticles('完全无关的问题', WENFAN_HELP_ARTICLES), []);
});
test('getFallbackHelpSuggestions returns starter examples when no article matches', () => {
assert.deepEqual(getFallbackHelpSuggestions().slice(0, 3), [
'怎么新建产品?',
'怎么把需求纳入版本?',
'开发任务怎么提测?',
]);
});

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

View File

@@ -25,6 +25,12 @@ test('wenfan xiaobao page provides records, chat, and voice input surfaces', ()
assert.match(page, /对话/);
assert.match(page, /textarea/);
assert.match(page, /Mic/);
assert.match(page, /searchHelpArticles/);
assert.match(page, /HelpAnswer/);
assert.match(page, /入口路径/);
assert.match(page, /必填字段/);
assert.match(page, /状态流转/);
assert.match(page, /注意事项/);
assert.match(page, /lg:grid-cols-\[300px_minmax\(0,1fr\)\]/);
assert.match(page, /rounded-\[28px\]/);
assert.doesNotMatch(page, /: 'border-\[var\(--line\)\] bg-\[var\(--bg\)\]/);