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

@@ -0,0 +1,125 @@
import type {
AgentDecomposeResult,
AgentDevTaskDraft,
AgentReference,
AgentTestCaseDraft,
} from '@ftb/shared';
import type { DevTask, Reference } from './dev-task';
import type { TestCase } from './test-case';
import type { TaskCategory } from './task-category';
interface RequirementIdentity {
id: string;
code: string;
}
export interface DecomposeDedupeInput {
existingDevTasks: DevTask[];
existingTestCases: TestCase[];
categories: TaskCategory[];
requirements: RequirementIdentity[];
}
export interface DecomposeDedupeResult {
result: AgentDecomposeResult;
removedDevTaskCount: number;
removedTestCaseCount: number;
}
export function filterDuplicateDecomposeDrafts(
result: AgentDecomposeResult,
input: DecomposeDedupeInput,
): DecomposeDedupeResult {
const devFingerprints = new Set(
input.existingDevTasks
.map((task) => existingItemFingerprint(task.title, task.categoryId, task.references ?? [], input))
.filter(Boolean),
);
const testCaseFingerprints = new Set(
input.existingTestCases
.map((testCase) => existingItemFingerprint(testCase.title, testCase.categoryId, testCase.references ?? [], input))
.filter(Boolean),
);
const filteredDevTaskDrafts = filterDrafts(result.devTaskDrafts, devFingerprints, input, draftFingerprint);
const filteredTestCaseDrafts = filterDrafts(result.testCaseDrafts, testCaseFingerprints, input, draftFingerprint);
return {
result: {
...result,
devTaskDrafts: filteredDevTaskDrafts.items,
testCaseDrafts: filteredTestCaseDrafts.items,
},
removedDevTaskCount: result.devTaskDrafts.length - filteredDevTaskDrafts.items.length,
removedTestCaseCount: result.testCaseDrafts.length - filteredTestCaseDrafts.items.length,
};
}
function filterDrafts<T extends AgentDevTaskDraft | AgentTestCaseDraft>(
drafts: T[],
existingFingerprints: Set<string>,
input: DecomposeDedupeInput,
getFingerprint: (draft: T, input: DecomposeDedupeInput) => string,
): { items: T[] } {
const seen = new Set<string>();
const items: T[] = [];
for (const draft of drafts) {
const fingerprint = getFingerprint(draft, input);
if (existingFingerprints.has(fingerprint) || seen.has(fingerprint)) continue;
seen.add(fingerprint);
items.push(draft);
}
return { items };
}
function draftFingerprint(
draft: AgentDevTaskDraft | AgentTestCaseDraft,
input: DecomposeDedupeInput,
): string {
return buildFingerprint(draft.title, draft.categoryCode, draft.references, input);
}
function existingItemFingerprint(
title: string,
categoryId: string,
references: Reference[],
input: DecomposeDedupeInput,
): string {
const categoryCode = input.categories.find((category) => category.id === categoryId)?.code ?? categoryId;
return buildFingerprint(title, categoryCode, references, input);
}
function buildFingerprint(
title: string,
categoryCode: string,
references: Array<AgentReference | Reference>,
input: DecomposeDedupeInput,
): string {
const refKey = references
.map((ref) => normalizeReferenceKey(ref, input.requirements))
.filter(Boolean)
.sort()
.join('|');
return `${categoryCode}::${normalizeTitle(title)}::${refKey}`;
}
function normalizeTitle(title: string): string {
return title
.trim()
.toLowerCase()
.replace(/[\s:,.。;;、\-—_]/g, '');
}
function normalizeReferenceKey(
ref: AgentReference | Reference,
requirements: RequirementIdentity[],
): string {
const rawId = String(ref.id || '').trim();
if (!rawId) return '';
if (ref.type === 'requirement') {
const matched = requirements.find((requirement) => requirement.id === rawId || requirement.code === rawId);
return `requirement:${matched?.id ?? rawId}`;
}
return `${ref.type}:${rawId.toLowerCase()}`;
}