feat(小宝): 优化助手入口与预警已读

关键改动:

- 更新 README 项目说明和小宝相关入口命名

- 优化问翻小宝页面与侧边栏测试

- 小宝预警已读状态抑制同签名重复 AI 请求

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-07-01 13:35:52 +08:00
parent 756621d366
commit d2a07b0da2
10 changed files with 320 additions and 171 deletions

View File

@@ -62,7 +62,7 @@ const INITIAL_MESSAGES: ChatMessage[] = [
role: 'assistant',
type: 'intro',
content:
'我是问翻小宝,第一阶段只从内置帮助中心回答系统怎么用,不调用 AI也不消耗模型 token。你可以问产品、项目、版本、需求池、开发任务、测试用例、Bug 和日志记录。',
'第一阶段只从内置帮助中心回答系统怎么用,不调用 AI也不消耗模型 token。你可以问产品、项目、版本、需求池、开发任务、测试用例、Bug 和日志记录。',
},
];
@@ -337,7 +337,6 @@ function MessageRow({
<BotMessageSquare className="h-4 w-4" strokeWidth={1.9} />
</div>
<div className="min-w-0 flex-1 pt-0.5">
<p className="mb-1 text-[13px] font-semibold text-[#171717]"></p>
{message.type === 'intro' && <p className="text-[14px] leading-7 text-[#202123]">{message.content}</p>}
{message.type === 'article' && <HelpAnswer result={message.result} onAsk={onAsk} />}
{message.type === 'fallback' && (

View File

@@ -16,6 +16,7 @@ import {
filterXiaobaoRiskWarnings,
formatRemainingWork,
isXiaobaoWarningUpdated,
shouldSkipXiaobaoRiskInsightRequestForReadState,
sanitizeRiskInsight,
} from '@/lib/xiaobao-warning-view';
import { formatDateTime } from '@/lib/format';
@@ -90,6 +91,7 @@ function XiaobaoWarningContent() {
useEffect(() => {
risks.forEach((risk) => {
if (shouldSkipXiaobaoRiskInsightRequestForReadState(risk, readStates, user?.id, readStateLoaded)) return;
const previous = findPreviousRiskSnapshot(snapshots, risk.versionId, today);
const key = buildXiaobaoRiskInsightPendingKey(risk);
if (!shouldRequestRiskInsightWithRequestGate({
@@ -123,11 +125,14 @@ function XiaobaoWarningContent() {
insights,
insightRequestAttempts,
pendingInsightKeys,
readStateLoaded,
readStates,
riskDataLoaded,
risks,
saveInsight,
snapshots,
today,
user?.id,
]);
const risksWithInsight = useMemo(() => risks.map((risk) => attachXiaobaoRiskSuggestion(risk, {

View File

@@ -95,45 +95,45 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel, readOnly = false
};
return (
<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}>
<div className="w-full max-w-md h-full bg-[var(--bg)] border-l border-[var(--line)] shadow-2xl flex flex-col" onClick={(e) => e.stopPropagation()}>
<div className="fixed inset-0 z-50 flex justify-end bg-black/40" onClick={onClose}>
<div className="flex h-full w-full max-w-xl flex-col border-l border-[var(--line)] bg-[var(--bg)]" onClick={(e) => e.stopPropagation()}>
{contextLabel && (
<div className="px-5 py-2 border-b border-[var(--line)] bg-[var(--bg-subtle)] shrink-0">
<span className="text-[11px] text-[var(--ink-muted)]">{contextLabel}</span>
</div>
)}
<div className="flex items-center justify-between h-14 px-5 border-b border-[var(--line)] bg-[var(--bg-card)] shrink-0">
<div className="flex items-center gap-2">
<span className="text-[12px] font-mono text-[var(--ink-muted)]">{bug.bugNo}</span>
<span className="text-[14px] font-semibold text-[var(--ink)] truncate max-w-[240px]">{bug.title}</span>
<div className="flex min-w-0 flex-1 items-center gap-2 pr-3">
<span className="shrink-0 text-[12px] font-mono text-[var(--ink-muted)]">{bug.bugNo}</span>
<span className="min-w-0 flex-1 truncate text-[15px] font-semibold text-[var(--ink)]">{bug.title}</span>
</div>
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-[var(--bg-subtle)]"><X className="h-4 w-4 text-[var(--ink-muted)]" /></button>
<button onClick={onClose} className="shrink-0 p-1.5 rounded-lg hover:bg-[var(--bg-subtle)]"><X className="h-4 w-4 text-[var(--ink-muted)]" /></button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
<div className="flex-1 space-y-4 overflow-y-auto p-5">
{/* 关联链路 */}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-3 space-y-2">
<div className="space-y-2 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide"></div>
{tc && (
<div className="flex items-center gap-2">
<div className="flex min-w-0 items-center gap-2">
<Link2 className="h-3.5 w-3.5 text-[var(--accent)]" />
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{tc.caseNo}</span>
<span className="text-[13px] text-[var(--ink)]">{tc.title}</span>
<span className="min-w-0 truncate text-[13px] text-[var(--ink)]">{tc.title}</span>
</div>
)}
{requirement && (
<div className="flex items-center gap-2 pl-5">
<div className="flex min-w-0 items-center gap-2 pl-5">
<ChevronRight className="h-3 w-3 text-[var(--ink-muted)]" />
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{requirement.code}</span>
<span className="text-[12px] text-[var(--ink-soft)]">{requirement.title}</span>
<span className="min-w-0 truncate text-[12px] text-[var(--ink-soft)]">{requirement.title}</span>
</div>
)}
</div>
{/* 状态 & 操作 */}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 space-y-3">
<div className="space-y-4 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide"> & </div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2">
<BugStatusBadge status={bug.status} />
<span className={`text-[10px] px-2 py-0.5 rounded ${BUG_SEVERITY_COLOR[bug.severity]}`}>{BUG_SEVERITY_LABEL[bug.severity]}</span>
<span className="text-[11px] text-[var(--ink-muted)]">{bug.priority}</span>
@@ -189,9 +189,9 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel, readOnly = false
</div>
{/* 基本信息 */}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-3"></div>
<div className="grid grid-cols-2 gap-y-3 gap-x-4 text-[12px]">
<div className="grid grid-cols-1 gap-2 text-[12px] sm:grid-cols-2 sm:gap-x-4 [&>div]:min-w-0 [&>div]:rounded-md [&>div]:bg-[var(--bg-subtle)] [&>div]:px-3 [&>div]:py-2 [&>div]:leading-5">
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)] font-medium">{bug.plannedFixAt ? formatDateTime(bug.plannedFixAt) : '待排期'}</span></div>
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{reporterName}</span></div>
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)] font-medium">{assigneeName}</span></div>
@@ -202,14 +202,14 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel, readOnly = false
</div>
{/* 描述 */}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-2">Bug </div>
<p className="text-[12px] text-[var(--ink-soft)] leading-relaxed whitespace-pre-wrap">{bug.description}</p>
</div>
{/* 截图 */}
{bug.images && bug.images.length > 0 && (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-2"></div>
<div className="flex flex-wrap gap-2">
{bug.images.map((src, i) => (
@@ -221,7 +221,7 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel, readOnly = false
{/* 修复说明 */}
{bug.resolution && (
<div className="rounded-xl border border-emerald-200 bg-emerald-50 p-4">
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-4">
<div className="text-[10px] text-emerald-700 uppercase tracking-wide mb-2"></div>
<p className="text-[12px] text-emerald-800 leading-relaxed whitespace-pre-wrap">{bug.resolution}</p>
</div>

View File

@@ -196,19 +196,19 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel,
};
return (
<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}>
<div className="w-full max-w-md h-full bg-[var(--bg)] border-l border-[var(--line)] shadow-2xl flex flex-col" onClick={(e) => e.stopPropagation()}>
<div className="fixed inset-0 z-50 flex justify-end bg-black/40" onClick={onClose}>
<div className="flex h-full w-full max-w-xl flex-col border-l border-[var(--line)] bg-[var(--bg)]" onClick={(e) => e.stopPropagation()}>
{contextLabel && (
<div className="px-5 py-2 border-b border-[var(--line)] bg-[var(--bg-subtle)] shrink-0">
<span className="text-[11px] text-[var(--ink-muted)]">{contextLabel}</span>
</div>
)}
<div className="flex items-center justify-between h-14 px-5 border-b border-[var(--line)] bg-[var(--bg-card)] shrink-0">
<div className="flex items-center gap-2">
<span className="text-[12px] font-mono text-[var(--ink-muted)]">{task.taskNo}</span>
<span className="text-[14px] font-semibold text-[var(--ink)] truncate max-w-[240px]">{task.title}</span>
<div className="flex min-w-0 flex-1 items-center gap-2 pr-3">
<span className="shrink-0 text-[12px] font-mono text-[var(--ink-muted)]">{task.taskNo}</span>
<span className="min-w-0 flex-1 truncate text-[15px] font-semibold text-[var(--ink)]">{task.title}</span>
</div>
<div className="flex items-center gap-1">
<div className="flex shrink-0 items-center gap-1">
{!readOnly && task.status !== 'submitted' && (
<button onClick={() => setShowTransfer(!showTransfer)} className="p-1.5 rounded-lg hover:bg-blue-50 text-[var(--ink-muted)] hover:text-blue-500" title="转交"><ArrowRightLeft className="h-4 w-4" /></button>
)}
@@ -233,22 +233,22 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel,
</div>
)}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
<div className="flex-1 space-y-4 overflow-y-auto p-5">
{requirement && (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-3">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-1.5"></div>
<div className="flex items-center gap-2">
<div className="flex min-w-0 items-center gap-2">
<Link2 className="h-3.5 w-3.5 text-[var(--accent)]" />
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{requirement.code}</span>
<span className="text-[13px] text-[var(--ink)]">{requirement.title}</span>
<span className="min-w-0 truncate text-[13px] text-[var(--ink)]">{requirement.title}</span>
</div>
</div>
)}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 space-y-3">
<div className="space-y-4 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide"> & </div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2">
<span className={`inline-flex items-center rounded-lg px-3 py-1.5 text-[13px] font-semibold ${DEV_TASK_STATUS_COLOR[task.status]}`}>
{DEV_TASK_STATUS_LABEL[task.status]}
</span>
@@ -295,7 +295,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel,
<div className="text-[11px] font-medium text-orange-700">
{needsClaim ? '领取并填写计划' : '填写计划'}
</div>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<label className="mb-1 block text-[11px] text-orange-700"></label>
<WorkDateTimePicker
@@ -368,7 +368,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel,
</div>
{task.status !== 'todo' && task.status !== 'submitted' && !readOnly && (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 space-y-3">
<div className="space-y-4 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide"></div>
<textarea
value={progressNote}
@@ -377,7 +377,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel,
rows={3}
className="w-full resize-none rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 py-2 text-[12px] leading-5 text-[var(--ink)] focus:border-[var(--accent)] focus:outline-none"
/>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<input
value={progressBlocker}
onChange={(e) => setProgressBlocker(e.target.value)}
@@ -425,9 +425,9 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel,
</div>
)}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-3 flex items-center gap-1.5"><CalendarRange className="h-3 w-3" /></div>
<div className="grid grid-cols-2 gap-y-2.5 gap-x-4 text-[12px]">
<div className="grid grid-cols-1 gap-2 text-[12px] sm:grid-cols-2 sm:gap-x-4 [&>div]:min-w-0 [&>div]:rounded-md [&>div]:bg-[var(--bg-subtle)] [&>div]:px-3 [&>div]:py-2">
<div className="flex flex-col gap-0.5">
<span className="text-[10px] text-[var(--ink-muted)]"></span>
<span className="text-[var(--ink)] font-medium tabular-nums">{task.expectedStartAt ? formatShortTime(task.expectedStartAt) : '-'}</span>
@@ -475,9 +475,9 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel,
)}
</div>
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-3"></div>
<div className="grid grid-cols-2 gap-y-3 gap-x-4 text-[12px]">
<div className="grid grid-cols-1 gap-2 text-[12px] sm:grid-cols-2 sm:gap-x-4 [&>div]:min-w-0 [&>div]:rounded-md [&>div]:bg-[var(--bg-subtle)] [&>div]:px-3 [&>div]:py-2">
<div className="flex items-center gap-2">
<Tag className="h-3 w-3 text-[var(--ink-muted)]" />
<span className="text-[var(--ink-muted)]"></span>
@@ -508,7 +508,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel,
<ActivityLogPanel sourceType="dev_task" sourceId={task.id} />
{predecessors.length > 0 && (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-2"></div>
<div className="space-y-2">
{predecessors.map((p: any) => (

View File

@@ -30,8 +30,8 @@ const NAV_GROUPS = [
{
label: '工作区',
items: [
{ label: '问翻小宝', path: '/wenfan-xiaobao', icon: MessageCircleQuestionMark, permission: null as string | null },
{ label: '小宝预警', path: '/xiaobao-warning', icon: TriangleAlert, permission: 'xiaobao.warning:view', badge: 'xiaobao-risk' as const },
{ label: 'AI 助手', path: '/wenfan-xiaobao', icon: MessageCircleQuestionMark, permission: null as string | null },
{ label: '智能预警', path: '/xiaobao-warning', icon: TriangleAlert, permission: 'xiaobao.warning:view', badge: 'xiaobao-risk' as const },
{ label: '与我相关', path: '/workspace', icon: Inbox, permission: null as string | null, badge: 'workspace-pending' as const },
{ label: '产品', path: '/products', icon: Package, permission: 'product:view' },
{ label: '项目', path: '/projects', icon: FolderKanban, permission: 'project:view' },

View File

@@ -130,19 +130,19 @@ export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug, context
};
return (
<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}>
<div className="w-full max-w-md h-full bg-[var(--bg)] border-l border-[var(--line)] shadow-2xl flex flex-col" onClick={(e) => e.stopPropagation()}>
<div className="fixed inset-0 z-50 flex justify-end bg-black/40" onClick={onClose}>
<div className="flex h-full w-full max-w-xl flex-col border-l border-[var(--line)] bg-[var(--bg)]" onClick={(e) => e.stopPropagation()}>
{contextLabel && (
<div className="px-5 py-2 border-b border-[var(--line)] bg-[var(--bg-subtle)] shrink-0">
<span className="text-[11px] text-[var(--ink-muted)]">{contextLabel}</span>
</div>
)}
<div className="flex items-center justify-between h-14 px-5 border-b border-[var(--line)] bg-[var(--bg-card)] shrink-0">
<div className="flex items-center gap-2">
<span className="text-[12px] font-mono text-[var(--ink-muted)]">{tc.caseNo}</span>
<span className="text-[14px] font-semibold text-[var(--ink)] truncate max-w-[240px]">{tc.title}</span>
<div className="flex min-w-0 flex-1 items-center gap-2 pr-3">
<span className="shrink-0 text-[12px] font-mono text-[var(--ink-muted)]">{tc.caseNo}</span>
<span className="min-w-0 flex-1 truncate text-[15px] font-semibold text-[var(--ink)]">{tc.title}</span>
</div>
<div className="flex items-center gap-1">
<div className="flex shrink-0 items-center gap-1">
{!readOnly && tc.status !== 'passed' && (
<button onClick={() => setShowTransfer(!showTransfer)} className="p-1.5 rounded-lg hover:bg-blue-50 text-[var(--ink-muted)] hover:text-blue-500" title="转交"><ArrowRightLeft className="h-4 w-4" /></button>
)}
@@ -168,29 +168,29 @@ export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug, context
</div>
)}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
<div className="flex-1 space-y-4 overflow-y-auto p-5">
{/* 关联需求 */}
{requirement && (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-3">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-1.5"></div>
<div className="flex items-center gap-2">
<div className="flex min-w-0 items-center gap-2">
<Link2 className="h-3.5 w-3.5 text-[var(--accent)]" />
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{requirement.code}</span>
<span className="text-[13px] text-[var(--ink)]">{requirement.title}</span>
<span className="min-w-0 truncate text-[13px] text-[var(--ink)]">{requirement.title}</span>
</div>
</div>
)}
{/* 状态 & 操作 */}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 space-y-3">
<div className="space-y-4 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide"> & </div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2">
<TestCaseStatusBadge status={tc.status} />
{tc.executedAt && <span className="text-[11px] text-[var(--ink-muted)]"> {tc.executedAt}</span>}
</div>
{visibleNextStatuses.length > 0 && !showFailInput && !showBlockInput && !readOnly && (
<div className="flex items-center gap-2 pt-1">
<div className="flex flex-wrap items-center gap-2 pt-1">
<ChevronRight className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
{visibleNextStatuses.map((s) => (
<button key={s} onClick={() => handleTransition(s)} className={`h-8 px-4 rounded-lg text-[12px] font-medium transition-colors ${s === 'passed' ? 'bg-emerald-500 text-white hover:bg-emerald-600' : s === 'failed' ? 'bg-red-500 text-white hover:bg-red-600' : 'bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]'}`}>
@@ -220,7 +220,7 @@ export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug, context
<div className="text-[11px] font-medium text-orange-700">
{needsClaim ? '领取并填写计划' : '填写计划'}
</div>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<label className="mb-1 block text-[11px] text-orange-700"></label>
<WorkDateTimePicker
@@ -276,9 +276,9 @@ export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug, context
</div>
{/* 基本信息 */}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-3"></div>
<div className="grid grid-cols-2 gap-y-3 gap-x-4 text-[12px]">
<div className="grid grid-cols-1 gap-2 text-[12px] sm:grid-cols-2 sm:gap-x-4 [&>div]:min-w-0 [&>div]:rounded-md [&>div]:bg-[var(--bg-subtle)] [&>div]:px-3 [&>div]:py-2 [&>div]:leading-5">
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)] font-medium">{planDisplay}</span></div>
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)] font-medium">{tc.priority}</span></div>
<div className="flex items-center gap-1.5"><span className="text-[var(--ink-muted)]"></span><CategoryChip category={category} /></div>
@@ -295,14 +295,14 @@ export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug, context
{/* 用例描述 */}
{tc.description && (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-2"> & </div>
<p className="text-[12px] text-[var(--ink-soft)] leading-relaxed whitespace-pre-wrap">{tc.description}</p>
</div>
)}
{/* 关联 Bug + 提BUG按钮 */}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="flex items-center justify-between mb-2">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide"> Bug ({relatedBugs.length})</div>
{tc.status === 'failed' && onCreateBug && !readOnly && (

View File

@@ -3,15 +3,17 @@ import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
test('sidebar links wenfan xiaobao as an independent entry before xiaobao warning', () => {
test('sidebar labels AI assistant before smart warning', () => {
const sidebar = readFileSync(join(process.cwd(), 'components/layout/Sidebar.tsx'), 'utf8');
const askIndex = sidebar.indexOf("label: '问翻小宝'");
const warningIndex = sidebar.indexOf("label: '小宝预警'");
const askIndex = sidebar.indexOf("label: 'AI 助手'");
const warningIndex = sidebar.indexOf("label: '智能预警'");
assert.notEqual(askIndex, -1);
assert.notEqual(warningIndex, -1);
assert.ok(askIndex < warningIndex);
assert.doesNotMatch(sidebar, /label: '问翻小宝'/);
assert.doesNotMatch(sidebar, /label: '小宝预警'/);
assert.match(sidebar, /path: '\/wenfan-xiaobao'/);
});
@@ -36,6 +38,7 @@ test('wenfan xiaobao page provides records, chat, and voice input surfaces', ()
assert.match(page, /deleteWenfanConversationRecord/);
assert.match(page, /删除历史记录/);
assert.doesNotMatch(page, /setHistory\(\(current\) => \[question/);
assert.doesNotMatch(page, /<p className="mb-1 text-\[13px\] font-semibold text-\[#171717\]">问翻小宝<\/p>/);
assert.match(page, /入口路径/);
assert.match(page, /必填字段/);
assert.match(page, /状态流转/);

View File

@@ -12,6 +12,7 @@ import {
getXiaobaoWarningUpdateSignature,
isXiaobaoWarningUpdated,
markXiaobaoWarningRead,
shouldSkipXiaobaoRiskInsightRequestForReadState,
sanitizeRiskInsight,
} from './xiaobao-warning-view';
@@ -192,6 +193,31 @@ test('xiaobao warning update state is unread until the current user reads the sa
assert.equal(isXiaobaoWarningUpdated(baseRisk, readStates, 'user-2'), true);
});
test('read current warning state suppresses refresh-triggered AI requests only for the same signature', () => {
const baseRisk = risk();
const readStates = markXiaobaoWarningRead([], 'user-1', baseRisk, '2026-06-30T11:00:00.000Z');
const changedRisk = risk({
signals: {
...baseRisk.signals,
failedTestCount: 1,
},
riskScore: 72,
});
assert.equal(
shouldSkipXiaobaoRiskInsightRequestForReadState(baseRisk, readStates, 'user-1', true),
true,
);
assert.equal(
shouldSkipXiaobaoRiskInsightRequestForReadState(changedRisk, readStates, 'user-1', true),
false,
);
assert.equal(
shouldSkipXiaobaoRiskInsightRequestForReadState(baseRisk, readStates, 'user-1', false),
true,
);
});
test('xiaobao warning update state becomes unread again when risk facts change', () => {
const baseRisk = risk();
const readStates = markXiaobaoWarningRead([], 'user-1', baseRisk, '2026-06-30T11:00:00.000Z');

View File

@@ -87,6 +87,17 @@ export function isXiaobaoWarningUpdated(
return readSignature !== currentSignature;
}
export function shouldSkipXiaobaoRiskInsightRequestForReadState(
risk: XiaobaoVersionRisk,
readStates: XiaobaoWarningReadState[],
userId: string | undefined,
readStateLoaded: boolean,
): boolean {
if (!userId) return false;
if (!readStateLoaded) return true;
return !isXiaobaoWarningUpdated(risk, readStates, userId);
}
export function getXiaobaoWarningUnreadUpdateCount(
risks: XiaobaoVersionRisk[],
readStates: XiaobaoWarningReadState[],