Files
massive-cloud/server/routers.ts

545 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { COOKIE_NAME } from "@shared/const";
import { getSessionCookieOptions } from "./_core/cookies";
import { systemRouter } from "./_core/systemRouter";
import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import * as db from "./db";
import { enqueueAssemblyInspectionJob } from "./assemblyInspectionJob.js";
import { enqueueDeviationHeatmapJob } from "./deviationHeatmapJob.js";
import { enqueueSmoothHeatmapJob } from "./smoothHeatmapJob.js";
import { stepParseCacheDir } from "./stepParse.js";
import { listPlugins, getPlugin } from "./plugins/registry.js";
import { enqueueAlgorithmJob, algorithmJobStats } from "./algorithmJob.js";
export const appRouter = router({
system: systemRouter,
auth: router({
me: publicProcedure.query(opts => opts.ctx.user),
logout: publicProcedure.mutation(({ ctx }) => {
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
return { success: true } as const;
}),
}),
// ─── 车系 ──────────────────────────────────────────────────────────────────
vehicleSeries: router({
list: protectedProcedure.query(async () => {
return db.listVehicleSeries();
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
}))
.mutation(async ({ input }) => {
return db.createVehicleSeries({
name: input.name,
description: input.description ?? null,
});
}),
update: protectedProcedure
.input(z.object({
id: z.number(),
name: z.string().min(1).max(100).optional(),
description: z.string().optional(),
}))
.mutation(async ({ input }) => {
const { id, ...data } = input;
await db.updateVehicleSeries(id, data);
return { success: true };
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteVehicleSeries(input.id);
return { success: true };
}),
}),
// ─── 车型 ──────────────────────────────────────────────────────────────────
vehicleModel: router({
listBySeries: protectedProcedure
.input(z.object({ seriesId: z.number() }))
.query(async ({ input }) => {
return db.listVehicleModelsBySeries(input.seriesId);
}),
get: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return db.getVehicleModelById(input.id);
}),
create: protectedProcedure
.input(z.object({
seriesId: z.number(),
name: z.string().min(1).max(100),
description: z.string().optional(),
}))
.mutation(async ({ input }) => {
return db.createVehicleModel({
seriesId: input.seriesId,
name: input.name,
description: input.description ?? null,
});
}),
update: protectedProcedure
.input(z.object({
id: z.number(),
name: z.string().min(1).max(100).optional(),
description: z.string().optional(),
}))
.mutation(async ({ input }) => {
const { id, ...data } = input;
await db.updateVehicleModel(id, data);
return { success: true };
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteVehicleModel(input.id);
return { success: true };
}),
}),
// ─── 车型 CAD 文件 ─────────────────────────────────────────────────────────
cadFile: router({
list: protectedProcedure
.input(z.object({ modelId: z.number() }))
.query(async ({ input }) => {
return db.listModelCadFiles(input.modelId);
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteModelCadFile(input.id);
return { success: true };
}),
/** STEP 解析后的 manifest(零件列表与 STL 相对路径) */
assemblyManifest: protectedProcedure
.input(z.object({ modelId: z.number(), cadFileId: z.number() }))
.query(async ({ input }) => {
const row = await db.getModelCadFileById(input.cadFileId);
if (!row || row.modelId !== input.modelId) {
throw new TRPCError({ code: "NOT_FOUND", message: "CAD 文件不存在" });
}
const manPath = path.join(stepParseCacheDir(input.modelId, input.cadFileId), "manifest.json");
if (!fs.existsSync(manPath)) {
return { ready: false as const, manifest: null as null };
}
const manifest = JSON.parse(fs.readFileSync(manPath, "utf-8")) as Record<string, unknown>;
return { ready: true as const, manifest };
}),
}),
// ─── 任务 CAD 文件 ────────────────────────────────────────────────────────────
taskCadFile: router({
list: protectedProcedure
.input(z.object({ taskId: z.number() }))
.query(async ({ input }) => {
return db.listTaskCadFiles(input.taskId);
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteTaskCadFile(input.id);
return { success: true };
}),
}),
// ─── 检测任务 ──────────────────────────────────────────────────────────────
inspectionTask: router({
listByModel: protectedProcedure
.input(z.object({ modelId: z.number() }))
.query(async ({ input }) => {
return db.listInspectionTasksByModel(input.modelId);
}),
get: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return db.getInspectionTaskById(input.id);
}),
create: protectedProcedure
.input(z.object({
modelId: z.number(),
name: z.string().min(1).max(255),
inspector: z.string().max(100).optional(),
notes: z.string().optional(),
}))
.mutation(async ({ input }) => {
return db.createInspectionTask({
modelId: input.modelId,
name: input.name,
inspector: input.inspector ?? null,
notes: input.notes ?? null,
status: "pending",
});
}),
update: protectedProcedure
.input(z.object({
id: z.number(),
name: z.string().max(255).optional(),
inspector: z.string().max(100).optional(),
notes: z.string().optional(),
status: z.enum(["pending", "processing", "completed", "error"]).optional(),
}))
.mutation(async ({ input }) => {
const { id, ...data } = input;
await db.updateInspectionTask(id, data);
return { success: true };
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteInspectionTask(input.id);
return { success: true };
}),
}),
// ─── 任务点云文件 ────────────────────────────────────────────────────────────
pointcloudFile: router({
list: protectedProcedure
.input(z.object({ taskId: z.number() }))
.query(async ({ input }) => {
return db.listTaskPointcloudFiles(input.taskId);
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteTaskPointcloudFile(input.id);
return { success: true };
}),
}),
// ─── 任务分析结果 ────────────────────────────────────────────────────────────
analysisResult: router({
list: protectedProcedure
.input(z.object({ taskId: z.number() }))
.query(async ({ input }) => {
return db.listTaskAnalysisResults(input.taskId);
}),
runAnalysis: protectedProcedure
.input(z.object({
taskId: z.number(),
type: z.enum([
"error_analysis",
"measurement",
"compression",
"smooth_heatmap",
"assembly_inspection",
"obj_deviation",
]),
/** 点云采样预算:平滑热力默认 40 万,偏差热力(GPU)可提高到千万级 */
totalPoints: z.number().min(50_000).max(20_000_000).optional(),
/** 装配检测:指定 STEP 的 model_cad_files.id;不填则取该车型下第一个 STEP */
cadFileId: z.number().optional(),
/** 装配检测:点云采样点数,默认 10 万 */
samplePoints: z.number().min(10_000).max(500_000).optional(),
/** 装配检测:合格阈值(mm) */
toleranceMm: z.number().positive().max(100).optional(),
/** 装配检测:是否生成每零件顶点热力缓存 */
includeVertexHeatmap: z.boolean().optional(),
}))
.mutation(async ({ input }) => {
if (input.type === "assembly_inspection") {
try {
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "assembly_inspection",
status: "processing",
});
enqueueAssemblyInspectionJob(record.id, input.taskId, {
cadFileId: input.cadFileId,
samplePoints: input.samplePoints ?? 100_000,
toleranceMm: input.toleranceMm ?? 0.3,
includeVertexHeatmap: input.includeVertexHeatmap ?? false,
});
return { success: true as const, id: record.id, kind: "assembly_inspection" as const };
} catch (e: unknown) {
const err = e as { errno?: number; message?: string };
const msg = err.message ?? "";
if (
err.errno === 1265 ||
err.errno === 1366 ||
msg.includes("Data truncated") ||
(msg.includes("Incorrect") && msg.includes("column 'type'"))
) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"数据库缺少 assembly_inspection 类型枚举,请应用迁移 drizzle/0005_task_analysis_assembly_inspection.sql 后重启。",
});
}
throw e;
}
}
if (input.type === "smooth_heatmap") {
try {
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "smooth_heatmap",
status: "processing",
});
enqueueSmoothHeatmapJob(record.id, input.taskId, input.totalPoints ?? 400_000);
return { success: true as const, id: record.id, kind: "smooth_heatmap" as const };
} catch (e: unknown) {
const err = e as { errno?: number; message?: string };
const msg = err.message ?? "";
if (
err.errno === 1265 ||
err.errno === 1366 ||
msg.includes("Data truncated") ||
msg.includes("Incorrect") && msg.includes("column 'type'")
) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"数据库 task_analysis_results.type 枚举缺少 smooth_heatmap。请重启服务以应用迁移 drizzle/0004_task_analysis_smooth_heatmap_enum.sql,或在 MySQL 执行 scripts/alter-task-analysis-type-enum.sql。",
});
}
throw e;
}
}
if (input.type === "obj_deviation") {
try {
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "obj_deviation",
status: "processing",
});
enqueueDeviationHeatmapJob(record.id, input.taskId, input.totalPoints ?? 5_000_000);
return { success: true as const, id: record.id, kind: "obj_deviation" as const };
} catch (e: unknown) {
const err = e as { errno?: number; message?: string };
const msg = err.message ?? "";
if (
err.errno === 1265 ||
err.errno === 1366 ||
msg.includes("Data truncated") ||
(msg.includes("Incorrect") && msg.includes("column 'type'"))
) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"数据库 task_analysis_results.type 枚举缺少 obj_deviation。请应用迁移 drizzle/0006_task_analysis_obj_deviation.sql 后重启。",
});
}
throw e;
}
}
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: input.type,
status: "processing",
});
await new Promise(resolve => setTimeout(resolve, 1500));
const errorStats = {
min: 0.001,
max: 2.543,
mean: 0.856,
stdDev: 0.423,
totalPoints: 1000000,
withinTolerance: 892345,
outOfTolerance: 107655,
};
const measurements = [
{ name: "车体长度", value: 25700.4, unit: "mm", tolerance: 5, status: "pass" },
{ name: "车体宽度", value: 3380.1, unit: "mm", tolerance: 3, status: "pass" },
{ name: "车体高度", value: 3700.2, unit: "mm", tolerance: 3, status: "pass" },
{ name: "轮对距D1", value: 1435.8, unit: "mm", tolerance: 1, status: "warning" },
{ name: "转向架轴距", value: 2500.3, unit: "mm", tolerance: 2, status: "pass" },
];
await db.updateTaskAnalysisResult(record.id, {
status: "completed",
errorStats,
measurements,
});
await db.updateInspectionTask(input.taskId, {
status: "completed",
completedAt: new Date(),
});
return { success: true, id: record.id, errorStats, measurements };
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteTaskAnalysisResult(input.id);
return { success: true };
}),
}),
// ─── 通用算法插件(Route B) ──────────────────────────────────────────────
//
// 不为每类算法做独立界面:前端按 manifest.inputs 动态渲染表单 + 多选并发执行;
// 后端按 manifest.runtime 启动子进程或返回 mock fixture;
// 结果统一落到 task_analysis_results.type = 'plugin',pluginId 区分。
algorithm: router({
/** 列出所有已注册插件(前端 AlgorithmRunPanel 用) */
list: protectedProcedure.query(() => {
return {
plugins: listPlugins().map((m) => ({
id: m.id,
name: m.name,
shortName: m.shortName,
description: m.description,
version: m.version,
schemaVersion: m.schemaVersion,
category: m.category,
inputs: m.inputs ?? [],
requiresCad: m.requiresCad ?? false,
requiresPointCloud: m.requiresPointCloud ?? true,
runtimeKind: m.runtime.kind,
})),
scheduler: algorithmJobStats(),
};
}),
/** 单插件执行:创建一条 'plugin' 类型的分析结果记录,丢进并发池 */
run: protectedProcedure
.input(
z.object({
taskId: z.number(),
pluginId: z.string().min(1),
params: z.record(z.string(), z.unknown()).optional(),
/** 可选:只处理选中的点云文件 ID(空/缺省 = 任务下全部文件) */
pointCloudFileIds: z.array(z.number()).optional(),
/** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */
cadFileIds: z.array(z.number()).optional(),
}),
)
.mutation(async ({ input }) => {
const manifest = getPlugin(input.pluginId);
if (!manifest) {
throw new TRPCError({
code: "NOT_FOUND",
message: `未注册的算法插件:${input.pluginId}`,
});
}
try {
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "plugin",
pluginId: manifest.id,
schemaVersion: manifest.schemaVersion,
status: "processing",
});
void enqueueAlgorithmJob(
record.id,
input.taskId,
manifest.id,
input.params ?? {},
input.pointCloudFileIds,
input.cadFileIds,
);
return {
success: true as const,
id: record.id,
pluginId: manifest.id,
schemaVersion: manifest.schemaVersion,
};
} catch (e: unknown) {
const err = e as { errno?: number; message?: string };
const msg = err.message ?? "";
if (
err.errno === 1265 ||
err.errno === 1366 ||
msg.includes("Data truncated") ||
(msg.includes("Incorrect") && msg.includes("column 'type'")) ||
msg.includes("Unknown column 'pluginId'")
) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"数据库缺少 type='plugin' 枚举或 pluginId/schemaVersion 列。请应用迁移 drizzle/0007_task_analysis_plugin_columns.sql 后重启。",
});
}
throw e;
}
}),
/** 多插件并发执行:每个插件都通过相同的 run 流程入池 */
runBatch: protectedProcedure
.input(
z.object({
taskId: z.number(),
/** 可选:只处理选中的点云文件 ID(空/缺省 = 任务下全部文件) */
pointCloudFileIds: z.array(z.number()).optional(),
/** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */
cadFileIds: z.array(z.number()).optional(),
jobs: z
.array(
z.object({
pluginId: z.string().min(1),
params: z.record(z.string(), z.unknown()).optional(),
}),
)
.min(1)
.max(16),
}),
)
.mutation(async ({ input }) => {
const created: { id: number; pluginId: string }[] = [];
for (const job of input.jobs) {
const manifest = getPlugin(job.pluginId);
if (!manifest) continue;
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "plugin",
pluginId: manifest.id,
schemaVersion: manifest.schemaVersion,
status: "processing",
});
void enqueueAlgorithmJob(
record.id,
input.taskId,
manifest.id,
job.params ?? {},
input.pointCloudFileIds,
input.cadFileIds,
);
created.push({ id: record.id, pluginId: manifest.id });
}
return { success: true as const, jobs: created, scheduler: algorithmJobStats() };
}),
}),
});
export type AppRouter = typeof appRouter;