更新功能

This commit is contained in:
2026-08-26 14:13:11 +08:00
parent beaa5dd9fe
commit a508e6f07e
47 changed files with 5616 additions and 520 deletions
+168 -2
View File
@@ -15,6 +15,7 @@ from jsonschema.exceptions import SchemaError
from vendor.cdsl_preview_runtime import step_to_glb
from app.services.quality import evaluate_quality, validate_verification
from app.services.storage import WorkspaceStore, now_iso, write_json
from app.services.cdsl_fragment import selector_bindings
from app.settings import Settings
@@ -299,7 +300,140 @@ def parameter_contract(cdsl: dict[str, Any]) -> dict[str, Any]:
return {"schema_version": "1.0", "parameters": _derived_parameters(cdsl), "source": "derived"}
def topology_sidecars(engine_result: dict[str, Any], preview: dict[str, Any] | None = None) -> tuple[dict[str, Any], dict[str, Any]]:
def topology_snapshot(
engine_result: dict[str, Any],
*,
task_id: str = "",
revision_id: str = "",
preview: dict[str, Any] | None = None,
) -> dict[str, Any]:
raw_records = [
raw for raw in engine_result.get("topology_records") or ()
if isinstance(raw, dict) and raw.get("record_id") and raw.get("kind")
]
active_body_id = next(
(
str(result.get("body_id"))
for result in reversed(engine_result.get("feature_results") or ())
if isinstance(result, dict) and result.get("body_id")
),
"",
)
if not active_body_id:
active_body_id = next(
(
str(raw.get("body_id"))
for raw in reversed(raw_records)
if raw.get("kind") == "body" and raw.get("body_id")
),
"",
)
# The runtime retains historical B-rep records for provenance, but only
# the final body can resolve face, edge, vertex, and body selectors.
active_records = [
raw for raw in raw_records
if not active_body_id
or raw.get("kind") in {"plane", "axis"}
or str(raw.get("body_id") or "") == active_body_id
]
records: list[dict[str, Any]] = []
for raw in active_records:
kind = str(raw.get("kind"))
records.append({
"record_id": str(raw["record_id"]),
"kind": kind,
"feature_id": str(raw.get("feature_id") or ""),
"body_id": str(raw.get("body_id") or "") or None,
"owner_feature_ids": [str(item) for item in raw.get("owner_feature_ids") or () if str(item)],
"geometry": copy.deepcopy(raw.get("geometry") or {}),
"executable": kind in {"body", "face", "edge", "vertex", "plane", "axis"},
"synthetic": False,
})
# Preview/B-rep fallback faces are useful for visual explanation only.
# Keep them in the unified audit snapshot, but never expose them as
# executable selector candidates.
if not any(item.get("kind") == "face" for item in records):
for index, raw in enumerate((preview or {}).get("topology_faces") or ()):
if not isinstance(raw, dict):
continue
record_id = str(raw.get("id") or f"synthetic:face:{index}")
center = raw.get("center")
normal = raw.get("normal")
raw_bbox = raw.get("bbox")
if isinstance(raw_bbox, dict) and isinstance(raw_bbox.get("min"), list) and isinstance(raw_bbox.get("max"), list):
raw_bbox = [*raw_bbox["min"], *raw_bbox["max"]]
geometry = {
"surface_type": str(raw.get("surface_type") or "unknown"),
"center_mm": copy.deepcopy(center) if isinstance(center, list) else None,
"normal": copy.deepcopy(normal) if isinstance(normal, list) else None,
"bbox_mm": copy.deepcopy(raw_bbox or {}),
}
records.append({
"record_id": record_id,
"kind": "face",
"feature_id": "",
"body_id": None,
"owner_feature_ids": [],
"geometry": geometry,
"executable": False,
"synthetic": True,
})
return {
"schema_version": "cad.topology.v1",
"task_id": task_id,
"revision_id": revision_id,
"snapshot_id": f"{task_id}/{revision_id}" if task_id and revision_id else "",
"body_id": active_body_id,
"records": records,
}
def topology_sidecars(
engine_result: dict[str, Any],
preview: dict[str, Any] | None = None,
*,
snapshot: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], dict[str, Any]]:
runtime_records = (snapshot or topology_snapshot(engine_result)).get("records") or []
runtime_faces = [item for item in runtime_records if item.get("kind") == "face" and item.get("executable", True)]
runtime_edges = [item for item in runtime_records if item.get("kind") == "edge" and item.get("executable", True)]
if runtime_faces or runtime_edges:
references = []
for record in runtime_faces:
geometry = record.get("geometry") or {}
references.append({
"id": str(record.get("record_id")),
"selectorType": "face",
"label": str(geometry.get("surface_type") or "face"),
"center": geometry.get("center_mm"),
"normal": geometry.get("normal"),
"frame": {
"origin_mm": geometry.get("center_mm"),
"normal": geometry.get("normal"),
"x_dir": [1, 0, 0],
"y_dir": [0, 1, 0],
},
"bbox": geometry.get("bbox_mm") or {},
"surface_type": str(geometry.get("surface_type") or "unknown"),
"owner_feature_ids": record.get("owner_feature_ids") or [],
"source": "runtime_snapshot",
"snapshot_id": (snapshot or {}).get("snapshot_id") or "",
"executable": True,
})
edge_records = [
{
**record,
"selectorType": "edge",
"source": "runtime_snapshot",
"snapshot_id": (snapshot or {}).get("snapshot_id") or "",
}
for record in runtime_edges
]
return (
{"schema_version": "cad.topology.v1", "references": references, "edges": edge_records},
{"schema_version": "cad.topology.v1", "edges": edge_records},
)
topology_faces = (preview or {}).get("topology_faces")
if isinstance(topology_faces, list) and topology_faces:
references = []
@@ -322,6 +456,9 @@ def topology_sidecars(engine_result: dict[str, Any], preview: dict[str, Any] | N
"surface_type": str(face.get("surface_type") or "unknown"),
"triangle_start": int(face.get("triangle_start") or 0),
"triangle_count": int(face.get("triangle_count") or 0),
"source": "preview",
"synthetic": True,
"executable": False,
})
if references:
return ({"schema_version": "1.1", "references": references}, {"schema_version": "1.0", "edges": []})
@@ -346,6 +483,9 @@ def topology_sidecars(engine_result: dict[str, Any], preview: dict[str, Any] | N
"center": point, "normal": normal,
"frame": {"origin_mm": point, "normal": normal, "x_dir": x_dir, "y_dir": y_dir},
"bbox": {"min": minimum, "max": maximum},
"source": "bbox_fallback",
"synthetic": True,
"executable": False,
}
for name, point, normal, x_dir, y_dir in definitions
]
@@ -463,6 +603,11 @@ def build_revision(
repair_attempts: int = 0,
verification: dict[str, Any] | None = None,
reference_records: list[dict[str, Any]] | None = None,
feature_plan: dict[str, Any] | None = None,
node_id: str = "",
fragment: dict[str, Any] | None = None,
branch_id: str = "main",
visibility: str = "final",
) -> dict[str, Any]:
engine = load_engine(settings)
task = store.ensure_task(task_id, request)
@@ -476,12 +621,17 @@ def build_revision(
parameters_path = revision_dir / "parameters.json"
selector_path = revision_dir / "model.selector.json"
edges_path = revision_dir / "model.edges.json"
topology_path = revision_dir / "model.topology.json"
part_skills_path = revision_dir / "part-skills.json"
quality_path = revision_dir / "quality-report.json"
snapshot_manifest_path = revision_dir / "snapshot-manifest.json"
fragment_path = revision_dir / "fragment.json"
selector_bindings_path = revision_dir / "selector-bindings.json"
part_skill_audit = _part_skill_audit(part_skills, request, generation_assumptions)
generation_context = _generation_context(reference_ids, part_skill_audit)
write_json(request_path, {"request": request, "created_at": now_iso()})
if isinstance(feature_plan, dict):
store.write_feature_plan(task["task_id"], feature_plan)
write_json(references_path, {"reference_ids": reference_ids, "records": [item for item in reference_records or [] if isinstance(item, dict)]})
write_json(part_skills_path, part_skill_audit)
write_json(snapshot_manifest_path, {
@@ -499,6 +649,7 @@ def build_revision(
"cdsl_path": cdsl_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"report_path": report_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"parameters_path": parameters_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"topology_path": topology_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"part_skills_path": part_skills_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"quality_path": quality_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"snapshot_manifest_path": snapshot_manifest_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
@@ -512,6 +663,11 @@ def build_revision(
"operation": operation or {},
"input_attachments": input_attachments or [],
"repair_attempts": max(0, int(repair_attempts)),
"branch_id": branch_id or "main",
"visibility": visibility if visibility in {"checkpoint", "final", "superseded"} else "checkpoint",
"node_id": node_id,
"fragment_path": fragment_path.relative_to(store.task_dir(task["task_id"])).as_posix() if fragment else "",
"selector_bindings_path": selector_bindings_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
}
if status == "success":
record.update({
@@ -519,6 +675,7 @@ def build_revision(
"glb_path": glb_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"selector_path": selector_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"edges_path": edges_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"topology_path": topology_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"engine": engine_name,
})
else:
@@ -538,6 +695,8 @@ def build_revision(
if not isinstance(meta.get("editable_parameters"), list) or not meta["editable_parameters"]:
meta["editable_parameters"] = _derived_parameters(cdsl_copy)
write_json(cdsl_path, cdsl_copy)
if fragment is not None:
write_json(fragment_path, fragment)
write_json(parameters_path, parameter_contract(cdsl_copy))
validate_cdsl(cdsl_copy, engine)
# Product revisions are semantic CDSL artifacts. Do not route them
@@ -547,7 +706,14 @@ def build_revision(
if engine_result.get("engine") != "cdsl_only" or not step_path.is_file() or step_path.stat().st_size == 0:
raise RuntimeError("Engine did not produce a CDSL-only STEP artifact")
preview = step_to_glb(step_path, glb_path)
selector, edges = topology_sidecars(engine_result, preview)
snapshot = topology_snapshot(engine_result, task_id=task["task_id"], revision_id=revision_id, preview=preview)
write_json(topology_path, snapshot)
write_json(selector_bindings_path, selector_bindings(
engine_result,
node_id=node_id,
snapshot_id=str(snapshot.get("snapshot_id") or ""),
))
selector, edges = topology_sidecars(engine_result, preview, snapshot=snapshot)
write_json(selector_path, selector)
write_json(edges_path, edges)
rules = validate_verification(verification, cdsl_copy)