feat(ai): 优化拆解目标和负责人推荐

This commit is contained in:
Script Generator
2026-06-29 16:19:52 +08:00
parent 0e27a4d4c5
commit 7f24e16491
19 changed files with 658 additions and 118 deletions

View File

@@ -12,6 +12,7 @@ import { useTestCaseStore } from '@/stores/useTestCaseStore';
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
import { api } from '@/lib/api';
import { filterDuplicateDecomposeDrafts } from '@/lib/ai-decompose-dedupe';
import { filterDecomposeResultByTarget } from '@/lib/ai-decompose-target';
import { DecomposeReportModal } from './DecomposeReportModal';
import type {
AgentDecomposeTarget,
@@ -25,6 +26,12 @@ interface Props {
version: VersionWithContext;
}
interface DecomposeModalState {
result: AgentDecomposeResponse;
target: AgentDecomposeTarget;
dedupeSummary: { removedDevTaskCount: number; removedTestCaseCount: number };
}
/** in_progress 视为"卡死"的阈值(秒)— 超过这个时间,按钮允许重新点击 */
const STUCK_THRESHOLD_SEC = 240;
@@ -44,9 +51,7 @@ export function AiDecomposeButton({ plan, version }: Props) {
const user = useAuthStore((s) => s.user);
const [activeTarget, setActiveTarget] = useState<AgentDecomposeTarget | null>(null);
const [result, setResult] = useState<AgentDecomposeResponse | null>(null);
const [resultTarget, setResultTarget] = useState<AgentDecomposeTarget>('all');
const [dedupeSummary, setDedupeSummary] = useState({ removedDevTaskCount: 0, removedTestCaseCount: 0 });
const [modalState, setModalState] = useState<DecomposeModalState | null>(null);
const [tick, setTick] = useState(0);
const startedAtRef = useRef<number | null>(null);
const loading = activeTarget !== null;
@@ -55,6 +60,7 @@ export function AiDecomposeButton({ plan, version }: Props) {
const persistStatus = plan.aiDecomposeStatus;
const persistError = plan.aiDecomposeError;
const persistAt = plan.aiDecomposeAt;
const persistTarget = plan.aiDecomposeTarget;
// 计算"已用时"
const elapsedSec = (() => {
@@ -72,6 +78,7 @@ export function AiDecomposeButton({ plan, version }: Props) {
const isInProgress = (loading || persistStatus === 'in_progress') && !isStaleInProgress;
const isError = persistStatus === 'error' && !loading;
const wasCompleted = persistStatus === 'completed' && !loading;
const runningTarget = activeTarget ?? (isInProgress ? persistTarget ?? null : null);
// tick 每秒更新一次,让"已用时"实时跳
useEffect(() => {
@@ -108,6 +115,7 @@ export function AiDecomposeButton({ plan, version }: Props) {
aiDecomposeStatus: 'in_progress',
aiDecomposeBy: user?.name,
aiDecomposeAt: new Date().toISOString(),
aiDecomposeTarget: target,
aiDecomposeError: undefined,
});
@@ -134,24 +142,29 @@ export function AiDecomposeButton({ plan, version }: Props) {
if (!resp.ok) {
updatePlan(plan.id, {
aiDecomposeStatus: 'error',
aiDecomposeTarget: target,
aiDecomposeError: resp.error,
});
} else {
const targetFilteredResult = filterDecomposeResultByTarget(resp.result, target);
const requirementIdSet = new Set(linkedReqs.map((req) => req.id));
const deduped = filterDuplicateDecomposeDrafts(resp.result, {
const deduped = filterDuplicateDecomposeDrafts(targetFilteredResult, {
existingDevTasks: devTasks.filter((task) => requirementIdSet.has(task.requirementId)),
existingTestCases: testCases.filter((testCase) => testCase.versionId === version.id),
categories,
requirements: linkedReqs,
});
setResult({ ...resp, result: deduped.result });
setDedupeSummary({
removedDevTaskCount: deduped.removedDevTaskCount,
removedTestCaseCount: deduped.removedTestCaseCount,
setModalState({
result: { ...resp, result: filterDecomposeResultByTarget(deduped.result, target) },
target,
dedupeSummary: {
removedDevTaskCount: deduped.removedDevTaskCount,
removedTestCaseCount: deduped.removedTestCaseCount,
},
});
setResultTarget(target);
updatePlan(plan.id, {
aiDecomposeStatus: 'completed',
aiDecomposeTarget: target,
aiDecomposeError: undefined,
});
}
@@ -159,6 +172,7 @@ export function AiDecomposeButton({ plan, version }: Props) {
const msg = e?.message || '调用 AI 服务失败';
updatePlan(plan.id, {
aiDecomposeStatus: 'error',
aiDecomposeTarget: target,
aiDecomposeError: msg,
});
} finally {
@@ -175,10 +189,11 @@ export function AiDecomposeButton({ plan, version }: Props) {
<>
<span className="inline-flex items-center gap-1.5">
{(['dev_tasks', 'test_cases'] as AgentDecomposeTarget[]).map((target) => {
const showSpinner = isInProgress && (!activeTarget || activeTarget === target);
const showSpinner = isInProgress && runningTarget === target;
const label = wasCompleted ? `重新拆解${targetText(target)}` : `AI 拆解${targetText(target)}`;
return (
<button
type="button"
key={target}
onClick={() => handleClick(target)}
disabled={isInProgress}
@@ -231,15 +246,15 @@ export function AiDecomposeButton({ plan, version }: Props) {
</span>
)}
{result && (
{modalState && (
<DecomposeReportModal
result={result}
result={modalState.result}
version={version}
plan={plan}
requirements={linkedReqs}
target={resultTarget}
dedupeSummary={dedupeSummary}
onClose={() => setResult(null)}
target={modalState.target}
dedupeSummary={modalState.dedupeSummary}
onClose={() => setModalState(null)}
/>
)}
</>