feat: add selectable demo flow tasks
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user