feat(ai): 优化拆解重跑与结果展示
This commit is contained in:
125
apps/web/lib/ai-decompose-dedupe.ts
Normal file
125
apps/web/lib/ai-decompose-dedupe.ts
Normal 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()}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user