feat: add knowledge-driven joint design studio
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user