feat: unify CAD workflows on DesignIR 3.0

This commit is contained in:
Jerry
2026-07-28 14:41:26 +08:00
parent 6e33bc0e11
commit b5738e9109
36 changed files with 9690 additions and 1305 deletions
+419 -9
View File
@@ -4,7 +4,13 @@ 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";
import {
ensureTask,
readManifest,
sanitizeFilename,
taskDir,
upsertManifest,
} from "@/lib/task-store";
const execFileAsync = promisify(execFile);
@@ -21,6 +27,13 @@ export type CadGenerationResult = {
summary: string;
};
type UploadedStepReconstructionInput = {
prompt: string;
teacherTaskId: string;
teacherPath: string;
teacherName?: string;
};
function ensureRelativeFilename(filename: string, extension: string, fallback: string) {
const safeName = sanitizeFilename(filename || fallback);
return safeName.toLowerCase().endsWith(extension) ? safeName : `${safeName}${extension}`;
@@ -39,13 +52,33 @@ function routerScript() {
return enginePath("text-to-cad", "skills", "cad-router", "scripts", "route.py");
}
async function runJson(command: string, args: string[], cwd: string) {
function surfaceirScript() {
return enginePath("designir-pipeline", "scripts", "surfaceir_pipeline.py");
}
async function runJson(
command: string,
args: string[],
cwd: string,
timeout = 120_000,
) {
const { stdout } = await execFileAsync(command, args, {
cwd,
timeout: 120_000,
timeout,
maxBuffer: 1024 * 1024 * 8,
});
return JSON.parse(stdout) as Record<string, unknown>;
const lines = stdout.split(/\r?\n/);
for (let index = lines.length - 1; index >= 0; index -= 1) {
if (!lines[index]?.trim().startsWith("{")) {
continue;
}
try {
return JSON.parse(lines.slice(index).join("\n")) as Record<string, unknown>;
} catch {
// An OCCT log line may contain a brace; continue to the enclosing object.
}
}
throw new Error("CAD worker did not return a valid JSON result.");
}
export async function executeDesignIR({
@@ -91,6 +124,20 @@ export async function executeDesignIR({
backend_hint: selectedBackend,
};
await fs.writeFile(sourceAbsolutePath, `${JSON.stringify(normalizedDesignIR, null, 2)}\n`);
const migration = await runJson(
pythonExecutable(),
[
pipelineScript(),
"migrate",
sourceAbsolutePath,
"--output",
sourceAbsolutePath,
],
task.dir,
);
if (migration.valid !== true || migration.schema_version !== "3.0") {
throw new Error("DesignIR 3.0 normalization failed.");
}
const antiCheat = await runJson(
pythonExecutable(),
@@ -112,18 +159,37 @@ export async function executeDesignIR({
selectedBackend,
],
task.dir,
900_000,
);
if (rebuild.valid !== true) {
throw new Error("Isolated DesignIR rebuild failed.");
}
const materialized = await runJson(
pythonExecutable(),
[
pipelineScript(),
"materialize-hybrid",
sourceAbsolutePath,
"--step",
artifactAbsolutePath,
"--output",
sourceAbsolutePath,
],
task.dir,
900_000,
);
if (materialized.valid !== true) {
throw new Error("DesignIR 3.0 SurfaceIR materialization failed.");
}
const validation: string[] = [
"CAD Router selected the backend before compilation.",
"DesignIR anti-cheat validation passed.",
"DesignIR 3.0 semantic-layer anti-cheat validation passed.",
"STEP rebuilt in an isolated workspace without teacher STEP access.",
"Generated STEP was materialized as a non-authoritative SurfaceIR snapshot.",
];
const artifacts: Array<{ path: string; role: string; kind?: string }> = [
{ path: sourcePath, role: "editable_contract", kind: "designir-2.0" },
{ path: sourcePath, role: "editable_contract", kind: "designir-3.0" },
{ path: artifactPath, role: "primary", kind: "step" },
];
if (typeof rebuild.model_json === "string" && rebuild.model_json) {
@@ -180,10 +246,10 @@ export async function executeDesignIR({
},
source: {
path: sourcePath,
format: "designir-2.0",
format: "designir-3.0",
backend: selectedBackend,
source_editability: "editable_source",
generated_by: "llm_designir_tool_call",
generated_by: "llm_semantic_designir3_tool_call",
},
artifacts,
assumptions,
@@ -207,7 +273,351 @@ export async function executeDesignIR({
viewerAssetPath,
viewerAssetUrl: artifactUrl(task.taskId, viewerAssetPath, artifactVersion),
sourcePath,
summary: summary || "CAD model generated from executable DesignIR 2.0.",
summary: summary || "CAD model generated from semantic DesignIR 3.0.",
};
}
export async function reconstructUploadedStep({
prompt,
teacherTaskId,
teacherPath,
teacherName,
}: UploadedStepReconstructionInput): Promise<CadGenerationResult> {
const { absolutePath: teacherAbsolutePath } = await readArtifact(
teacherTaskId,
teacherPath,
);
const task = await ensureTask();
const route = await runJson(
pythonExecutable(),
[
routerScript(),
prompt || "重建上传的 STEP,并返回可修改参数",
"--edit-source",
teacherAbsolutePath,
"--task-dir",
task.dir,
"--execute",
"--maximum-parameters",
"8",
"--timeout-seconds",
"180",
],
task.dir,
900_000,
);
const execution = (
route.execution && typeof route.execution === "object"
? route.execution
: {}
) as Record<string, unknown>;
if (
execution.status !== "accepted"
&& execution.status !== "accepted_empty_source"
) {
throw new Error(
`DesignIR 3.0 reconstruction was not accepted: ${JSON.stringify(execution)}`,
);
}
const relativeArtifact = (value: unknown) => {
const absolute = path.resolve(String(value || ""));
const relative = path.relative(task.dir, absolute);
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`CAD Router returned an artifact outside the task: ${absolute}`);
}
return relative;
};
const sourcePath = relativeArtifact(execution.designir);
const artifactPath = relativeArtifact(execution.rebuilt_step);
const parametersPath = relativeArtifact(execution.parameters);
const geometryReportPath = relativeArtifact(execution.geometry_report);
const editReportPath = relativeArtifact(execution.edit_report);
const parameterCatalog = JSON.parse(
await fs.readFile(path.join(task.dir, parametersPath), "utf8"),
) as Record<string, unknown>;
const existingManifest = JSON.parse(
await fs.readFile(path.join(task.dir, "cad-task.json"), "utf8"),
) as Record<string, unknown>;
const viewerAssetPath = await createStepGlbPreview({
taskId: task.taskId,
stepPath: artifactPath,
});
const artifactVersion = Date.now().toString(36);
const experience = (
execution.experience && typeof execution.experience === "object"
? execution.experience
: {}
) as Record<string, unknown>;
await upsertManifest(task.taskId, {
request: prompt,
route: {
selected_backend: String(route.selected_backend || "build123d"),
runner_skill: "cad-router",
project: "text-to-cad",
execution_adapter: "DesignIR 3.0 / SurfaceIR",
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-3.0",
backend: "surfaceir_occt",
source_editability: "validated_parameter_source",
generated_by: "cad-router_uploaded_step_execute",
teacher_embedded: false,
},
artifacts: [
{ path: sourcePath, role: "editable_contract", kind: "designir-3.0" },
{ path: artifactPath, role: "primary", kind: "step" },
{ path: parametersPath, role: "validated_parameters", kind: "json" },
{ path: geometryReportPath, role: "geometry_acceptance", kind: "json" },
{ path: editReportPath, role: "parameter_acceptance", kind: "json" },
{ path: viewerAssetPath, role: "viewer", kind: "glb" },
],
parameters: parameterCatalog,
experience: {
context_kind: "promoted_surfaceir_runtime",
...experience,
},
assumptions: [],
validation: [
"Uploaded STEP was used only as extraction and acceptance truth.",
"DesignIR 3.0 anti-cheat validation passed.",
"STEP was independently rebuilt without passing the teacher path to the rebuild worker.",
"Independent geometry acceptance passed.",
`${Number(execution.validated_parameter_count || 0)} editable parameters passed isolated perturbation acceptance.`,
],
studio: {
latestArtifact: artifactPath,
latestPreview: artifactPath,
latestViewerAsset: viewerAssetPath,
latestVersion: artifactVersion,
uploadReconstruction: {
teacherTaskId,
teacherPath,
teacherName: teacherName || path.basename(teacherPath),
sourceEmbedded: false,
},
editCommandTemplate: existingManifest.edit_command_template || [],
},
});
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: [
`${teacherName || path.basename(teacherPath)} 已通过 DesignIR 3.0 / SurfaceIR 独立重建。`,
`几何验收通过,已验证可修改参数 ${Number(execution.validated_parameter_count || 0)} 个。`,
].join(" "),
};
}
export async function editDesignIR3Parameter({
prompt,
sourceTaskId,
parameter,
value,
}: {
prompt: string;
sourceTaskId: string;
parameter: string;
value: number;
}): Promise<CadGenerationResult> {
const sourceManifest = await readManifest(sourceTaskId);
if (!sourceManifest || sourceManifest.source?.format !== "designir-3.0") {
throw new Error("Current task is not an editable DesignIR 3.0 reconstruction.");
}
const sourcePath = String(sourceManifest.source.path || "");
const sourceAbsolutePath = path.join(taskDir(sourceTaskId), sourcePath);
const sourceDesignIR = JSON.parse(
await fs.readFile(sourceAbsolutePath, "utf8"),
) as Record<string, any>;
const parameterRecord = sourceDesignIR.semantic_layer?.parameters?.[parameter];
if (
!parameterRecord
|| parameterRecord.editable !== true
|| parameterRecord.edit_state !== "validated_executable_binding"
) {
throw new Error(
`${parameter} is not a perturbation-validated DesignIR 3.0 parameter.`,
);
}
const range = parameterRecord.validated_range || {};
const minimum = Number(range.minimum_tested_inclusive);
const maximum = Number(range.maximum_tested_inclusive);
if (
!Number.isFinite(value)
|| !Number.isFinite(minimum)
|| !Number.isFinite(maximum)
|| value < minimum
|| value > maximum
) {
throw new Error(
`${parameter} must remain within the validated range [${minimum}, ${maximum}].`,
);
}
const uploadReconstruction = (
sourceManifest.studio?.uploadReconstruction
&& typeof sourceManifest.studio.uploadReconstruction === "object"
? sourceManifest.studio.uploadReconstruction
: {}
) as Record<string, unknown>;
const teacherTaskId = String(uploadReconstruction.teacherTaskId || "");
const teacherPath = String(uploadReconstruction.teacherPath || "");
if (!teacherTaskId || !teacherPath) {
throw new Error(
"The DesignIR 3.0 task has no teacher reference for independent edit acceptance.",
);
}
const { absolutePath: teacherAbsolutePath } = await readArtifact(
teacherTaskId,
teacherPath,
);
const task = await ensureTask();
const acceptanceDir = path.join(task.dir, "validation", "parameter-edit");
const acceptance = await runJson(
pythonExecutable(),
[
surfaceirScript(),
"validate-edit",
teacherAbsolutePath,
sourceAbsolutePath,
"--parameter",
parameter,
"--value",
String(value),
"--output-dir",
acceptanceDir,
],
task.dir,
300_000,
);
if (acceptance.accepted !== true) {
throw new Error(
`DesignIR 3.0 parameter edit failed independent acceptance: ${String(acceptance.state || "rejected")}`,
);
}
const generatedStem = `${path.parse(sourcePath).name}.edited`;
const generatedDesignIR = path.join(
acceptanceDir,
`${generatedStem}.json`,
);
const generatedStep = path.join(
acceptanceDir,
`${generatedStem}.step`,
);
const safeParameter = sanitizeFilename(parameter);
const outputStem = `${path.parse(sourcePath).name}.${safeParameter}`;
const outputDesignIRPath = `${outputStem}.json`;
const outputStepPath = `${outputStem}.step`;
await fs.copyFile(
generatedDesignIR,
path.join(task.dir, outputDesignIRPath),
);
await fs.copyFile(generatedStep, path.join(task.dir, outputStepPath));
const editedDesignIR = JSON.parse(
await fs.readFile(path.join(task.dir, outputDesignIRPath), "utf8"),
) as Record<string, any>;
const updatedParameter = editedDesignIR.semantic_layer.parameters[parameter];
const parameterCatalog = {
...(sourceManifest.parameters || {}),
parameters: Array.isArray(sourceManifest.parameters?.parameters)
? sourceManifest.parameters.parameters.map((item: any) => (
item?.name === parameter
? { ...item, value: updatedParameter.value }
: item
))
: [],
};
const viewerAssetPath = await createStepGlbPreview({
taskId: task.taskId,
stepPath: outputStepPath,
});
const artifactVersion = Date.now().toString(36);
await upsertManifest(task.taskId, {
request: prompt,
route: {
selected_backend: "build123d",
runner_skill: "cad-router",
project: "text-to-cad",
execution_adapter: "DesignIR 3.0 / SurfaceIR parameter edit",
fallback_order: ["simplecadapi"],
workflow_profiles: [],
},
source: {
path: outputDesignIRPath,
format: "designir-3.0",
backend: "surfaceir_occt",
source_editability: "validated_parameter_source",
generated_by: "cad-router_designir3_parameter_edit",
parentTaskId: sourceTaskId,
teacher_embedded: false,
},
artifacts: [
{ path: outputDesignIRPath, role: "editable_contract", kind: "designir-3.0" },
{ path: outputStepPath, role: "primary", kind: "step" },
{
path: path.relative(
task.dir,
path.join(acceptanceDir, "acceptance-report.json"),
),
role: "parameter_acceptance",
kind: "json",
},
{ path: viewerAssetPath, role: "viewer", kind: "glb" },
],
parameters: parameterCatalog,
experience: sourceManifest.experience || {},
assumptions: [],
validation: [
`Parameter ${parameter} changed to ${value} within its validated range.`,
"The edited STEP passed independent teacher-side semantic edit acceptance.",
"The output remains independently rebuildable from DesignIR 3.0.",
],
studio: {
latestArtifact: outputStepPath,
latestPreview: outputStepPath,
latestViewerAsset: viewerAssetPath,
latestVersion: artifactVersion,
uploadReconstruction,
parentTaskId: sourceTaskId,
},
});
const stepUrl = artifactUrl(task.taskId, outputStepPath, artifactVersion);
return {
handled: true,
taskId: task.taskId,
artifactPath: outputStepPath,
artifactUrl: stepUrl,
previewPath: outputStepPath,
previewUrl: stepUrl,
viewerAssetPath,
viewerAssetUrl: artifactUrl(
task.taskId,
viewerAssetPath,
artifactVersion,
),
sourcePath: outputDesignIRPath,
summary: `${parameter} 已修改为 ${value},并通过独立参数验收。`,
};
}
+175 -19
View File
@@ -15,7 +15,12 @@ import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { z } from "zod/v4";
import { executeDesignIR, type CadGenerationResult } from "@/lib/cad-generator";
import {
editDesignIR3Parameter,
executeDesignIR,
reconstructUploadedStep,
type CadGenerationResult,
} from "@/lib/cad-generator";
import { loadLlmConfig, resolveProviderApiKey, selectedModelId, type LlmConfig } from "@/lib/config";
import { readManifest, taskDir } from "@/lib/task-store";
@@ -244,7 +249,7 @@ async function buildAttachmentUserContent(lastUserContent: string, attachments:
"\nUploaded files available to this turn:",
...attachedFileLines,
"Images are attached as model-visible image parts when the selected provider/model supports vision.",
"For STEP/STP or binary files, call inspect_uploaded_file with id/taskId/path before using their contents.",
"For STEP/STP reconstruction, call reconstruct_uploaded_step with id/taskId/path. Do not ask the model to author DesignIR from STEP text.",
].join("\n"),
});
}
@@ -307,7 +312,7 @@ function attachmentSummary(attachments: unknown[]) {
return [
"Current uploaded attachment JSON available to the agent:",
compactJson(attachments, 14000),
"If a STEP/STP attachment is present, inspect its deterministic evidence, then author DesignIR without reading or importing the teacher STEP.",
"If a STEP/STP attachment is present, call reconstruct_uploaded_step so CAD Router executes the deterministic DesignIR 3.0 / SurfaceIR pipeline.",
].join("\n");
}
@@ -319,15 +324,20 @@ function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], tas
"Never return only code when the user asks to generate CAD. Use the generate_cad tool instead.",
"Do not rely on hardcoded templates, canned examples, mock data, fake filenames, or imaginary viewer state.",
"The generate_cad tool validates executable DesignIR, selects a backend through CAD Router, and rebuilds STEP in a teacher-free sandbox.",
"When STEP files are attached, the source is teacher and acceptance truth only. Reconstruction consumes deterministic evidence, never source B-Rep.",
"When STEP files are attached, the source is teacher and acceptance truth only. Use reconstruct_uploaded_step; never import the teacher as model geometry.",
"All uploaded files are available to you. Images are attached directly to the latest user message when the selected provider/model supports vision. STEP/STP and binary files are available through inspect_uploaded_file.",
"When the user asks to model from an uploaded image, inspect the image directly from the image part. If the selected provider/model cannot process images, say that clearly and ask the user to switch to a vision-capable OpenAI-compatible model.",
"When the user asks to use an uploaded STEP/STP or other non-image file, call inspect_uploaded_file before making claims about its geometry or contents.",
"When the user asks to reconstruct an uploaded STEP/STP, call reconstruct_uploaded_step directly. Use inspect_uploaded_file only for read-only questions that do not request reconstruction.",
"When viewer context is present, use its cad-edit-intent.v1 or cad-ai-geometry-selection.v1 payload as precise geometry context.",
"If viewer context selection.scope is selected_reference_only, modify only that selected topology/feature. Do not change a global pattern parameter or all repeated/symmetric features unless the user explicitly asks for all of them.",
"For selected holes, use the selected reference center/surface/bbox/adjacent selectors to identify the individual feature. If the editable source models a repeated pattern with one shared diameter variable, split out or override the selected instance instead of changing the shared variable.",
"Prefer editing the DesignIR recorded in cad-task.json before regenerating STEP and viewer assets.",
"For CAD generation, author complete DesignIR 2.0 with parameters, datums, features, constraints, edit interface, and validation perturbations.",
"If the current task is an uploaded-STEP DesignIR 3.0 surface_parametric reconstruction and the requested change matches a validated parameter, call edit_designir3_parameter.",
"If the current task is a text/image DesignIR 3.0 fully_semantic_parametric or hybrid_semantic_surface_parametric model, modify its authoritative semantic_layer and call generate_cad again; preserve unrelated parameters, constraints, and features.",
"For every new text/image CAD generation, author complete DesignIR 3.0 with designir_kind=independent_parametric_cad, reconstruction_mode=fully_semantic_parametric, authoring_mode=semantic_feature_program, and semantic_layer containing reconstruction_status, coordinate_system, datums, parameters, expressions, sketches, constraints, features, patterns, attachments, and construction_stages.",
"For semantic DesignIR 3.0, edit_interface must contain semantic_parameters, surface_parameter_groups=[], modification_levels=[semantic_feature], and preserved_interfaces. validation_contract must contain source_independence=true, geometry_checks, edit_checks, invariants, perturbations, and thresholds.",
"Use semantic parameter names that describe design intent, such as flange_thickness, shelf_depth, hole_spacing, rib_count, bore_diameter, and bolt_circle_diameter. Every editable parameter must directly drive at least one feature and have a perturbation test.",
"Uploaded STEP reconstruction also uses DesignIR 3.0, but in deterministic surface_parametric mode. Do not substitute one mode for the other.",
"Never embed STEP/B-Rep/mesh data or source topology references in DesignIR.",
"If CAD generation is requested and the generate_cad tool is available, call generate_cad.",
"For modifications, use the current editable source of truth when provided. Preserve unrelated geometry and change only the selected/requested feature.",
@@ -494,24 +504,20 @@ async function inspectStepAttachment(attachment: AttachmentRecord) {
"}",
"print(json.dumps(payload, ensure_ascii=False))",
].join("\n");
const [{ stdout }, excerpt] = await Promise.all([
execFileAsync("python3", ["-c", script, absolutePath], {
const { stdout } = await execFileAsync(
process.env.CAD_PYTHON || path.join(process.cwd(), "..", "text-to-cad", ".venv", "bin", "python"),
["-c", script, absolutePath],
{
timeout: 30_000,
maxBuffer: 1024 * 1024,
}),
readTextExcerpt(attachment, 24_000).catch(() => null),
]);
},
);
const geometry = JSON.parse(stdout.trim().split(/\r?\n/).pop() || "{}");
return {
attachment,
inspectionKind: "step_geometry",
geometry,
stepTextExcerpt: excerpt ? {
text: excerpt.text,
truncated: excerpt.truncated,
bytesRead: excerpt.bytesRead,
totalBytes: excerpt.totalBytes,
} : null,
sourcePolicy: "Teacher geometry was measured read-only; raw STEP text is not exposed to the reconstruction model.",
};
}
@@ -687,9 +693,159 @@ export async function streamAgentResponse({
}
},
}),
reconstruct_uploaded_step: tool({
description: [
"Reconstruct one uploaded STEP/STP through CAD Router's executable DesignIR 3.0 / SurfaceIR pipeline.",
"Use this instead of generate_cad for an uploaded STEP.",
"It independently rebuilds STEP, runs geometry acceptance, validates editable parameters, and publishes the preview.",
].join(" "),
inputSchema: z.object({
id: z.string().optional().describe("Attachment id from the uploaded file list."),
taskId: z.string().optional().describe("Attachment taskId."),
path: z.string().optional().describe("STEP path inside the upload task."),
}),
execute: async ({ id, taskId, path: attachmentPath }) => {
const attachment = findAttachment(uploadedAttachments, {
id,
taskId,
path: attachmentPath,
});
if (!attachment || !isStepAttachment(attachment)) {
throw new Error(
"A STEP/STP attachment is required. Use id/taskId/path from the uploaded attachment list.",
);
}
writeCadProgress({
step: "execute_cad",
label: "DesignIR 3.0 独立重建",
status: "running",
message: "正在提取 SurfaceIR、独立重建、执行几何和参数验收。",
});
try {
cadGeneration = await reconstructUploadedStep({
prompt: lastUserText(chatMessages),
teacherTaskId: String(attachment.taskId),
teacherPath: String(attachment.path),
teacherName: attachment.name,
});
} catch (error) {
const message = error instanceof Error
? error.message
: "Uploaded STEP reconstruction failed.";
writeCadProgress({
step: "execute_cad",
label: "DesignIR 3.0 独立重建",
status: "error",
message,
});
failAgentStream(message);
throw error;
}
writeCadProgress({
step: "execute_cad",
label: "DesignIR 3.0 独立重建",
status: "success",
message: `${cadGeneration.artifactPath},参数验收已完成。`,
});
writeCadProgress({
step: "update_preview",
label: "更新右侧预览",
status: "success",
message: "独立重建模型已载入。",
});
completeAgentStream("DesignIR 3.0 重建和验收已完成。");
streamWriter?.write({
type: "data-cad-result",
data: {
taskId: cadGeneration.taskId,
sourcePath: cadGeneration.sourcePath,
artifactPath: cadGeneration.artifactPath,
artifactUrl: cadGeneration.artifactUrl,
previewPath: cadGeneration.previewPath,
previewUrl: cadGeneration.previewUrl,
viewerAssetPath: cadGeneration.viewerAssetPath,
viewerAssetUrl: cadGeneration.viewerAssetUrl,
summary: cadGeneration.summary,
},
id: `cad_result_${cadGeneration.taskId}`,
});
return cadGeneration;
},
}),
edit_designir3_parameter: tool({
description: [
"Edit one perturbation-validated parameter on the current DesignIR 3.0 reconstruction.",
"The requested value must be within the validated range shown in current cad-task.json.",
"The server reruns independent teacher-side semantic edit acceptance before publishing.",
].join(" "),
inputSchema: z.object({
parameter: z.string().describe("Validated parameter name from the current task."),
value: z.number().describe("New value within the parameter's validated range."),
}),
execute: async ({ parameter, value }) => {
if (!selectedTaskId) {
throw new Error("No current DesignIR 3.0 task is selected.");
}
writeCadProgress({
step: "execute_cad",
label: "修改 DesignIR 3.0 参数",
status: "running",
message: `${parameter} → ${value},正在执行独立参数验收。`,
});
try {
cadGeneration = await editDesignIR3Parameter({
prompt: lastUserText(chatMessages),
sourceTaskId: selectedTaskId,
parameter,
value,
});
} catch (error) {
const message = error instanceof Error
? error.message
: "DesignIR 3.0 parameter edit failed.";
writeCadProgress({
step: "execute_cad",
label: "修改 DesignIR 3.0 参数",
status: "error",
message,
});
failAgentStream(message);
throw error;
}
writeCadProgress({
step: "execute_cad",
label: "修改 DesignIR 3.0 参数",
status: "success",
message: cadGeneration.summary,
});
writeCadProgress({
step: "update_preview",
label: "更新右侧预览",
status: "success",
message: "参数修改后的模型已载入。",
});
completeAgentStream("DesignIR 3.0 参数修改和验收已完成。");
streamWriter?.write({
type: "data-cad-result",
data: {
taskId: cadGeneration.taskId,
sourcePath: cadGeneration.sourcePath,
artifactPath: cadGeneration.artifactPath,
artifactUrl: cadGeneration.artifactUrl,
previewPath: cadGeneration.previewPath,
previewUrl: cadGeneration.previewUrl,
viewerAssetPath: cadGeneration.viewerAssetPath,
viewerAssetUrl: cadGeneration.viewerAssetUrl,
summary: cadGeneration.summary,
},
id: `cad_result_${cadGeneration.taskId}`,
});
return cadGeneration;
},
}),
generate_cad: tool({
description: [
"Generate a real CAD model from agent-authored executable DesignIR 2.0.",
"Generate a real CAD model from agent-authored semantic DesignIR 3.0.",
"Call this only when the user wants CAD generation or a model modification.",
"CAD Router selects the backend and the server rebuilds STEP without teacher geometry.",
].join(" "),
@@ -697,7 +853,7 @@ export async function streamAgentResponse({
summary: z.string().describe("Concise user-facing summary of the generated CAD model."),
designirFilename: z.string().describe("DesignIR filename, e.g. model.designir.json."),
stepFilename: z.string().describe("STEP output filename, e.g. model.step."),
designir: z.record(z.string(), z.unknown()).describe("Complete executable DesignIR 2.0 JSON object."),
designir: z.record(z.string(), z.unknown()).describe("Complete semantic DesignIR 3.0 JSON object in fully_semantic_parametric mode."),
teacherTaskId: z.string().optional().describe("Uploaded teacher STEP task id for independent acceptance only."),
teacherPath: z.string().optional().describe("Uploaded teacher STEP path for independent acceptance only."),
assumptions: z.array(z.string()).default([]).describe("Assumptions made because the user did not specify every dimension."),