feat(问翻小宝): 增强帮助搜索与会话历史
关键改动: - 增加问翻小宝会话历史记录规则与测试 - 优化帮助搜索、兜底建议和页面交互结构 - 更新帮助截图资产和截图采集脚本 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
54
apps/web/lib/wenfan-conversation-history.test.ts
Normal file
54
apps/web/lib/wenfan-conversation-history.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createWenfanConversationRecord,
|
||||
deleteWenfanConversationRecord,
|
||||
updateWenfanConversationRecord,
|
||||
} from './wenfan-conversation-history';
|
||||
|
||||
type TestMessage = {
|
||||
role: 'assistant' | 'user';
|
||||
content?: string;
|
||||
};
|
||||
|
||||
const introMessage: TestMessage = {
|
||||
role: 'assistant',
|
||||
content: '我是问翻小宝',
|
||||
};
|
||||
|
||||
test('createWenfanConversationRecord creates history only from the new conversation action', () => {
|
||||
const record = createWenfanConversationRecord('conv-1', [introMessage]);
|
||||
|
||||
assert.equal(record.id, 'conv-1');
|
||||
assert.equal(record.title, '新对话');
|
||||
assert.deepEqual(record.messages, [introMessage]);
|
||||
});
|
||||
|
||||
test('updateWenfanConversationRecord updates an existing conversation without adding a question record', () => {
|
||||
const record = createWenfanConversationRecord('conv-1', [introMessage]);
|
||||
const messages: TestMessage[] = [
|
||||
introMessage,
|
||||
{ role: 'user', content: '怎么新建产品?' },
|
||||
{ role: 'assistant', content: '产品创建帮助' },
|
||||
{ role: 'user', content: '怎么新建项目?' },
|
||||
];
|
||||
|
||||
const next = updateWenfanConversationRecord([record], 'conv-1', messages);
|
||||
|
||||
assert.equal(next.length, 1);
|
||||
assert.equal(next[0]?.title, '怎么新建产品?');
|
||||
assert.deepEqual(next[0]?.messages, messages);
|
||||
});
|
||||
|
||||
test('updateWenfanConversationRecord does not create history when there is no active conversation', () => {
|
||||
const messages: TestMessage[] = [introMessage, { role: 'user', content: '怎么新建产品?' }];
|
||||
|
||||
assert.deepEqual(updateWenfanConversationRecord([], null, messages), []);
|
||||
});
|
||||
|
||||
test('deleteWenfanConversationRecord removes a conversation history item', () => {
|
||||
const first = createWenfanConversationRecord('conv-1', [introMessage]);
|
||||
const second = createWenfanConversationRecord('conv-2', [introMessage]);
|
||||
|
||||
assert.deepEqual(deleteWenfanConversationRecord([first, second], 'conv-1'), [second]);
|
||||
});
|
||||
53
apps/web/lib/wenfan-conversation-history.ts
Normal file
53
apps/web/lib/wenfan-conversation-history.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type WenfanHistoryMessage = {
|
||||
role: string;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
export type WenfanConversationRecord<TMessage extends WenfanHistoryMessage> = {
|
||||
id: string;
|
||||
title: string;
|
||||
messages: TMessage[];
|
||||
};
|
||||
|
||||
const DEFAULT_CONVERSATION_TITLE = '新对话';
|
||||
|
||||
function getConversationTitle<TMessage extends WenfanHistoryMessage>(messages: TMessage[]): string {
|
||||
const firstQuestion = messages.find((message) => message.role === 'user' && message.content?.trim());
|
||||
return firstQuestion?.content?.trim() || DEFAULT_CONVERSATION_TITLE;
|
||||
}
|
||||
|
||||
export function createWenfanConversationRecord<TMessage extends WenfanHistoryMessage>(
|
||||
id: string,
|
||||
messages: TMessage[],
|
||||
): WenfanConversationRecord<TMessage> {
|
||||
return {
|
||||
id,
|
||||
title: getConversationTitle(messages),
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateWenfanConversationRecord<TMessage extends WenfanHistoryMessage>(
|
||||
records: WenfanConversationRecord<TMessage>[],
|
||||
activeId: string | null | undefined,
|
||||
messages: TMessage[],
|
||||
): WenfanConversationRecord<TMessage>[] {
|
||||
if (!activeId) return records;
|
||||
|
||||
return records.map((record) =>
|
||||
record.id === activeId
|
||||
? {
|
||||
...record,
|
||||
title: getConversationTitle(messages),
|
||||
messages,
|
||||
}
|
||||
: record,
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteWenfanConversationRecord<TMessage extends WenfanHistoryMessage>(
|
||||
records: WenfanConversationRecord<TMessage>[],
|
||||
id: string,
|
||||
): WenfanConversationRecord<TMessage>[] {
|
||||
return records.filter((record) => record.id !== id);
|
||||
}
|
||||
@@ -435,7 +435,7 @@ export const WENFAN_HELP_ARTICLES: HelpArticle[] = [
|
||||
},
|
||||
{
|
||||
id: 'bug-workflow',
|
||||
title: 'Bug 如何新建和流转',
|
||||
title: 'Bug 如何从失败用例创建和流转',
|
||||
category: 'Bug',
|
||||
keywords: [
|
||||
'Bug',
|
||||
@@ -448,16 +448,25 @@ export const WENFAN_HELP_ARTICLES: HelpArticle[] = [
|
||||
'提Bug',
|
||||
'怎么提bug',
|
||||
'测试失败提bug',
|
||||
'失败后提Bug',
|
||||
'失败后提 BUG',
|
||||
'测试用例失败后提Bug',
|
||||
'测试用例不通过提Bug',
|
||||
'不通过后提Bug',
|
||||
'从失败用例提Bug',
|
||||
'失败用例提 BUG',
|
||||
'修复Bug',
|
||||
'验证Bug',
|
||||
'关闭Bug',
|
||||
'Bug状态',
|
||||
],
|
||||
scenario: '当测试用例失败或线上发现问题时,创建 Bug 并跟踪修复、验证和关闭。',
|
||||
entry: '版本详情 -> Bug Tab -> 新建 Bug,或测试用例失败后点击提 Bug',
|
||||
scenario: '当测试用例执行失败后,从失败用例里提 Bug,并跟踪修复、验证和关闭。',
|
||||
entry: '版本详情 -> 测试用例 Tab -> 打开失败/不通过用例 -> 关联 Bug 区域点击提 BUG',
|
||||
steps: [
|
||||
'从 Bug Tab 点击新建 Bug,或在失败测试用例里点击提 Bug。',
|
||||
'填写 Bug 标题、严重程度、关联版本、关联测试用例、复现步骤。',
|
||||
'打开版本详情并进入测试用例 Tab。',
|
||||
'执行测试用例,并将问题用例标记为不通过/失败。',
|
||||
'打开该失败测试用例详情,在“关联 Bug”区域点击提 BUG。',
|
||||
'填写 Bug 标题、严重程度、复现步骤;版本和测试用例来源会自动带入。',
|
||||
'分配修复负责人和计划修复时间。',
|
||||
'负责人将 Bug 切换到修复中。',
|
||||
'修复完成后切换为待验证。',
|
||||
@@ -466,11 +475,12 @@ export const WENFAN_HELP_ARTICLES: HelpArticle[] = [
|
||||
requiredFields: ['Bug 标题', '严重程度', '所属版本', '复现步骤', '负责人', '计划修复时间'],
|
||||
statusFlow: ['待修复', '修复中', '待验证', '已关闭', '已拒绝'],
|
||||
notes: [
|
||||
'当前不能从 Bug Tab 直接新建 Bug;Bug Tab 用于查看、筛选和跟踪已由失败用例提出的 Bug。',
|
||||
'Bug 直接挂在版本上,测试用例只是来源追踪。',
|
||||
'关键 Bug 会影响小宝预警风险等级。',
|
||||
'关闭前应确认修复结果和验证记录。',
|
||||
],
|
||||
images: [helpImage('bug-workflow', 'Bug 页面截图', '在 Bug Tab 新建并跟踪 Bug。')],
|
||||
images: [helpImage('bug-workflow', '失败用例提 Bug 截图', '在失败测试用例详情中点击提 BUG,并在 Bug Tab 跟踪修复。')],
|
||||
relatedIds: ['test-case-workflow', 'xiaobao-warning', 'activity-logs'],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { getFallbackHelpSuggestions, searchHelpArticles } from './wenfan-help-search';
|
||||
import {
|
||||
getFallbackHelpMessage,
|
||||
getFallbackHelpSuggestions,
|
||||
searchHelpArticles,
|
||||
shouldShowGenericHelpSuggestions,
|
||||
} from './wenfan-help-search';
|
||||
import { WENFAN_HELP_ARTICLES } from './wenfan-help-articles';
|
||||
|
||||
test('searchHelpArticles matches common product creation phrasing', () => {
|
||||
@@ -27,6 +32,17 @@ test('searchHelpArticles matches bug creation from failed test cases', () => {
|
||||
assert.equal(result[0]?.article.id, 'bug-workflow');
|
||||
});
|
||||
|
||||
test('bug help article explains bug creation only from failed test cases', () => {
|
||||
const article = WENFAN_HELP_ARTICLES.find((item) => item.id === 'bug-workflow');
|
||||
|
||||
assert.ok(article);
|
||||
assert.match(article.entry, /测试用例/);
|
||||
assert.match(article.entry, /提 BUG|提 Bug/);
|
||||
assert.doesNotMatch(article.entry, /Bug Tab -> 新建 Bug/);
|
||||
assert.ok(article.steps.some((step) => /失败|不通过/.test(step) && /提 BUG|提 Bug/.test(step)));
|
||||
assert.ok(article.notes.some((note) => /不支持|不能/.test(note) && /Bug Tab/.test(note)));
|
||||
});
|
||||
|
||||
test('searchHelpArticles keeps related article ids from the matched article', () => {
|
||||
const result = searchHelpArticles('AI拆解测试用例在哪里', WENFAN_HELP_ARTICLES);
|
||||
|
||||
@@ -46,3 +62,16 @@ test('getFallbackHelpSuggestions returns starter examples when no article matche
|
||||
'开发任务怎么提测?',
|
||||
]);
|
||||
});
|
||||
|
||||
test('getFallbackHelpMessage removes generic prompt wording when generic suggestions are hidden', () => {
|
||||
assert.match(getFallbackHelpMessage(true), /常见问题/);
|
||||
assert.doesNotMatch(getFallbackHelpMessage(false), /常见问题/);
|
||||
assert.match(getFallbackHelpMessage(false), /更具体的系统关键词/);
|
||||
});
|
||||
|
||||
test('shouldShowGenericHelpSuggestions hides generic prompts after the first user follow-up', () => {
|
||||
assert.equal(shouldShowGenericHelpSuggestions(0), true);
|
||||
assert.equal(shouldShowGenericHelpSuggestions(1), true);
|
||||
assert.equal(shouldShowGenericHelpSuggestions(2), false);
|
||||
assert.equal(shouldShowGenericHelpSuggestions(5), false);
|
||||
});
|
||||
|
||||
@@ -103,3 +103,14 @@ export function searchHelpArticles(
|
||||
export function getFallbackHelpSuggestions(): string[] {
|
||||
return FALLBACK_SUGGESTIONS;
|
||||
}
|
||||
|
||||
export function getFallbackHelpMessage(showGenericSuggestions: boolean): string {
|
||||
if (showGenericSuggestions) {
|
||||
return '暂时没有找到对应的内置帮助内容。你可以换个关键词,或者从下面这些常见问题开始。';
|
||||
}
|
||||
return '暂时没有找到对应的内置帮助内容。你可以换个更具体的系统关键词,或继续描述你要操作的模块。';
|
||||
}
|
||||
|
||||
export function shouldShowGenericHelpSuggestions(userQuestionCount: number): boolean {
|
||||
return userQuestionCount < 2;
|
||||
}
|
||||
|
||||
@@ -27,12 +27,42 @@ test('wenfan xiaobao page provides records, chat, and voice input surfaces', ()
|
||||
assert.match(page, /Mic/);
|
||||
assert.match(page, /searchHelpArticles/);
|
||||
assert.match(page, /HelpAnswer/);
|
||||
assert.match(page, /shouldShowGenericHelpSuggestions/);
|
||||
assert.match(page, /showGenericSuggestions/);
|
||||
assert.match(page, /showStarterQuestions/);
|
||||
assert.match(page, /showGenericSuggestions && message\.suggestions\.length > 0/);
|
||||
assert.match(page, /createWenfanConversationRecord/);
|
||||
assert.match(page, /updateWenfanConversationRecord/);
|
||||
assert.match(page, /deleteWenfanConversationRecord/);
|
||||
assert.match(page, /删除历史记录/);
|
||||
assert.doesNotMatch(page, /setHistory\(\(current\) => \[question/);
|
||||
assert.match(page, /入口路径/);
|
||||
assert.match(page, /必填字段/);
|
||||
assert.match(page, /状态流转/);
|
||||
assert.match(page, /注意事项/);
|
||||
assert.match(page, /previewImage/);
|
||||
assert.match(page, /aria-modal="true"/);
|
||||
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\)\]/);
|
||||
assert.match(page, /: 'bg-white hover:bg-\[var\(--bg-subtle\)\]'/);
|
||||
});
|
||||
|
||||
test('wenfan help screenshot script targets version detail tabs and failed test case bug entry', () => {
|
||||
const script = readFileSync(join(process.cwd(), 'scripts/capture-wenfan-help-screenshots.mjs'), 'utf8');
|
||||
|
||||
assert.match(script, /resolveScreenshotContext/);
|
||||
assert.match(script, /loadDataKey\('test-cases'\)/);
|
||||
assert.match(script, /loadDataKey\('version-plans'\)/);
|
||||
assert.match(script, /findFailedTestCase/);
|
||||
assert.match(script, /findAiReadyProductPlan/);
|
||||
assert.match(script, /openProductPlanAiEntry/);
|
||||
assert.match(script, /tabLabel: '关联需求'/);
|
||||
assert.match(script, /tabLabel: '产品方案'/);
|
||||
assert.match(script, /tabLabel: '开发任务'/);
|
||||
assert.match(script, /tabLabel: '测试用例'/);
|
||||
assert.match(script, /openFailedTestCaseBugEntry/);
|
||||
assert.match(script, /提 BUG/);
|
||||
assert.doesNotMatch(script, /\{ file: 'bug-workflow\.png', route: '\/versions' \}/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user