feat: add selectable demo flow tasks

This commit is contained in:
2026-09-02 15:23:15 +08:00
parent f879328562
commit e3dc7ebe21
19 changed files with 871 additions and 110 deletions

View File

@@ -13,6 +13,7 @@ import { enqueueDeviationHeatmapJob } from "./deviationHeatmapJob.js";
import { enqueueSmoothHeatmapJob } from "./smoothHeatmapJob.js";
import { stepParseCacheDir } from "./stepParse.js";
import { listPlugins, getPlugin } from "./plugins/registry.js";
import { buildDemoTaskCatalog, resolveSelectedDemoJobs } from "./demoFlow.js";
import { enqueueAlgorithmJob, algorithmJobStats, cancelAlgorithmJob, cancelAlgorithmJobsByTask } from "./algorithmJob.js";
import { cancelLegacyAnalysisJob, cancelLegacyAnalysisJobsByTask } from "./analysisJobControl.js";
import { deleteAnalysisResult } from "./analysisResultDeletion.js";
@@ -313,13 +314,26 @@ async function cancelTaskAnalysisResultsByTask(taskId: number, reason = "用户
};
}
interface DemoFlowMeasurementJob {
pluginId: string;
params?: Record<string, unknown>;
taskNos?: number[];
}
interface DemoFlowMetadata {
demoFlowMode: "all" | "selected";
demoFlowSelectedTaskNos?: number[];
demoFlowSelectedTaskCount?: number;
}
async function enqueueDemoRegisteredMeasurementsAfterRegistration(input: {
taskId: number;
sourcePointCloudFileId: number;
cadFileId: number;
registrationResultId: number;
measurementPluginIds: string[];
measurementJobs: DemoFlowMeasurementJob[];
demoFlowRunId: string;
metadata: DemoFlowMetadata;
}) {
const registrationResult = await db.getTaskAnalysisResultById(input.registrationResultId);
if (registrationResult?.status !== "completed") {
@@ -335,13 +349,14 @@ async function enqueueDemoRegisteredMeasurementsAfterRegistration(input: {
return;
}
for (let idx = 0; idx < input.measurementPluginIds.length; idx += 1) {
const pluginId = input.measurementPluginIds[idx];
for (let idx = 0; idx < input.measurementJobs.length; idx += 1) {
const job = input.measurementJobs[idx];
const pluginId = job.pluginId;
const manifest = getPlugin(pluginId);
await createAndEnqueuePluginJob({
taskId: input.taskId,
pluginId,
params: paramsForDemoPlugin(pluginId),
params: { ...paramsForDemoPlugin(pluginId), ...(job.params ?? {}) },
pointCloudFileIds: DEMO_REGISTERED_POINTCLOUD_PLUGIN_IDS.has(pluginId)
? [registered.id]
: [input.sourcePointCloudFileId],
@@ -350,14 +365,86 @@ async function enqueueDemoRegisteredMeasurementsAfterRegistration(input: {
demoFlowRunId: input.demoFlowRunId,
demoFlowRole: "measurement",
demoFlowIndex: idx + 2,
demoFlowTotal: input.measurementPluginIds.length + 1,
demoFlowTotal: input.measurementJobs.length + 1,
demoFlowSourcePointCloudFileId: input.sourcePointCloudFileId,
demoFlowCadFileId: input.cadFileId,
...input.metadata,
...(job.taskNos ? { demoFlowPluginTaskNos: job.taskNos } : {}),
},
});
}
}
async function startDemoFlow(input: {
taskId: number;
sourcePointCloudFileId: number;
cadFileId: number;
measurementJobs: DemoFlowMeasurementJob[];
metadata: DemoFlowMetadata;
}) {
const demoFlowRunId = randomUUID();
clearDemoFlowCancelled(input.taskId);
for (const job of input.measurementJobs) {
const manifest = getPlugin(job.pluginId);
if (!manifest) throw new TRPCError({ code: "NOT_FOUND", message: `未注册的算法插件:${job.pluginId}` });
if (manifest.category === "配准") {
throw new TRPCError({ code: "BAD_REQUEST", message: "全流程的测量算法列表不能包含配准算法。" });
}
}
const pointcloudFiles = await db.listTaskPointcloudFiles(input.taskId);
const sourcePointcloud = pointcloudFiles.find((row) => row.id === input.sourcePointCloudFileId);
if (!sourcePointcloud) throw new TRPCError({ code: "NOT_FOUND", message: "选中的原始点云文件不存在。" });
if (sourcePointcloud.source === "registered") {
throw new TRPCError({ code: "BAD_REQUEST", message: "请选择原始点云,系统会自动找到对应配准产物。" });
}
const cadFiles = await db.listTaskCadFiles(input.taskId);
const cad = cadFiles.find((row) => row.id === input.cadFileId);
if (!cad) throw new TRPCError({ code: "NOT_FOUND", message: "选中的 CAD 文件不存在。" });
await validateTaskInputCompatibility(input.taskId, sourcePointcloud, cad);
const registration = await createAndEnqueuePluginJob({
taskId: input.taskId,
pluginId: "vehicle-registration",
params: {},
pointCloudFileIds: [input.sourcePointCloudFileId],
cadFileIds: [input.cadFileId],
resultData: {
demoFlowRunId,
demoFlowRole: "registration",
demoFlowIndex: 1,
demoFlowTotal: input.measurementJobs.length + 1,
demoFlowSourcePointCloudFileId: input.sourcePointCloudFileId,
demoFlowCadFileId: input.cadFileId,
...input.metadata,
},
});
const registrationJob = { id: registration.id, pluginId: registration.pluginId };
void registration.promise.then(() => {
if (isDemoFlowCancelled(input.taskId)) {
console.log(`[demoFlow] 任务 ${input.taskId} 已停止全流程,跳过后续测量提交。`);
return undefined;
}
return enqueueDemoRegisteredMeasurementsAfterRegistration({
taskId: input.taskId,
sourcePointCloudFileId: input.sourcePointCloudFileId,
cadFileId: input.cadFileId,
registrationResultId: registration.id,
measurementJobs: input.measurementJobs,
demoFlowRunId,
metadata: input.metadata,
});
}).catch((err) => console.error("[demoFlow] 配准链式提交失败:", err));
return {
success: true as const,
jobs: [] as { id: number; pluginId: string }[],
registrationJob,
queuedMeasurementCount: input.measurementJobs.length,
scheduler: algorithmJobStats(),
};
}
export const appRouter = router({
system: systemRouter,
@@ -1325,6 +1412,19 @@ export const appRouter = router({
return { success: true as const, jobs: created, scheduler: algorithmJobStats() };
}),
/** 当前检测对象可用的正式任务目录。 */
demoTaskCatalog: protectedProcedure
.input(z.object({ taskId: z.number() }))
.query(async ({ input }) => {
const task = await db.getInspectionTaskById(input.taskId);
if (!task) throw new TRPCError({ code: "NOT_FOUND", message: "检测任务不存在。" });
const inspectionObject = task.inspectionObjectId
? await db.getInspectionObjectById(task.inspectionObjectId)
: undefined;
const context = inspectionObjectContextFromName(inspectionObject?.name);
return buildDemoTaskCatalog(listPlugins(), context.inspectionObject, context.inspectionObjectName);
}),
/** 一键全流程演示:先整车配准,成功后再一起提交测量算法。 */
runDemoAll: protectedProcedure
.input(
@@ -1337,85 +1437,50 @@ export const appRouter = router({
)
.mutation(async ({ input }) => {
const measurementPluginIds = normalizeDemoMeasurementPluginIds(input.measurementPluginIds);
const demoFlowRunId = randomUUID();
clearDemoFlowCancelled(input.taskId);
for (const pluginId of measurementPluginIds) {
const manifest = getPlugin(pluginId);
if (!manifest) {
throw new TRPCError({
code: "NOT_FOUND",
message: `未注册的算法插件:${pluginId}`,
});
}
if (manifest.category === "配准") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "一键全流程演示的测量算法列表不能包含配准算法。",
});
}
}
const pointcloudFiles = await db.listTaskPointcloudFiles(input.taskId);
const sourcePointcloud = pointcloudFiles.find((row) => row.id === input.sourcePointCloudFileId);
if (!sourcePointcloud) {
throw new TRPCError({ code: "NOT_FOUND", message: "选中的原始点云文件不存在。" });
}
if (sourcePointcloud.source === "registered") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "请选择原始点云,系统会自动找到对应配准产物。",
});
}
const cadFiles = await db.listTaskCadFiles(input.taskId);
const cad = cadFiles.find((row) => row.id === input.cadFileId);
if (!cad) {
throw new TRPCError({ code: "NOT_FOUND", message: "选中的 CAD 文件不存在。" });
}
await validateTaskInputCompatibility(input.taskId, sourcePointcloud, cad);
const registration = await createAndEnqueuePluginJob({
return startDemoFlow({
taskId: input.taskId,
pluginId: "vehicle-registration",
params: {},
pointCloudFileIds: [input.sourcePointCloudFileId],
cadFileIds: [input.cadFileId],
resultData: {
demoFlowRunId,
demoFlowRole: "registration",
demoFlowIndex: 1,
demoFlowTotal: measurementPluginIds.length + 1,
demoFlowSourcePointCloudFileId: input.sourcePointCloudFileId,
demoFlowCadFileId: input.cadFileId,
sourcePointCloudFileId: input.sourcePointCloudFileId,
cadFileId: input.cadFileId,
measurementJobs: measurementPluginIds.map((pluginId) => ({ pluginId })),
metadata: { demoFlowMode: "all" },
});
}),
/** 自选任务全流程:服务端校验任务并只提交覆盖所选任务的最小插件集合。 */
runDemoSelected: protectedProcedure
.input(z.object({
taskId: z.number(),
sourcePointCloudFileId: z.number(),
cadFileId: z.number(),
taskNos: z.array(z.number().int().positive()).min(1).max(64),
}))
.mutation(async ({ input }) => {
const task = await db.getInspectionTaskById(input.taskId);
if (!task) throw new TRPCError({ code: "NOT_FOUND", message: "检测任务不存在。" });
const inspectionObject = task.inspectionObjectId
? await db.getInspectionObjectById(task.inspectionObjectId)
: undefined;
const context = inspectionObjectContextFromName(inspectionObject?.name);
const manifests = listPlugins();
const catalog = buildDemoTaskCatalog(manifests, context.inspectionObject, context.inspectionObjectName);
let selectedPlan: ReturnType<typeof resolveSelectedDemoJobs>;
try {
selectedPlan = resolveSelectedDemoJobs(manifests, catalog, input.taskNos);
} catch (error) {
throw new TRPCError({ code: "BAD_REQUEST", message: error instanceof Error ? error.message : "所选任务无效" });
}
const result = await startDemoFlow({
taskId: input.taskId,
sourcePointCloudFileId: input.sourcePointCloudFileId,
cadFileId: input.cadFileId,
measurementJobs: selectedPlan.jobs,
metadata: {
demoFlowMode: "selected",
demoFlowSelectedTaskNos: selectedPlan.taskNos,
demoFlowSelectedTaskCount: selectedPlan.taskNos.length,
},
});
const registrationJob = { id: registration.id, pluginId: registration.pluginId };
void registration.promise
.then(() => {
if (isDemoFlowCancelled(input.taskId)) {
console.log(`[runDemoAll] 任务 ${input.taskId} 已停止全流程,跳过后续测量提交。`);
return undefined;
}
return enqueueDemoRegisteredMeasurementsAfterRegistration({
taskId: input.taskId,
sourcePointCloudFileId: input.sourcePointCloudFileId,
cadFileId: input.cadFileId,
registrationResultId: registration.id,
measurementPluginIds,
demoFlowRunId,
});
})
.catch((err) => {
console.error("[runDemoAll] 配准链式提交失败:", err);
});
return {
success: true as const,
jobs: [] as { id: number; pluginId: string }[],
registrationJob,
queuedMeasurementCount: measurementPluginIds.length,
scheduler: algorithmJobStats(),
};
return { ...result, selectedTaskNos: selectedPlan.taskNos };
}),
cancelDemoAll: protectedProcedure