feat: deduplicate capability palette
This commit is contained in:
1 parent
d22468e29f
commit
0132871885
8 files changed
+6603
-11
No files matched your search
@@ -0,0 +1,19 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("duplicate capability policy", () => {
|
||||
it("keeps one canonical capability and marks compatible aliases", async () => {
|
||||
const audit = JSON.parse(await readFile("resources/capability-duplicates.json", "utf8"));
|
||||
const decisions = audit.decisions as Array<{ id: string; status: string; canonical?: string }>;
|
||||
const kept = decisions.filter((item) => item.status === "keep");
|
||||
const hidden = decisions.filter((item) => item.status === "hide");
|
||||
expect(kept.length).toBe(658);
|
||||
expect(hidden.length).toBe(236);
|
||||
expect(audit.editableBefore).toBe(kept.length + hidden.length);
|
||||
expect(audit.editableAfter).toBe(kept.length);
|
||||
const keptIds = new Set(kept.map((item) => item.id));
|
||||
expect(keptIds.size).toBe(kept.length);
|
||||
for (const item of hidden) { if (!item.canonical) throw new Error("hidden capability missing canonical"); expect(keptIds.has(item.canonical)).toBe(true); }
|
||||
expect(hidden.every((item) => item.canonical && !keptIds.has(item.id))).toBe(true);
|
||||
});
|
||||
});
|
||||
+39
-7
@@ -5,6 +5,7 @@ import type { Catalog, CatalogComponent } from "../shared/types";
|
||||
|
||||
let cached: Catalog | null = null;
|
||||
let excludedIds: Set<string> | null = null;
|
||||
let duplicateMap: Map<string, string> | null = null;
|
||||
|
||||
function resolveCatalogPath(): string {
|
||||
const candidates = [
|
||||
@@ -19,6 +20,31 @@ function resolveCatalogPath(): string {
|
||||
throw new Error(`catalog.json not found; tried: ${candidates.join(", ")}`);
|
||||
}
|
||||
|
||||
function duplicatesPath(): string {
|
||||
const candidates = [
|
||||
process.env.RPA_DUPLICATE_PATH,
|
||||
path.join(process.resourcesPath, "capability-duplicates.json"),
|
||||
path.join(app.getAppPath(), "resources", "capability-duplicates.json"),
|
||||
path.join(__dirname, "../../resources/capability-duplicates.json")
|
||||
].filter(Boolean) as string[];
|
||||
return candidates.find((candidate) => fs.existsSync(candidate)) || "";
|
||||
}
|
||||
|
||||
function loadDuplicateMap(): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
const file = duplicatesPath();
|
||||
if (!file) return map;
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as { decisions?: Array<{ id?: string; status?: string; canonical?: string }> };
|
||||
for (const decision of parsed.decisions || []) {
|
||||
if (decision.id && decision.status === "hide" && decision.canonical) map.set(decision.id, decision.canonical);
|
||||
}
|
||||
} catch {
|
||||
// 审计文件损坏时保留完整目录
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function excludedPath(): string {
|
||||
const candidates = [
|
||||
process.env.RPA_EXCLUDED_PATH,
|
||||
@@ -53,15 +79,21 @@ export function loadCatalog(): Catalog {
|
||||
const raw = fs.readFileSync(catalogPath(), "utf8");
|
||||
const catalog = JSON.parse(raw) as Catalog;
|
||||
const excluded = loadExcludedIds();
|
||||
const duplicates = duplicateMap || loadDuplicateMap();
|
||||
duplicateMap = duplicates;
|
||||
catalog.components = catalog.components
|
||||
.filter((component) => !excluded.has(component.id))
|
||||
.map((component: CatalogComponent) => component);
|
||||
catalog.counts.total = catalog.components.length;
|
||||
catalog.counts.vendor = catalog.components.filter((component) => component.kind === "vendor").length;
|
||||
catalog.counts.native = catalog.components.filter((component) => component.kind === "native").length;
|
||||
catalog.counts.triggers = catalog.components.filter((component) => component.kind === "trigger").length;
|
||||
catalog.counts.fields = catalog.components.reduce((sum, component) => sum + component.fields.length, 0);
|
||||
for (const group of catalog.groups) group.count = catalog.components.filter((component) => component.group === group.id).length;
|
||||
.map((component: CatalogComponent) => ({
|
||||
...component,
|
||||
duplicateOf: duplicates.get(component.id)
|
||||
}));
|
||||
const visibleComponents = catalog.components.filter((component) => !component.duplicateOf);
|
||||
catalog.counts.total = visibleComponents.length;
|
||||
catalog.counts.vendor = visibleComponents.filter((component) => component.kind === "vendor").length;
|
||||
catalog.counts.native = visibleComponents.filter((component) => component.kind === "native").length;
|
||||
catalog.counts.triggers = visibleComponents.filter((component) => component.kind === "trigger").length;
|
||||
catalog.counts.fields = visibleComponents.reduce((sum, component) => sum + component.fields.length, 0);
|
||||
for (const group of catalog.groups) group.count = visibleComponents.filter((component) => component.group === group.id).length;
|
||||
cached = catalog;
|
||||
return catalog;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export function Palette({ catalog, onAdd }: PaletteProps) {
|
||||
const filtered = useMemo(() => {
|
||||
const keyword = query.trim().toLowerCase();
|
||||
return catalog.components.filter((component) => {
|
||||
if (component.duplicateOf) return false;
|
||||
if (!keyword) return true;
|
||||
const haystack = [
|
||||
component.name,
|
||||
|
||||
@@ -59,6 +59,8 @@ export interface CatalogComponent {
|
||||
output: CatalogOutput;
|
||||
slots: string[];
|
||||
nodeKind?: string;
|
||||
/** 同名旧版本 / 重复别名指向的保留能力 ID。 */
|
||||
duplicateOf?: string;
|
||||
}
|
||||
|
||||
export interface CatalogGroup {
|
||||
|
||||
Reference in new issue
Block a user