feat: add knowledge-driven joint design studio

This commit is contained in:
Jerry
2026-08-14 13:53:51 +08:00
parent 7d12459a32
commit 281afdfd9e
57 changed files with 269659 additions and 1152 deletions
+144 -1
View File
@@ -337,6 +337,129 @@ def _project_summary(run_name: str) -> dict[str, object]:
}
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,
@@ -811,6 +934,21 @@ class ToolchainHandler(BaseHTTPRequestHandler):
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:
@@ -854,7 +992,12 @@ class ToolchainHandler(BaseHTTPRequestHandler):
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")})
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)