feat(ai): 优化拆解目标和负责人推荐
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user