feat: 推进SCDM-first后端接入和大模型编辑优化
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from step_editor.scdm_backend import ScdmBackendInfo # noqa: E402
|
||||
from step_editor.scdm_edit_runner import generate_scdm_edit_script, prepare_scdm_edit_job, run_scdm_edit_job # noqa: E402
|
||||
from step_editor.scdm_schema import read_json, write_json # noqa: E402
|
||||
|
||||
|
||||
def _assert(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def _fake_spaceclaim(path: Path) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("fake", encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def _script_path_from_command(command: list[str] | tuple[str, ...]) -> Path:
|
||||
for item in command:
|
||||
if item.startswith("/RunScript="):
|
||||
return Path(item.split("=", 1)[1])
|
||||
raise AssertionError(f"missing /RunScript argument: {command}")
|
||||
|
||||
|
||||
def _job_path_from_script(script_path: Path) -> Path:
|
||||
text = script_path.read_text(encoding="utf-8")
|
||||
match = re.search(r"^JOB_PATH = (.+)$", text, flags=re.MULTILINE)
|
||||
_assert(match is not None, f"generated script should embed JOB_PATH: {script_path}")
|
||||
return Path(ast.literal_eval(match.group(1)))
|
||||
|
||||
|
||||
def _successful_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
||||
script_path = _script_path_from_command(command)
|
||||
job_path = _job_path_from_script(script_path)
|
||||
job = read_json(job_path)
|
||||
outputs = job.get("outputs")
|
||||
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
||||
output_step = Path(str(outputs["outputStep"]))
|
||||
output_step.write_text("ISO-10303-21;\n/* fake SCDM output */\nEND-ISO-10303-21;\n", encoding="utf-8")
|
||||
write_json(
|
||||
outputs["result"],
|
||||
{
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"message": "fake edit finished",
|
||||
"outputStep": str(output_step),
|
||||
"backendOperation": job.get("target", {}).get("backendOperation"),
|
||||
},
|
||||
)
|
||||
return subprocess.CompletedProcess(command, 0, stdout="ok", stderr="")
|
||||
|
||||
|
||||
def _failure_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
||||
script_path = _script_path_from_command(command)
|
||||
job_path = _job_path_from_script(script_path)
|
||||
job = read_json(job_path)
|
||||
outputs = job.get("outputs")
|
||||
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
||||
write_json(
|
||||
outputs["error"],
|
||||
{
|
||||
"ok": False,
|
||||
"reason": "fake-failed",
|
||||
"message": "fake SCDM command failed",
|
||||
"backendOperation": job.get("target", {}).get("backendOperation"),
|
||||
},
|
||||
)
|
||||
return subprocess.CompletedProcess(command, 7, stdout="", stderr="fake failure")
|
||||
|
||||
|
||||
def _missing_output_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
||||
script_path = _script_path_from_command(command)
|
||||
job_path = _job_path_from_script(script_path)
|
||||
job = read_json(job_path)
|
||||
outputs = job.get("outputs")
|
||||
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
||||
write_json(
|
||||
outputs["result"],
|
||||
{
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"message": "fake success without STEP",
|
||||
"outputStep": str(outputs["outputStep"]),
|
||||
},
|
||||
)
|
||||
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
|
||||
|
||||
|
||||
def _empty_output_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
||||
script_path = _script_path_from_command(command)
|
||||
job_path = _job_path_from_script(script_path)
|
||||
job = read_json(job_path)
|
||||
outputs = job.get("outputs")
|
||||
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
||||
Path(str(outputs["outputStep"])).write_text("", encoding="utf-8")
|
||||
write_json(
|
||||
outputs["result"],
|
||||
{
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"message": "fake success with empty STEP",
|
||||
"outputStep": str(outputs["outputStep"]),
|
||||
},
|
||||
)
|
||||
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with tempfile.TemporaryDirectory(prefix="step_editor_scdm_edit_") as temp:
|
||||
root = Path(temp)
|
||||
step_path = root / "sample.step"
|
||||
step_path.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
||||
backend = ScdmBackendInfo(path=_fake_spaceclaim(root / "SpaceClaim.exe"), source="test", version="v222")
|
||||
signature = {
|
||||
"objectType": "hole",
|
||||
"faceIds": [85, 94],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 12,
|
||||
"faceOrdinals": [12, 19],
|
||||
"scdmFaceLocators": [
|
||||
{"bodyIndex": 0, "faceOrdinal": 12, "globalFaceOrdinal": 85},
|
||||
{"bodyIndex": 0, "faceOrdinal": 19, "globalFaceOrdinal": 96},
|
||||
],
|
||||
"center": [0.5, 1.0, 9.5],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
}
|
||||
|
||||
prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "prepared",
|
||||
backend=backend,
|
||||
capability_key="hole.diameter",
|
||||
target_value="0.75",
|
||||
object_id="hole:85-94",
|
||||
object_signature=signature,
|
||||
timeout_seconds=45.0,
|
||||
)
|
||||
_assert(prepared.get("ok") is True, f"edit job should be prepared: {prepared}")
|
||||
job_path = Path(str(prepared["job_path"]))
|
||||
script_path = Path(str(prepared["script_path"]))
|
||||
_assert(job_path.is_file(), "scdm_edit_job.json should be written")
|
||||
_assert(script_path.is_file(), "scdm_edit.py should be written")
|
||||
job = read_json(job_path)
|
||||
_assert(job.get("adapter") == "spaceclaim-v1", f"bad adapter: {job}")
|
||||
_assert(job.get("schemaVersion") == 1, f"bad schema version: {job}")
|
||||
_assert(job.get("model", {}).get("sourceStep") == str(step_path.resolve(strict=False)), f"bad source path: {job}")
|
||||
_assert(job.get("model", {}).get("rollbackStep") == str(step_path.resolve(strict=False)), f"bad rollback path: {job}")
|
||||
_assert(job.get("target", {}).get("capabilityKey") == "hole.diameter", f"bad capability: {job}")
|
||||
_assert(job.get("target", {}).get("backendOperation") == "change_hole_diameter", f"bad operation: {job}")
|
||||
_assert(job.get("target", {}).get("value") == 0.75, f"target should be numeric: {job}")
|
||||
_assert(job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [85, 94], f"bad signature: {job}")
|
||||
_assert(job.get("execution", {}).get("isolatedProcess") is True, f"job should record isolated execution: {job}")
|
||||
script = script_path.read_text(encoding="utf-8")
|
||||
for token in (
|
||||
"change_hole_diameter",
|
||||
"move_hole_axis",
|
||||
"move_slot",
|
||||
"move_boss",
|
||||
"pull_face_offset",
|
||||
"fill_feature",
|
||||
"StandardHoles.ModifyHoleRadius",
|
||||
"OffsetFaces.Execute",
|
||||
"Move.Translate",
|
||||
"MoveOptions",
|
||||
"OffsetFaceOptions",
|
||||
"FillOptions",
|
||||
"FillMode",
|
||||
"Delete.Execute",
|
||||
"DocumentSave",
|
||||
"scdmFaceLocators",
|
||||
"result.json",
|
||||
"error.json",
|
||||
):
|
||||
_assert(token in script, f"generated edit script missing {token}")
|
||||
_assert("Pull.Execute" not in script, "generated edit script should use documented OffsetFaces instead of Pull.Execute")
|
||||
generated = generate_scdm_edit_script(job_path)
|
||||
_assert("JOB_PATH =" in generated and job_path.name in generated, "generated edit script should embed the job path")
|
||||
|
||||
slot_signature = {
|
||||
"objectType": "slot",
|
||||
"faceIds": [30, 31, 32],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 30,
|
||||
"faceOrdinals": [30, 31, 32],
|
||||
"globalFaceOrdinal": 40,
|
||||
"globalFaceOrdinals": [40, 41, 42],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
}
|
||||
slot_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-position-prepared",
|
||||
backend=backend,
|
||||
capability_key="slot.position",
|
||||
target_value=[1.0, 2.0, 5.0],
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
)
|
||||
_assert(slot_prepared.get("ok") is True, f"slot.position should be productized and prepare an edit job: {slot_prepared}")
|
||||
slot_job = read_json(slot_prepared["job_path"])
|
||||
_assert(slot_job.get("target", {}).get("backendOperation") == "move_slot", f"slot.position should route to move_slot: {slot_job}")
|
||||
_assert(slot_job.get("target", {}).get("value") == [1.0, 2.0, 5.0], f"slot.position target should be vector3: {slot_job}")
|
||||
_assert(slot_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [30, 31, 32], f"slot faces should be preserved: {slot_job}")
|
||||
|
||||
boss_signature = {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [50, 51, 52],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 50,
|
||||
"faceOrdinals": [50, 51, 52],
|
||||
"globalFaceOrdinal": 70,
|
||||
"globalFaceOrdinals": [70, 71, 72],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
}
|
||||
boss_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-position-prepared",
|
||||
backend=backend,
|
||||
capability_key="boss.position",
|
||||
target_value=[2.0, 0.0, 2.0],
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
)
|
||||
_assert(boss_prepared.get("ok") is True, f"boss.position should be productized and prepare an edit job: {boss_prepared}")
|
||||
boss_job = read_json(boss_prepared["job_path"])
|
||||
_assert(boss_job.get("target", {}).get("backendOperation") == "move_boss", f"boss.position should route to move_boss: {boss_job}")
|
||||
_assert(boss_job.get("target", {}).get("value") == [2.0, 0.0, 2.0], f"boss.position target should be vector3: {boss_job}")
|
||||
_assert(boss_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [50, 51, 52], f"boss faces should be preserved: {boss_job}")
|
||||
|
||||
planned = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "unsupported",
|
||||
backend=backend,
|
||||
capability_key="slot.width",
|
||||
target_value="1",
|
||||
object_signature=signature,
|
||||
)
|
||||
_assert(
|
||||
planned.get("ok") is False and planned.get("reason") == "capability-not-productized",
|
||||
f"planned capability should fail early with a roadmap reason: {planned}",
|
||||
)
|
||||
|
||||
unsupported = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "unsupported-unknown",
|
||||
backend=backend,
|
||||
capability_key="not.real",
|
||||
target_value="1",
|
||||
object_signature=signature,
|
||||
)
|
||||
_assert(unsupported.get("ok") is False and unsupported.get("reason") == "unsupported-capability", f"unknown capability should fail early: {unsupported}")
|
||||
|
||||
success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "success",
|
||||
backend=backend,
|
||||
capability_key="hole.diameter",
|
||||
target_value=0.9,
|
||||
object_id="hole:85-94",
|
||||
object_signature=signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(success.get("ok") is True, f"fake edit should succeed: {success}")
|
||||
_assert(Path(str(success["output_step"])).is_file(), f"output STEP should exist: {success}")
|
||||
success_backend = success.get("backend")
|
||||
_assert(isinstance(success_backend, dict), f"successful edit should carry backend status: {success}")
|
||||
_assert(success_backend.get("runScriptOk") is True, f"successful edit should mark /RunScript usable: {success_backend}")
|
||||
_assert(success_backend.get("licenseOk") is True, f"successful edit should mark license usable: {success_backend}")
|
||||
|
||||
failed = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "failed",
|
||||
backend=backend,
|
||||
capability_key="hole.position",
|
||||
target_value=[0.5, 1.0, 6.0],
|
||||
object_id="hole:85-94",
|
||||
object_signature=signature,
|
||||
runner=_failure_runner,
|
||||
)
|
||||
_assert(failed.get("ok") is False and failed.get("reason") == "fake-failed", f"error.json should drive failure reason: {failed}")
|
||||
|
||||
slot_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-success",
|
||||
backend=backend,
|
||||
capability_key="slot.position",
|
||||
target_value=[1.0, 2.0, 6.0],
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(slot_success.get("ok") is True, f"fake slot.position edit should succeed: {slot_success}")
|
||||
_assert(slot_success.get("backend_operation") == "move_slot", f"slot.position should report move_slot: {slot_success}")
|
||||
|
||||
boss_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-success",
|
||||
backend=backend,
|
||||
capability_key="boss.position",
|
||||
target_value=[3.0, 0.0, 2.0],
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(boss_success.get("ok") is True, f"fake boss.position edit should succeed: {boss_success}")
|
||||
_assert(boss_success.get("backend_operation") == "move_boss", f"boss.position should report move_boss: {boss_success}")
|
||||
|
||||
missing_output = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "missing-output",
|
||||
backend=backend,
|
||||
capability_key="face.offset",
|
||||
target_value=5,
|
||||
object_id="face:9",
|
||||
object_signature={"objectType": "face", "faceIds": [9], "bodyIndex": 0, "faceOrdinal": 3},
|
||||
runner=_missing_output_runner,
|
||||
)
|
||||
_assert(missing_output.get("ok") is False and missing_output.get("reason") == "missing-output-step", f"missing output STEP should be rejected: {missing_output}")
|
||||
|
||||
empty_output = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "empty-output",
|
||||
backend=backend,
|
||||
capability_key="face.offset",
|
||||
target_value=5,
|
||||
object_id="face:9",
|
||||
object_signature={"objectType": "face", "faceIds": [9], "bodyIndex": 0, "faceOrdinal": 3},
|
||||
runner=_empty_output_runner,
|
||||
)
|
||||
_assert(empty_output.get("ok") is False and empty_output.get("reason") == "empty-output-step", f"empty output STEP should be rejected: {empty_output}")
|
||||
|
||||
print("scdm edit runner ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user