from __future__ import annotations import json import mimetypes import re import shutil import subprocess import sys import traceback import zipfile import xml.etree.ElementTree as ET from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlparse from ..cli import _prepare_structured_run_dir from ..joint_module.auto_design import DEFAULT_PLATFORM_PROFILE, run_auto_joint_design from ..joint_module.generator import run_joint_module from ..joint_module.urdf_exporter import export_urdf from ..models import read_json, write_json, write_model_json from ..parameter_solver import load_requirement, solve_parameters from ..kinematics import motion_samples, solve_instance from ..placement import solve_reducer_placements from ..cad.simplecad_generator import generate_simplecad from ..reducer_urdf_exporter import export_reducer_urdf from ..topology import build_graph, validate_topology from ..topology_runtime import load_default_template from ..validators import validate_from_files from ..task_store import ( BACKEND_ROOT, RUN_ROOT, artifact_url, ensure_task, list_tasks, read_task_manifest, safe_task_id, task_dir, update_task, write_upload, ) DEFAULT_RUN_NAME = "frontend_split_motor_7nm" DEFAULT_JOINT_REQUIREMENT = BACKEND_ROOT / "input" / "requirements" / "joint_modules" / "3500_motor_housing.json" JOINT_REQUIREMENT_TEMPLATES = { "simple_2k_h": DEFAULT_JOINT_REQUIREMENT, "simple_2k_h_cascade": BACKEND_ROOT / "input" / "requirements" / "joint_modules" / "3500_motor_housing_cascade.json", } CAD_ROUTER_RECONSTRUCTOR = Path("/Users/lk/Projects/cadSet/text-to-cad/skills/cad-router/scripts/reconstruct_step.py") REDUCER_REQUIREMENT_TEMPLATES = { "simple_2k_h": BACKEND_ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / "simple_2kh_ratio_5.json", "simple_2k_h_cascade": BACKEND_ROOT / "input" / "requirements" / "reducers" / "simple_2k_h_cascade" / "simple_2kh_cascade_ratio_9_spur.json", "ferguson_wolfrom": BACKEND_ROOT / "input" / "requirements" / "reducers" / "ferguson_wolfrom" / "ferguson_wolfrom_ratio_531p25_spur.json", } SUPPORTED_REQUIREMENT_OVERRIDES = { "topology_family", "target_ratio", "tooth_form", "module_mm", "planet_count", "helix_angle_deg", "pressure_angle_deg", "face_width_mm", "backlash_mm", "ring_rim_thickness_mm", "max_outer_diameter_mm", } def _send_json(handler: BaseHTTPRequestHandler, status: int, payload: object) -> None: body = json.dumps(payload, ensure_ascii=False).encode("utf-8") handler.send_response(status) handler.send_header("Content-Type", "application/json; charset=utf-8") handler.send_header("Content-Length", str(len(body))) handler.send_header("Access-Control-Allow-Origin", "*") handler.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS") handler.send_header("Access-Control-Allow-Headers", "Content-Type") handler.end_headers() handler.wfile.write(body) def _send_file(handler: BaseHTTPRequestHandler, path: Path) -> None: if not path.exists() or not path.is_file(): _send_json(handler, 404, {"error": f"file_not_found: {path}"}) return content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream" body = path.read_bytes() handler.send_response(200) handler.send_header("Content-Type", content_type) handler.send_header("Content-Length", str(len(body))) handler.send_header("Access-Control-Allow-Origin", "*") handler.end_headers() handler.wfile.write(body) def _read_body(handler: BaseHTTPRequestHandler) -> bytes: length = int(handler.headers.get("Content-Length") or 0) return handler.rfile.read(length) if length else b"" def _read_json(handler: BaseHTTPRequestHandler) -> dict[str, object]: body = _read_body(handler) if not body.strip(): return {} payload = json.loads(body.decode("utf-8")) if not isinstance(payload, dict): raise ValueError("request_body_must_be_json_object") return payload def _safe_relative_file(task_id: str, relative: str) -> Path: root = task_dir(task_id).resolve() path = (root / unquote(relative)).resolve() try: path.relative_to(root) except ValueError as exc: raise ValueError("invalid_artifact_path") from exc return path def _first_number(text: str, patterns: list[str], *, integer: bool = False) -> int | float | None: for pattern in patterns: match = re.search(pattern, text, flags=re.IGNORECASE) if match: value = float(match.group(1)) return int(value) if integer else value return None def _request_requirement_overrides(request: str) -> dict[str, object]: text = str(request or "") overrides: dict[str, object] = {} if re.search(r"(双级|两级|二级|串联|cascade|two[- ]stage)", text, flags=re.IGNORECASE): overrides["topology_family"] = "simple_2k_h_cascade" elif re.search(r"(ferguson|wolfrom|沃尔夫罗姆|弗格森)", text, flags=re.IGNORECASE): overrides["topology_family"] = "ferguson_wolfrom" elif re.search(r"(单级|simple[_ -]?2k[_ -]?h)", text, flags=re.IGNORECASE): overrides["topology_family"] = "simple_2k_h" tooth_form_match = re.search(r"(斜齿|helical|直齿|spur)", text, flags=re.IGNORECASE) if tooth_form_match: overrides["tooth_form"] = ( "helical" if re.search(r"(斜齿|helical)", tooth_form_match.group(1), flags=re.IGNORECASE) else "spur" ) numeric_patterns: dict[str, list[str]] = { "target_ratio": [ r"(?:传动比|减速比|速比|ratio)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)", r"([0-9]+(?:\.[0-9]+)?)\s*(?::|:)\s*1\s*(?:传动比|减速比)?", ], "module_mm": [ r"(?:齿轮)?模数\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)", r"module(?:_mm)?\s*(?:=|:|to)?\s*([0-9]+(?:\.[0-9]+)?)", ], "planet_count": [ r"(?:行星轮数量|行星轮数|行星轮)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+)", r"([0-9]+)\s*(?:个|颗|只)?\s*行星轮", r"planet_count\s*(?:=|:|to)?\s*([0-9]+)", ], "helix_angle_deg": [ r"(?:螺旋角|helix_angle(?:_deg)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)", ], "pressure_angle_deg": [ r"(?:压力角|pressure_angle(?:_deg)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)", ], "face_width_mm": [ r"(?:齿宽|face_width(?:_mm)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)", ], "backlash_mm": [ r"(?:齿侧间隙|侧隙|backlash(?:_mm)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)", ], "ring_rim_thickness_mm": [ r"(?:齿圈轮缘厚度|轮缘厚度|ring_rim_thickness(?:_mm)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)", ], "max_outer_diameter_mm": [ r"(?:最大外径|外径)\s*(?:小于|不超过|限制为|为|是|=|:|:|to|≤|<=)?\s*([0-9]+(?:\.[0-9]+)?)\s*(?:mm|毫米)?", r"max_outer_diameter(?:_mm)?\s*(?:=|:|to|<=)?\s*([0-9]+(?:\.[0-9]+)?)", ], } for name, patterns in numeric_patterns.items(): value = _first_number(text, patterns, integer=name == "planet_count") if value is not None: overrides[name] = value return overrides def _normalized_requirement_overrides(payload: dict[str, object]) -> dict[str, object]: overrides = _request_requirement_overrides(str(payload.get("request") or "")) explicit = payload.get("requirementOverrides") if explicit is not None and not isinstance(explicit, dict): raise ValueError("requirementOverrides_must_be_an_object") if isinstance(explicit, dict): unknown = sorted(set(explicit) - SUPPORTED_REQUIREMENT_OVERRIDES) if unknown: raise ValueError(f"unsupported_requirement_overrides: {unknown}") overrides.update({key: value for key, value in explicit.items() if value is not None}) return overrides def _apply_requirement_overrides( requirement: dict[str, object], overrides: dict[str, object], ) -> dict[str, object]: result = json.loads(json.dumps(requirement)) topology = str(overrides.get("topology_family") or result.get("topology_family") or "simple_2k_h") result["topology_family"] = topology direct_fields = { "target_ratio", "tooth_form", "helix_angle_deg", "pressure_angle_deg", "face_width_mm", "backlash_mm", "ring_rim_thickness_mm", } for name in direct_fields: if name in overrides: result[name] = overrides[name] if "module_mm" in overrides: result["module_candidates_mm"] = [float(overrides["module_mm"])] if "planet_count" in overrides: result["planet_count_candidates"] = [int(overrides["planet_count"])] if "max_outer_diameter_mm" in overrides: constraints = dict(result.get("constraints") or {}) constraints["max_outer_diameter_mm"] = float(overrides["max_outer_diameter_mm"]) result["constraints"] = constraints if result.get("tooth_form") == "helical" and not result.get("helix_angle_deg"): result["helix_angle_deg"] = 15.0 if result.get("tooth_form") == "spur": result.pop("helix_angle_deg", None) stages = result.get("stages") if isinstance(stages, list): for stage_value in stages: if not isinstance(stage_value, dict): continue if "module_mm" in overrides: stage_value["module_candidates_mm"] = [float(overrides["module_mm"])] if "planet_count" in overrides: stage_value["planet_count_candidates"] = [int(overrides["planet_count"])] for name in { "tooth_form", "helix_angle_deg", "pressure_angle_deg", "face_width_mm", "backlash_mm", "ring_rim_thickness_mm", }: if name in overrides: stage_value[name] = overrides[name] if result.get("tooth_form") == "spur": stage_value.pop("helix_angle_deg", None) return result def _materialize_reducer_requirement( task_id: str, payload: dict[str, object], ) -> tuple[Path, dict[str, object]]: overrides = _normalized_requirement_overrides(payload) requested_path = payload.get("requirementPath") if requested_path: base_path = Path(str(requested_path)) if not base_path.is_absolute(): base_path = BACKEND_ROOT / base_path else: topology = str(overrides.get("topology_family") or "simple_2k_h") if topology not in REDUCER_REQUIREMENT_TEMPLATES: raise ValueError(f"unsupported_topology_family: {topology}") base_path = REDUCER_REQUIREMENT_TEMPLATES[topology] requirement = read_json(base_path) if overrides: requirement = _apply_requirement_overrides(requirement, overrides) topology = str(requirement.get("topology_family") or "") if topology == "ferguson_wolfrom": requested_ratio = float(requirement.get("target_ratio") or 0) if abs(requested_ratio - 531.25) > 1e-9: raise ValueError( "ferguson_wolfrom currently supports only the validated ratio 531.25" ) target = task_dir(task_id) / "requirements" / "active_reducer_requirement.json" write_json(target, requirement) return target, overrides def _joint_reducer_requirement_path(joint_requirement_path: Path) -> Path: payload = read_json(joint_requirement_path) reducer_path = Path(str(payload.get("reducer_requirement") or "")) if not str(reducer_path): raise ValueError("joint_requirement_has_no_reducer_requirement") if not reducer_path.is_absolute(): reducer_path = joint_requirement_path.parent / reducer_path return reducer_path.resolve() def _materialize_joint_requirement( task_id: str, joint_requirement_path: Path, overrides: dict[str, object], ) -> tuple[Path, Path]: joint_requirement = read_json(joint_requirement_path) reducer_path = _joint_reducer_requirement_path(joint_requirement_path) reducer_requirement = _apply_requirement_overrides(read_json(reducer_path), overrides) reducer_target = task_dir(task_id) / "requirements" / "active_reducer_requirement.json" joint_target = task_dir(task_id) / "requirements" / "active_joint_requirement.json" write_json(reducer_target, reducer_requirement) joint_requirement["reducer_requirement"] = str(reducer_target.resolve()) if "platform_profile" in joint_requirement: profile_path = Path(str(joint_requirement["platform_profile"])) if not profile_path.is_absolute(): profile_path = (joint_requirement_path.parent / profile_path).resolve() joint_requirement["platform_profile"] = str(profile_path) write_json(joint_target, joint_requirement) return joint_target, reducer_target def _project_summary(run_name: str) -> dict[str, object]: run = RUN_ROOT / run_name formula = read_json(run / "reducer" / "formula" / "formula_instance.json") kinematics = read_json(run / "reducer" / "kinematics" / "kinematic_solution.json") reducer_validation = read_json(run / "reducer" / "validation" / "validation_report.json") joint_validation_path = run / "joint_module_validation_report.json" joint_validation = read_json(joint_validation_path) if joint_validation_path.exists() else {"summary": {"total": 0, "passed": 0, "failed": 0}, "run_id": ""} return { "source": f"output/runs/{run_name}", "runId": joint_validation.get("run_id") or formula.get("run_id"), "ratio": formula["derived"]["ratio"], "parameters": formula["parameters"], "derived": formula["derived"], "speeds": kinematics["speeds"], "reducerValidation": reducer_validation["summary"], "jointValidation": joint_validation["summary"], } def _design_catalog() -> list[dict[str, object]]: title_by_topology = { "simple_2k_h": "单级 2K-H 关节模组", "simple_2k_h_cascade": "两级 2K-H 串联关节", "ferguson_wolfrom": "高传动比复合行星减速器", } designs: list[dict[str, object]] = [] for run_dir in sorted(RUN_ROOT.iterdir() if RUN_ROOT.exists() else []): if not run_dir.is_dir(): continue urdf_dir = run_dir / "exports" / "urdf" urdf_path = urdf_dir / "joint_module.urdf" kind = "joint_module" if not urdf_path.exists(): urdf_path = urdf_dir / "reducer.urdf" kind = "reducer" if not urdf_path.exists(): continue formula_candidates = [ run_dir / "joint" / "reducer" / "formula" / "formula_instance.json", run_dir / "reducer" / "formula" / "formula_instance.json", run_dir / "formula" / "formula_instance.json", ] formula_path = next((path for path in formula_candidates if path.exists()), None) formula = read_json(formula_path) if formula_path else {} topology = str(formula.get("topology_family") or "planetary_reducer") derived = formula.get("derived") if isinstance(formula.get("derived"), dict) else {} parameters = formula.get("parameters") if isinstance(formula.get("parameters"), dict) else {} motion_path = urdf_dir / ("joint_motion_demo.json" if kind == "joint_module" else "reducer_motion_demo.json") motion = read_json(motion_path) if motion_path.exists() else {} ratio = motion.get("ratio") or derived.get("ratio") task_manifest = read_task_manifest(run_dir.name) request = str((task_manifest or {}).get("request") or "") joint_validation_path = run_dir / "joint_module_validation_report.json" reducer_validation_candidates = [ run_dir / "reducer" / "validation" / "validation_report.json", run_dir / "joint" / "reducer" / "validation" / "validation_report.json", run_dir / "validation" / "validation_report.json", ] reducer_validation_path = next( (path for path in reducer_validation_candidates if path.exists()), None, ) validation_path = ( joint_validation_path if kind == "joint_module" and joint_validation_path.exists() else reducer_validation_path ) validation = read_json(validation_path) if validation_path else {} validation_summary = ( validation.get("summary") if isinstance(validation.get("summary"), dict) else {} ) failed_checks = [ { "code": str(check.get("code") or "validation_failed"), "message": str(check.get("message") or "验证未通过"), } for check in validation.get("checks", []) if isinstance(check, dict) and check.get("passed") is False ] physically_verified = bool(validation) and validation.get("overall_passed") is True source = "api_task" if task_manifest else "project_preset_run" is_test_artifact = bool(task_manifest) and bool( re.search(r"(?:集成验证|测试|\btest\b)", request, flags=re.IGNORECASE) ) root = ET.fromstring(urdf_path.read_text(encoding="utf-8")) link_names = [str(node.get("name") or "") for node in root.findall("link")] continuous_joints = [ str(node.get("name") or "") for node in root.findall("joint") if node.get("type") in {"continuous", "revolute"} ] title = title_by_topology.get(topology, "行星传动方案") if task_manifest and request: title = request designs.append({ "id": run_dir.name, "runId": run_dir.name, "kind": kind, "topology": topology, "title": title, "request": request, "ratio": ratio, "parameters": parameters, "derived": derived, "urdfUrl": f"/api/design-assets/{run_dir.name}/{urdf_path.name}", "assetBase": f"/api/design-assets/{run_dir.name}", "motion": motion, "linkCount": len(link_names), "movingJointCount": len(continuous_joints), "motorIncluded": any( link_name == "motor_link" or link_name.startswith("motor_") for link_name in link_names ), # A successful generation task only means that files were written. # Catalog eligibility is based on the relevant physical validation # report, never on the presence of a URDF or on task status alone. "status": "verified" if physically_verified else "invalid", "eligible": physically_verified, "source": source, "sourceLabel": "CAD API 测试任务" if is_test_artifact else ( "CAD API 生成" if task_manifest else "项目预设生成" ), "catalogClass": "test_artifact" if is_test_artifact else "design", "validationScope": "关节模组" if kind == "joint_module" else "减速器", "validationSummary": validation_summary, "failedChecks": failed_checks, "validationReport": str(validation_path.relative_to(run_dir)) if validation_path else "", }) preferred = { "frontend_split_motor_7nm": 0, "frontend_split_motor_7nm_cascade": 1, "ferguson_wolfrom_ratio_531p25_spur": 2, } return sorted(designs, key=lambda item: (preferred.get(str(item["id"]), 10), str(item["id"]))) def _artifact_record(task_id: str, name: str, path: Path, role: str, kind: str) -> dict[str, object]: return { "name": name, "role": role, "kind": kind, "path": str(path.resolve()), "url": artifact_url(task_id, path), "size": path.stat().st_size if path.exists() else 0, } def _summarize_reducer_task(task_id: str, run_dir: Path, request: str) -> dict[str, object]: formula = read_json(run_dir / "formula" / "formula_instance.json") validation = read_json(run_dir / "validation" / "validation_report.json") manifest = read_json(run_dir / "cad" / "assembly_manifest.json") artifacts = [ _artifact_record(task_id, "reducer.step", run_dir / "cad" / "reducer.step", "primary", "step"), _artifact_record(task_id, "assembly_manifest.json", run_dir / "cad" / "assembly_manifest.json", "manifest", "json"), _artifact_record(task_id, "formula_instance.json", run_dir / "formula" / "formula_instance.json", "formula", "json"), _artifact_record(task_id, "validation_report.json", run_dir / "validation" / "validation_report.json", "validation", "json"), ] return { "status": "success" if validation.get("overall_passed") else "error", "kind": "reducer", "request": request, "summary": f"{formula['topology_family']} reducer ratio={formula['derived']['ratio']}", "ratio": formula["derived"]["ratio"], "parameters": formula["parameters"], "derived": formula["derived"], "validation": validation, "assemblyManifest": manifest, "artifacts": artifacts, "route": { "selected_backend": "simplecadapi", "effective_execution_engine": "simplecadapi", "runner_skill": "cad-router", "project": "planetary-reducer-system", "fallback_order": ["build123d"], "workflow_profiles": ["requirement_refinement", "visual_repair"], }, "source": { "path": "formula/formula_instance.json", "format": "json", "backend": "simplecadapi", "requirement_path": "input_snapshot", }, "validation": [validation], } def _summarize_joint_task(task_id: str, run_dir: Path, request: str) -> dict[str, object]: joint_dir = run_dir / "joint" if (run_dir / "joint").exists() else run_dir reducer_dir = joint_dir / "reducer" formula = read_json(reducer_dir / "formula" / "formula_instance.json") reducer_validation = read_json(reducer_dir / "validation" / "validation_report.json") joint_validation = read_json(joint_dir / "joint_module_validation_report.json") joint_manifest = read_json(joint_dir / "joint_module_manifest.json") artifacts = [ _artifact_record(task_id, "joint_module.step", joint_dir / "joint_module.step", "primary", "step"), _artifact_record(task_id, "joint_module_manifest.json", joint_dir / "joint_module_manifest.json", "manifest", "json"), _artifact_record(task_id, "joint_module_validation_report.json", joint_dir / "joint_module_validation_report.json", "validation", "json"), _artifact_record(task_id, "formula_instance.json", reducer_dir / "formula" / "formula_instance.json", "formula", "json"), ] exports_dir = run_dir / "exports" / "urdf" if (exports_dir / "joint_module.urdf").exists(): artifacts.append(_artifact_record(task_id, "joint_module.urdf", exports_dir / "joint_module.urdf", "viewer", "urdf")) return { "status": "success" if joint_validation.get("overall_passed") else "error", "kind": "joint_module", "request": request, "summary": f"planetary joint module ratio={formula['derived']['ratio']}", "ratio": formula["derived"]["ratio"], "parameters": formula["parameters"], "derived": formula["derived"], "reducerValidation": reducer_validation, "jointValidation": joint_validation, "jointManifest": joint_manifest, "artifacts": artifacts, "route": { "selected_backend": "simplecadapi", "effective_execution_engine": "simplecadapi", "runner_skill": "cad-router", "project": "planetary-reducer-system", "fallback_order": ["build123d"], "workflow_profiles": ["requirement_refinement", "visual_repair"], }, "source": { "path": "joint/reducer/formula/formula_instance.json" if (run_dir / "joint").exists() else "reducer/formula/formula_instance.json", "format": "json", "backend": "simplecadapi", "requirement_path": "generated_joint_requirement.json", }, "validation": [reducer_validation, joint_validation], } def _write_zip(source_dir: Path, target: Path) -> Path: target.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(target, "w", compression=zipfile.ZIP_DEFLATED) as archive: for path in sorted(source_dir.rglob("*")): if path.is_file(): archive.write(path, path.relative_to(source_dir)) return target def _find_requirement_snapshot(task_id: str) -> Path: snapshot_dir = task_dir(task_id) / "input_snapshot" candidates = sorted(snapshot_dir.glob("*.json")) if not candidates: candidates = sorted((task_dir(task_id) / "joint").glob("**/input_snapshot/*.json")) if not candidates: raise ValueError("task_has_no_requirement_snapshot") return candidates[0] def _parameter_patch(parameter: str, value: object) -> dict[str, object]: name = str(parameter or "").strip() if not name: raise ValueError("parameter_is_required") if name in {"module", "module_mm"}: return {"module_candidates_mm": [float(value)]} if name in {"planet_count", "planetCount"}: return {"planet_count_candidates": [int(value)]} if name in {"target_ratio", "ratio"}: return {"target_ratio": float(value)} if name in {"tooth_form", "toothForm"}: normalized = str(value).strip().lower() if normalized not in {"spur", "helical"}: raise ValueError("tooth_form_must_be_spur_or_helical") return {"tooth_form": normalized} if name in {"helix_angle_deg", "pressure_angle_deg", "face_width_mm", "backlash_mm", "ring_rim_thickness_mm"}: return {name: float(value)} raise ValueError(f"parameter_not_editable: {name}") def edit_task_parameter(task_id: str, parameter: str, value: object) -> dict[str, object]: task = read_task_manifest(task_id) if not task: raise ValueError("task_not_found") kind = str(task.get("kind") or "") if kind not in {"reducer", "joint_module"}: raise ValueError("parameter_edit_requires_generated_reducer_or_joint_task") run_dir = task_dir(task_id) active_requirement = task.get("activeReducerRequirementPath") requirement_path = Path(str(active_requirement)) if active_requirement else ( run_dir / "generated_reducer_requirement.json" if kind == "joint_module" and (run_dir / "generated_reducer_requirement.json").exists() else _find_requirement_snapshot(task_id) ) requirement = read_json(requirement_path) requirement.update(_parameter_patch(parameter, value)) revision = int(task.get("revision") or 0) + 1 edited_requirement = run_dir / "revisions" / f"reducer_requirement_{revision}.json" edited_requirement.parent.mkdir(parents=True, exist_ok=True) write_json(edited_requirement, requirement) request = f"{task.get('request', 'CAD task')} · edit {parameter}={value}" if kind == "joint_module": joint_requirement_path = run_dir / "generated_joint_requirement.json" if not joint_requirement_path.exists(): raise ValueError("joint_task_has_no_generated_joint_requirement") joint_requirement = read_json(joint_requirement_path) joint_requirement["reducer_requirement"] = str(edited_requirement.resolve()) edited_joint_requirement = run_dir / "revisions" / f"joint_requirement_{revision}.json" write_json(edited_joint_requirement, joint_requirement) payload = { "request": request, "requirementPath": str(edited_joint_requirement), "mode": "requirement", "kind": "joint_module", } else: payload = { "request": request, "requirementPath": str(edited_requirement), "mode": "requirement", "kind": "reducer", } update_task(task_id, { "status": "running", "revision": revision, "parentTaskId": task_id, "editedParameter": {"name": parameter, "value": value}, }) result = ( generate_joint_task(task_id, payload) if kind == "joint_module" else generate_reducer_task(task_id, payload) ) return update_task(task_id, { **result, "editedParameter": {"name": parameter, "value": value}, "revision": revision, "activeReducerRequirementPath": str(edited_requirement.resolve()), }) def reconstruct_uploaded_task(task_id: str, request: str = "重建上传的 STEP") -> dict[str, object]: manifest = read_task_manifest(task_id) if not manifest: raise ValueError("task_not_found") uploads = [item for item in manifest.get("uploads", []) if item.get("kind") == "step"] if not uploads: raise ValueError("step_upload_required") attachment = uploads[-1] source = task_dir(task_id) / str(attachment["path"]) if not source.exists(): raise ValueError("uploaded_step_not_found") if not CAD_ROUTER_RECONSTRUCTOR.exists(): raise ValueError(f"cad_router_reconstructor_not_found: {CAD_ROUTER_RECONSTRUCTOR}") teacher_root = RUN_ROOT / "_reconstruction_sources" / safe_task_id(task_id) teacher_root.mkdir(parents=True, exist_ok=True) teacher = teacher_root / source.name shutil.copy2(source, teacher) revision = int(manifest.get("reconstructionRevision") or 0) + 1 reconstruction_dir = task_dir(task_id) / "revisions" / f"reconstruction_{revision}" completed = subprocess.run( [ sys.executable, str(CAD_ROUTER_RECONSTRUCTOR), "--source", str(teacher), "--task-dir", str(reconstruction_dir), "--request", request, ], capture_output=True, text=True, timeout=900, check=False, ) if completed.returncode != 0: detail = completed.stderr.strip() or completed.stdout.strip() raise RuntimeError(f"cad_router_reconstruction_failed: {detail[-3000:]}") lines = [line for line in completed.stdout.splitlines() if line.strip()] result = json.loads(lines[-1]) if lines else {} artifact_paths = [] for path in sorted(reconstruction_dir.rglob("*")): if path.is_file() and path.name not in {"cad-task.json"}: artifact_paths.append(_artifact_record(task_id, path.name, path, "reconstruction", path.suffix.lstrip(".") or "file")) return update_task(task_id, { "status": "success", "kind": "step_reconstruction", "request": request, "route": { "selected_backend": result.get("selected_backend", "build123d"), "effective_execution_engine": "surfaceir", "runner_skill": "cad-router", "project": "text-to-cad", }, "source": { "path": str(attachment["path"]), "format": "step", "backend": "uploaded", "source_editability": "reconstruction_reference", }, "reconstruction": result, "reconstructionRevision": revision, "artifacts": artifact_paths, }) def export_task_robot(task_id: str, format_name: str) -> dict[str, object]: manifest = read_task_manifest(task_id) if not manifest: raise ValueError("task_not_found") if format_name not in {"urdf", "mjcf"}: raise ValueError("export_format_must_be_urdf_or_mjcf") run_dir = task_dir(task_id) urdf_output = run_dir / "exports" / "urdf" if manifest.get("kind") == "reducer": export_manifest = export_reducer_urdf( assembly_manifest_path=run_dir / "cad" / "assembly_manifest.json", reducer_instance_path=run_dir / "formula" / "formula_instance.json", out_dir=urdf_output, ) elif manifest.get("kind") == "joint_module": export_manifest = export_urdf( joint_manifest_path=run_dir / "joint" / "joint_module_manifest.json" if (run_dir / "joint").exists() else run_dir / "joint_module_manifest.json", reducer_instance_path=run_dir / "joint" / "reducer" / "formula" / "formula_instance.json" if (run_dir / "joint").exists() else run_dir / "reducer" / "formula" / "formula_instance.json", out_dir=urdf_output, ) else: raise ValueError("robot_export_requires_generated_task") if format_name == "mjcf": try: import mujoco # type: ignore import pymeshlab # type: ignore except ImportError as exc: raise ValueError("mjcf_export_requires_mujoco_and_pymeshlab") from exc output = run_dir / "exports" / "mjcf" mesh_dir = output / "meshes" mesh_dir.mkdir(parents=True, exist_ok=True) for source in sorted((urdf_output / "meshes").glob("*.stl")): mesh_set = pymeshlab.MeshSet() mesh_set.load_new_mesh(str(source)) mesh_set.save_current_mesh(str(mesh_dir / source.name), binary=True) urdf_path = ( urdf_output / "joint_module.urdf" if (urdf_output / "joint_module.urdf").exists() else urdf_output / "reducer.urdf" ) if not urdf_path.exists(): raise ValueError("urdf_intermediate_not_found") root = ET.fromstring(urdf_path.read_text(encoding="utf-8")) for mesh in root.findall(".//mesh"): filename = str(mesh.get("filename") or "") mesh.set("filename", str((mesh_dir / Path(filename).name).resolve())) intermediate = output / "mujoco-input.urdf" intermediate.write_text( ET.tostring(root, encoding="unicode", xml_declaration=True), encoding="utf-8", ) description = output / "robot.mjcf.xml" model = mujoco.MjModel.from_xml_path(str(intermediate)) mujoco.mj_saveLastXML(str(description), model) intermediate.unlink(missing_ok=True) validation = { "engine": f"mujoco {mujoco.__version__}", "parsed": True, "body_count": int(model.nbody), "joint_count": int(model.njnt), "mesh_count": int(model.nmesh), } write_json(output / "mjcf_validation.json", validation) package = _write_zip(output, run_dir / "exports" / "mjcf.zip") export_manifest = { "format": "mjcf", "description_path": str(description.resolve()), "validation": validation, } else: output = urdf_output package = _write_zip(output, run_dir / "exports" / "urdf.zip") artifacts = list(manifest.get("artifacts", [])) for path in [ package, output / "joint_module.urdf", output / "reducer.urdf", output / "robot.mjcf.xml", output / "urdf_export_manifest.json", output / "export_manifest.json", output / "mjcf_validation.json", ]: if path.exists(): artifacts.append(_artifact_record(task_id, path.name, path, "robot_export", path.suffix.lstrip(".") or "file")) return update_task(task_id, { "status": "success", "artifacts": artifacts, "robotExport": { "format": format_name, "packageUrl": artifact_url(task_id, package), "manifest": export_manifest, }, }) def generate_reducer_task(task_id: str, payload: dict[str, object]) -> dict[str, object]: request = str(payload.get("request") or "Generate planetary reducer") requirement_path, overrides = _materialize_reducer_requirement(task_id, payload) run_dir = task_dir(task_id) paths = _prepare_structured_run_dir(run_dir) requirement = load_requirement(requirement_path) template = load_default_template(requirement.topology_family) topology_errors = validate_topology(template, requirement) if topology_errors: raise ValueError(f"topology_invalid: {topology_errors}") instance, report = solve_parameters(requirement=requirement, template=template) solution = solve_instance(instance) samples = motion_samples(instance, solution) placement_plan = solve_reducer_placements(instance) shutil.copy2(requirement_path, paths["input_snapshot"] / requirement_path.name) write_model_json(paths["formula"] / "formula_instance.json", instance) write_model_json(paths["formula"] / "parameter_search_report.json", report) write_model_json(paths["kinematics"] / "kinematic_solution.json", solution) write_model_json(paths["kinematics"] / "motion_samples.json", samples) write_model_json(paths["placement"] / "placement_plan.json", placement_plan) if instance.industrial_parameters is not None: write_model_json(paths["formula"] / "industrial_parameters.json", instance.industrial_parameters) if instance.assembly_topology is not None: write_model_json(paths["formula"] / "assembly_topology.json", instance.assembly_topology) generate_simplecad(instance, paths["cad"]) if (paths["cad"] / "placement_plan.json").exists(): shutil.copy2(paths["cad"] / "placement_plan.json", paths["placement"] / "placement_plan.json") validate_from_files( instance=instance, model_path=paths["cad"] / "reducer.model.json", manifest_path=paths["cad"] / "assembly_manifest.json", build_meta_path=paths["cad"] / "build_meta.json", snapshot_out=paths["validation"] / "assembly_snapshot.json", report_out=paths["validation"] / "validation_report.json", ) result = _summarize_reducer_task(task_id, run_dir, request) return update_task(task_id, { **result, "requirementOverrides": overrides, "activeReducerRequirementPath": str(requirement_path.resolve()), }) def generate_joint_task(task_id: str, payload: dict[str, object]) -> dict[str, object]: request = str(payload.get("request") or "Generate planetary joint module") run_dir = task_dir(task_id) joint_dir = run_dir / "joint" mode = str(payload.get("mode") or "auto") overrides = _normalized_requirement_overrides(payload) topology = str(overrides.get("topology_family") or "simple_2k_h") if topology not in JOINT_REQUIREMENT_TEMPLATES: raise ValueError(f"joint_module currently supports only {sorted(JOINT_REQUIREMENT_TEMPLATES)}") if mode == "requirement": requirement_path = Path(str(payload.get("requirementPath") or JOINT_REQUIREMENT_TEMPLATES[topology])) if not requirement_path.is_absolute(): requirement_path = BACKEND_ROOT / requirement_path active_joint_requirement, active_reducer_requirement = _materialize_joint_requirement( task_id, requirement_path, overrides, ) run_joint_module(requirement_path=active_joint_requirement, out_dir=joint_dir) else: platform_profile = Path(str(payload.get("platformProfile") or DEFAULT_PLATFORM_PROFILE)) if not platform_profile.is_absolute(): platform_profile = BACKEND_ROOT / platform_profile legacy_motor_step = payload.get("motorStep") legacy_housing_source = payload.get("housingPython") run_auto_joint_design( out_dir=run_dir, platform_profile=platform_profile, motor_step=Path(str(legacy_motor_step)) if legacy_motor_step else None, housing_source=Path(str(legacy_housing_source)) if legacy_housing_source else None, tooth_form=str(payload.get("toothForm") or "spur"), radial_clearance_mm=float(payload.get("radialClearanceMm") or 14.0), reducer_axial_allowance_mm=float(payload.get("reducerAxialAllowanceMm") or 42.0), reducer_radial_clearance_mm=float(payload.get("reducerRadialClearanceMm") or 8.0), reducer_requirement_overrides=overrides, ) reducer_dir = joint_dir / "reducer" export_urdf( joint_manifest_path=joint_dir / "joint_module_manifest.json", reducer_instance_path=reducer_dir / "formula" / "formula_instance.json", out_dir=run_dir / "exports" / "urdf", ) result = _summarize_joint_task(task_id, run_dir, request) active_reducer_requirement = ( run_dir / "generated_reducer_requirement.json" if mode != "requirement" else active_reducer_requirement ) return update_task(task_id, { **result, "requirementOverrides": overrides, "activeReducerRequirementPath": str(active_reducer_requirement.resolve()), }) class ToolchainHandler(BaseHTTPRequestHandler): run_name = DEFAULT_RUN_NAME def do_OPTIONS(self) -> None: # noqa: N802 self.send_response(204) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() def do_GET(self) -> None: # noqa: N802 parsed = urlparse(self.path) try: if parsed.path == "/api/project-summary": _send_json(self, 200, _project_summary(self.run_name)) return if parsed.path == "/api/tasks": _send_json(self, 200, {"tasks": list_tasks()}) return if parsed.path == "/api/designs": _send_json(self, 200, {"designs": _design_catalog()}) return if parsed.path.startswith("/api/design-assets/"): parts = parsed.path.split("/") if len(parts) < 5: _send_json(self, 400, {"error": "invalid_design_asset_path"}) return run_name = safe_task_id(parts[3]) relative_path = Path(unquote("/".join(parts[4:]))) if relative_path.is_absolute() or ".." in relative_path.parts: _send_json(self, 400, {"error": "invalid_design_asset_path"}) return _send_file(self, RUN_ROOT / run_name / "exports" / "urdf" / relative_path) return if parsed.path.startswith("/api/tasks/"): parts = parsed.path.split("/") if len(parts) >= 4: task_id = safe_task_id(parts[3]) if len(parts) == 4: manifest = read_task_manifest(task_id) _send_json(self, 200 if manifest else 404, manifest or {"error": "task_not_found"}) return if len(parts) == 5 and parts[4] == "parameters": manifest = read_task_manifest(task_id) _send_json(self, 200 if manifest else 404, { "taskId": task_id, "parameters": (manifest or {}).get("parameters", {}), }) return if len(parts) >= 6 and parts[4] == "artifacts": relative = "/".join(parts[5:]) _send_file(self, _safe_relative_file(task_id, relative)) return if parsed.path.startswith("/project-assets/urdf/"): relative_path = Path(unquote(parsed.path[len("/project-assets/urdf/"):])) if relative_path.is_absolute() or ".." in relative_path.parts: _send_json(self, 400, {"error": "invalid_asset_path"}) return _send_file(self, RUN_ROOT / self.run_name / "exports" / "urdf" / relative_path) return _send_json(self, 404, {"error": "not_found"}) except Exception as exc: # noqa: BLE001 _send_json(self, 500, {"error": str(exc)}) def do_POST(self) -> None: # noqa: N802 parsed = urlparse(self.path) try: if parsed.path == "/api/tasks": payload = _read_json(self) task = ensure_task(str(payload.get("taskId") or "") or None, request=str(payload.get("request") or "CAD Agent Studio task")) _send_json(self, 200, task) return if parsed.path == "/api/generate": payload = _read_json(self) kind = str(payload.get("kind") or "joint_module") task = ensure_task(str(payload.get("taskId") or "") or None, request=str(payload.get("request") or "CAD generation")) task_id = task["taskId"] update_task(task_id, { "status": "running", "kind": kind, "request": payload.get("request") or task["manifest"].get("request"), "planner": payload.get("planner") if isinstance(payload.get("planner"), dict) else {}, }) try: result = generate_reducer_task(task_id, payload) if kind == "reducer" else generate_joint_task(task_id, payload) _send_json(self, 200, result) except Exception as exc: # noqa: BLE001 error_manifest = update_task(task_id, { "status": "error", "error": { "type": type(exc).__name__, "message": str(exc), "traceback": traceback.format_exc(), }, }) _send_json(self, 500, error_manifest) return if parsed.path.startswith("/api/tasks/") and parsed.path.endswith("/reconstruct"): task_id = safe_task_id(parsed.path.split("/")[3]) payload = _read_json(self) result = reconstruct_uploaded_task( task_id, request=str(payload.get("request") or "重建上传的 STEP"), ) _send_json(self, 200, result) return if parsed.path.startswith("/api/tasks/") and parsed.path.endswith("/parameters"): task_id = safe_task_id(parsed.path.split("/")[3]) payload = _read_json(self) result = edit_task_parameter( task_id, str(payload.get("parameter") or ""), payload.get("value"), ) _send_json(self, 200, result) return if parsed.path.startswith("/api/tasks/") and "/exports/" in parsed.path: parts = parsed.path.split("/") task_id = safe_task_id(parts[3]) format_name = str(parts[5] if len(parts) > 5 else "").lower() result = export_task_robot(task_id, format_name) _send_json(self, 200, result) return if parsed.path.startswith("/api/upload/"): task_id = safe_task_id(parsed.path.rsplit("/", 1)[-1]) filename = unquote(self.headers.get("X-Filename") or "upload.bin") mime = self.headers.get("Content-Type") or "application/octet-stream" attachment = write_upload(task_id, filename, _read_body(self), mime) _send_json(self, 200, attachment) return _send_json(self, 404, {"error": "not_found"}) except Exception as exc: # noqa: BLE001 _send_json(self, 500, {"error": str(exc)}) def create_toolchain_app(host: str, port: int, *, run_name: str = DEFAULT_RUN_NAME) -> ThreadingHTTPServer: ToolchainHandler.run_name = run_name return ThreadingHTTPServer((host, port), ToolchainHandler)