feat(ai): 优化拆解重跑与结果展示

This commit is contained in:
Script Generator
2026-06-26 11:01:11 +08:00
parent 55e24442ab
commit 56e0fe562d
36 changed files with 1342 additions and 186 deletions

View File

@@ -9,9 +9,10 @@ import { useTestCaseStore } from '@/stores/useTestCaseStore';
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
import { useAuthStore } from '@/stores/useAuthStore';
import { findCategoryByCode, resolveCategoryIdFromCode } from '@/lib/task-category';
import { addWorkHours } from '@/lib/work-hours';
import { clampDevEstimateHours, clampTestCaseEstimateHours } from '@/lib/ai-estimation-policy';
import { clampDevAiEstimateHours, clampTestCaseAiEstimateHours } from '@/lib/ai-estimation-policy';
import { formatReportRequirementLabel } from '@/lib/ai-decompose-report';
import type {
AgentDecomposeTarget,
AgentDecomposeResponse,
AgentDevTaskDraft,
AgentTestCaseDraft,
@@ -22,16 +23,22 @@ interface Props {
version: VersionWithContext;
plan: VersionPlan;
requirements: Array<{ id: string; code: string; title: string; description?: string }>;
target: AgentDecomposeTarget;
dedupeSummary?: { removedDevTaskCount: number; removedTestCaseCount: number };
onClose: () => void;
}
export function DecomposeReportModal({ result, version, plan, requirements, onClose }: Props) {
export function DecomposeReportModal({ result, version, plan, requirements, target, dedupeSummary, onClose }: Props) {
const { createTask } = useDevTaskStore();
const { createTestCase } = useTestCaseStore();
const { categories } = useTaskCategoryStore();
const user = useAuthStore((s) => s.user);
const { result: data, meta } = result;
const showDevDrafts = target !== 'test_cases';
const showTestDrafts = target !== 'dev_tasks';
const filteredDuplicateCount =
(dedupeSummary?.removedDevTaskCount ?? 0) + (dedupeSummary?.removedTestCaseCount ?? 0);
const [selectedDevIdx, setSelectedDevIdx] = useState<Set<number>>(
new Set(data.devTaskDrafts.map((_, i) => i)),
@@ -90,9 +97,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
const reqRef = refs.find((r) => r.type === 'requirement');
const requirementId = reqRef?.id ?? requirements[0]?.id ?? '';
const categoryId = resolveCategoryIdFromCode(categories, draft.categoryCode, 'development');
const estimateHours = clampDevEstimateHours(draft.categoryCode, draft.estimateHours);
const startISO = new Date().toISOString();
const endISO = addWorkHours(startISO, estimateHours);
const aiEstimateHours = clampDevAiEstimateHours(draft.categoryCode, draft.aiEstimateHours);
createTask({
requirementId,
@@ -102,9 +107,10 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
assigneeId: '',
reviewerId: undefined,
priority: draft.priority,
expectedStartAt: startISO,
expectedEndAt: endISO,
estimateHours,
expectedStartAt: '',
expectedEndAt: '',
estimateHours: undefined,
aiEstimateHours,
actualStartAt: undefined,
actualEndAt: undefined,
status: 'todo',
@@ -129,7 +135,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
const refs = normalizeRefs(draft.references);
const reqRef = refs.find((r) => r.type === 'requirement');
const categoryId = resolveCategoryIdFromCode(categories, draft.categoryCode, 'testing');
const estimateHours = clampTestCaseEstimateHours(draft.categoryCode, draft.estimateHours);
const aiEstimateHours = clampTestCaseAiEstimateHours(draft.categoryCode, draft.aiEstimateHours);
createTestCase({
versionId: version.id,
@@ -138,7 +144,8 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
description: draft.description,
categoryId,
priority: draft.priority,
estimateHours,
estimateHours: undefined,
aiEstimateHours,
assigneeId: undefined,
references: refs,
aiDraft: true,
@@ -155,6 +162,8 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
setTimeout(() => onClose(), 1500);
};
const selectedVisibleCount = (showDevDrafts ? selectedDevIdx.size : 0) + (showTestDrafts ? selectedTcIdx.size : 0);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
<div
@@ -178,6 +187,12 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
{/* Body */}
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4">
{filteredDuplicateCount > 0 && (
<div className="rounded-lg border border-purple-200 bg-purple-50 px-3 py-2 text-[12px] text-purple-700">
{dedupeSummary?.removedDevTaskCount ?? 0} {dedupeSummary?.removedTestCaseCount ?? 0}
</div>
)}
{/* 对账报告 */}
<section>
<h4 className="text-[12px] font-semibold text-[var(--ink-soft)] mb-2"></h4>
@@ -192,8 +207,14 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
</div>
<ul className="text-[12px] text-emerald-800 space-y-1 ml-5">
{data.report.matched.map((m, i) => (
<li key={i}>
{m.reqId} {m.noteIds.join(', ') || '(仅需求驱动)'} {m.taskCount}
<li key={i} className="flex flex-wrap items-center gap-1">
<span
className="max-w-[280px] truncate font-medium"
title={formatReportRequirementLabel(m.reqId, requirements)}
>
{formatReportRequirementLabel(m.reqId, requirements)}
</span>
<span> {m.noteIds.join(', ') || '(仅需求驱动)'} {m.taskCount} </span>
</li>
))}
</ul>
@@ -210,7 +231,10 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
</p>
<ul className="text-[12px] text-amber-800 ml-5 list-disc">
{data.report.reqOnly.map((id, i) => <li key={i}>{id}</li>)}
{data.report.reqOnly.map((id, i) => {
const label = formatReportRequirementLabel(id, requirements);
return <li key={i} className="max-w-[360px] truncate" title={label}>{label}</li>;
})}
</ul>
</div>
)}
@@ -248,6 +272,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
</section>
{/* DevTask 草案 */}
{showDevDrafts && (
<section>
<h4 className="text-[12px] font-semibold text-[var(--ink-soft)] mb-2">
{data.devTaskDrafts.length}
@@ -277,7 +302,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
{d.priority}
</span>
<span className="text-[10px] text-[var(--ink-muted)]">
{clampDevEstimateHours(d.categoryCode, d.estimateHours)}h
AI预估 {clampDevAiEstimateHours(d.categoryCode, d.aiEstimateHours)}h
</span>
</div>
{d.description && (
@@ -299,8 +324,10 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
</div>
)}
</section>
)}
{/* TestCase 草案 */}
{showTestDrafts && (
<section>
<h4 className="text-[12px] font-semibold text-[var(--ink-soft)] mb-2">
{data.testCaseDrafts.length}
@@ -330,7 +357,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
{d.priority}
</span>
<span className="text-[10px] text-[var(--ink-muted)]">
{clampTestCaseEstimateHours(d.categoryCode, d.estimateHours)}h
AI预估 {clampTestCaseAiEstimateHours(d.categoryCode, d.aiEstimateHours)}h
</span>
</div>
<pre className="mt-1 text-[11px] text-[var(--ink-soft)] whitespace-pre-wrap font-sans line-clamp-3">
@@ -352,12 +379,13 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
</div>
)}
</section>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between px-5 py-3 border-t border-[var(--line)]">
<div className="text-[12px] text-[var(--ink-muted)]">
{selectedDevIdx.size} + {selectedTcIdx.size}
{showDevDrafts ? selectedDevIdx.size : 0} + {showTestDrafts ? selectedTcIdx.size : 0}
</div>
<div className="flex gap-2">
<button
@@ -368,7 +396,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
</button>
<button
onClick={handleAdopt}
disabled={submitting || submitted || (selectedDevIdx.size === 0 && selectedTcIdx.size === 0)}
disabled={submitting || submitted || selectedVisibleCount === 0}
className="h-8 px-4 rounded-lg text-[12px] font-medium bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-50"
>
{submitted ? '✓ 已采纳' : submitting ? '采纳中...' : '采纳选中'}