Files
cadSet/cad-agent-studio/src/lib/cad-generator.ts
T
2026-07-27 17:24:04 +08:00

217 lines
6.5 KiB
TypeScript

import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { artifactUrl, createStepGlbPreview, readArtifact } from "@/lib/preview-artifacts";
import { enginePath } from "@/lib/paths";
import { ensureTask, sanitizeFilename, upsertManifest } from "@/lib/task-store";
const execFileAsync = promisify(execFile);
export type CadGenerationResult = {
handled: true;
taskId: string;
artifactPath: string;
artifactUrl: string;
previewPath: string;
previewUrl: string;
viewerAssetPath: string;
viewerAssetUrl: string;
sourcePath: string;
summary: string;
};
function ensureRelativeFilename(filename: string, extension: string, fallback: string) {
const safeName = sanitizeFilename(filename || fallback);
return safeName.toLowerCase().endsWith(extension) ? safeName : `${safeName}${extension}`;
}
function pythonExecutable() {
return process.env.CAD_PYTHON
|| enginePath("text-to-cad", ".venv", "bin", "python");
}
function pipelineScript() {
return enginePath("designir-pipeline", "scripts", "designir_pipeline.py");
}
function routerScript() {
return enginePath("text-to-cad", "skills", "cad-router", "scripts", "route.py");
}
async function runJson(command: string, args: string[], cwd: string) {
const { stdout } = await execFileAsync(command, args, {
cwd,
timeout: 120_000,
maxBuffer: 1024 * 1024 * 8,
});
return JSON.parse(stdout) as Record<string, unknown>;
}
export async function executeDesignIR({
prompt,
summary,
designir,
designirFilename,
stepFilename,
selectedTaskId,
assumptions = [],
teacherTaskId,
teacherPath,
}: {
prompt: string;
summary: string;
designir: Record<string, unknown>;
designirFilename?: string;
stepFilename?: string;
selectedTaskId?: string;
assumptions?: string[];
teacherTaskId?: string;
teacherPath?: string;
}): Promise<CadGenerationResult> {
const task = await ensureTask(selectedTaskId || undefined);
const sourcePath = ensureRelativeFilename(
designirFilename || "model.designir.json",
".designir.json",
"model.designir.json",
);
const artifactPath = ensureRelativeFilename(stepFilename || "model.step", ".step", "model.step");
const sourceAbsolutePath = path.join(task.dir, sourcePath);
const artifactAbsolutePath = path.join(task.dir, artifactPath);
await fs.writeFile(sourceAbsolutePath, `${JSON.stringify(designir, null, 2)}\n`);
const route = await runJson(
pythonExecutable(),
[routerScript(), prompt],
task.dir,
);
const selectedBackend = String(route.selected_backend || "build123d");
const normalizedDesignIR = {
...designir,
backend_hint: selectedBackend,
};
await fs.writeFile(sourceAbsolutePath, `${JSON.stringify(normalizedDesignIR, null, 2)}\n`);
const antiCheat = await runJson(
pythonExecutable(),
[pipelineScript(), "anti-cheat", sourceAbsolutePath],
task.dir,
);
if (antiCheat.valid !== true) {
throw new Error(`DesignIR anti-cheat validation failed: ${JSON.stringify(antiCheat.violations || [])}`);
}
const rebuild = await runJson(
pythonExecutable(),
[
pipelineScript(),
"isolated-rebuild",
sourceAbsolutePath,
"--output",
artifactAbsolutePath,
"--backend",
selectedBackend,
],
task.dir,
);
if (rebuild.valid !== true) {
throw new Error("Isolated DesignIR rebuild failed.");
}
const validation: string[] = [
"CAD Router selected the backend before compilation.",
"DesignIR anti-cheat validation passed.",
"STEP rebuilt in an isolated workspace without teacher STEP access.",
];
const artifacts: Array<{ path: string; role: string; kind?: string }> = [
{ path: sourcePath, role: "editable_contract", kind: "designir-2.0" },
{ path: artifactPath, role: "primary", kind: "step" },
];
if (typeof rebuild.model_json === "string" && rebuild.model_json) {
const simpleCadModelPath = path.basename(rebuild.model_json);
artifacts.push({
path: simpleCadModelPath,
role: "backend_editable_graph",
kind: "simplecad-model-json",
});
}
if (teacherTaskId && teacherPath) {
const { absolutePath: teacherAbsolutePath } = await readArtifact(
teacherTaskId,
teacherPath,
);
const reportPath = "acceptance-report.json";
const reportAbsolutePath = path.join(task.dir, reportPath);
const acceptance = await runJson(
pythonExecutable(),
[
pipelineScript(),
"accept",
"--teacher",
teacherAbsolutePath,
"--designir",
sourceAbsolutePath,
"--rebuilt",
artifactAbsolutePath,
"--report",
reportAbsolutePath,
],
task.dir,
);
artifacts.push({ path: reportPath, role: "acceptance", kind: "json" });
validation.push(`Independent acceptance verdict: ${String(acceptance.verdict || "unknown")}.`);
}
const viewerAssetPath = await createStepGlbPreview({
taskId: task.taskId,
stepPath: artifactPath,
});
artifacts.push({ path: viewerAssetPath, role: "viewer", kind: "glb" });
const artifactVersion = Date.now().toString(36);
await upsertManifest(task.taskId, {
request: prompt,
route: {
selected_backend: selectedBackend,
runner_skill: String(route.runner_skill || "cad"),
project: String(route.project || "text-to-cad"),
fallback_order: Array.isArray(route.fallback_order) ? route.fallback_order : [],
workflow_profiles: Array.isArray(route.workflow_profiles) ? route.workflow_profiles : [],
},
source: {
path: sourcePath,
format: "designir-2.0",
backend: selectedBackend,
source_editability: "editable_source",
generated_by: "llm_designir_tool_call",
},
artifacts,
assumptions,
validation,
studio: {
latestArtifact: artifactPath,
latestPreview: artifactPath,
latestViewerAsset: viewerAssetPath,
latestVersion: artifactVersion,
},
});
const stepUrl = artifactUrl(task.taskId, artifactPath, artifactVersion);
return {
handled: true,
taskId: task.taskId,
artifactPath,
artifactUrl: stepUrl,
previewPath: artifactPath,
previewUrl: stepUrl,
viewerAssetPath,
viewerAssetUrl: artifactUrl(task.taskId, viewerAssetPath, artifactVersion),
sourcePath,
summary: summary || "CAD model generated from executable DesignIR 2.0.",
};
}
export function cadResultDirective(result: CadGenerationResult) {
return `::cad-result{base64="${Buffer.from(JSON.stringify(result), "utf8").toString("base64")}"}`;
}