| @@ -0,0 +1,491 @@ | |||
| { | |||
| "mounted": true, | |||
| "hasEnvironment": true, | |||
| "hasEnvironmentTarget": true, | |||
| "toneMappingExposure": 1.05, | |||
| "background": "a8cbe1", | |||
| "meshCount": 45, | |||
| "materials": [ | |||
| { | |||
| "name": "M_3dfreehub_mat_2", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "M_3dfreehub_mat_3", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "M_3dfreehub_mat_4", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "M_3dfreehub_mat_5", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Material__85", | |||
| "color": "695c5b", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "BaiSe", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Cai1", | |||
| "color": "3d4b3b", | |||
| "metalness": 0.6, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "HeiSe", | |||
| "color": "4f4f4f", | |||
| "metalness": 0.3, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "JinShu", | |||
| "color": "929292", | |||
| "metalness": 0.6, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "LanSe", | |||
| "color": "3b76d0", | |||
| "metalness": 0.6, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "LvSe", | |||
| "color": "3d4b3b", | |||
| "metalness": 0.6, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Mat_black", | |||
| "color": "322e2e", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Mat_supportBridgeBridge_01", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Mat_supportBridgeBridge_02", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Mat_supportBridge_05", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Mat_white", | |||
| "color": "9da3a4", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Material__1676", | |||
| "color": "d96e6e", | |||
| "metalness": 0.6, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "MoShaJinShu", | |||
| "color": "cbcbcb", | |||
| "metalness": 0.8, | |||
| "roughness": 0.35, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "__", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "adskMatMat_supportBridge_05", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "adskMatMat_white", | |||
| "color": "9da3a4", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "(未命名)", | |||
| "color": "ffffff", | |||
| "metalness": null, | |||
| "roughness": null, | |||
| "envMapIntensity": null, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Material__579", | |||
| "color": "404040", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "(未命名)", | |||
| "color": "ffffff", | |||
| "metalness": null, | |||
| "roughness": null, | |||
| "envMapIntensity": null, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "Material__1673", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": true, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "QianlvSe2", | |||
| "color": "749f62", | |||
| "metalness": 0, | |||
| "roughness": 0.6, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "WaiQiang", | |||
| "color": "ffffff", | |||
| "metalness": 0, | |||
| "roughness": 0.5, | |||
| "envMapIntensity": 1, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "(未命名)", | |||
| "color": "ffffff", | |||
| "metalness": null, | |||
| "roughness": null, | |||
| "envMapIntensity": null, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| }, | |||
| { | |||
| "name": "(未命名)", | |||
| "color": "ffffff", | |||
| "metalness": null, | |||
| "roughness": null, | |||
| "envMapIntensity": null, | |||
| "hasMap": false, | |||
| "preset": null, | |||
| "rule": null, | |||
| "authored": null | |||
| } | |||
| ], | |||
| "server": { | |||
| "stepId": "front-pump-step-01", | |||
| "completed": [], | |||
| "registeredTargets": [ | |||
| "scene:front-pump-rescue-vehicle", | |||
| "front-pump-hydraulic-pump", | |||
| "front-pump-power-take-off", | |||
| "front-pump-pto-switch", | |||
| "front-pump-ball-valve-switch", | |||
| "front-pump-aux-control-box", | |||
| "front-pump-aux-control-box-cover", | |||
| "front-pump-rescue-switch" | |||
| ], | |||
| "readOnly": false, | |||
| "animations": [ | |||
| { | |||
| "modelId": "front-pump-field-terrain", | |||
| "versionId": "", | |||
| "bindings": 0, | |||
| "unresolved": 0, | |||
| "flowVisible": false, | |||
| "native": [] | |||
| }, | |||
| { | |||
| "modelId": "front-pump-step-vehicle", | |||
| "versionId": "", | |||
| "bindings": 0, | |||
| "unresolved": 0, | |||
| "flowVisible": false, | |||
| "native": [] | |||
| } | |||
| ] | |||
| }, | |||
| "bridgeWorkshop": null, | |||
| "bridgeGrounding": [ | |||
| { | |||
| "adjustment": 1.418086860486326, | |||
| "contacts": 22, | |||
| "minGap": 0, | |||
| "maxGap": 1.654042585678425, | |||
| "position": [ | |||
| 0, | |||
| 1.418086860486326, | |||
| 0 | |||
| ] | |||
| } | |||
| ], | |||
| "publishedContentMounted": true, | |||
| "publishedContentName": "架桥车前泵故障互救处置训练场景", | |||
| "publishedTrainingContent": { | |||
| "sceneId": "SCENE-FRONT-PUMP-RESCUE-01", | |||
| "objectCount": 2 | |||
| }, | |||
| "proceduralMachineVisible": false, | |||
| "publishedModelRootCount": 2, | |||
| "framing": { | |||
| "cameraPosition": [ | |||
| 10.6, | |||
| 6.4, | |||
| 13.2 | |||
| ], | |||
| "controlsTarget": [ | |||
| 0, | |||
| 1.9, | |||
| 0 | |||
| ], | |||
| "distance": 17.52, | |||
| "minDistance": 0.05, | |||
| "maxDistance": 800, | |||
| "fov": 45, | |||
| "near": 0.1, | |||
| "far": 800, | |||
| "fogNear": 70, | |||
| "fogFar": 240, | |||
| "modelSize": [ | |||
| 140, | |||
| 28.34, | |||
| 116.19 | |||
| ], | |||
| "modelCenter": [ | |||
| -8.4, | |||
| 7.33, | |||
| -34.86 | |||
| ], | |||
| "stepCameraPolicy": "exterior", | |||
| "explosionItemCount": 36, | |||
| "structureNodeCount": 44, | |||
| "enablePan": true, | |||
| "outlineActive": false, | |||
| "labelsVisible": false, | |||
| "hideOthers": false, | |||
| "hiddenMeshCount": 0, | |||
| "keptTargets": [], | |||
| "targetOnScreen": { | |||
| "targetId": "front-pump-hydraulic-pump", | |||
| "localX": 647, | |||
| "localY": 392, | |||
| "insideCanvas": true, | |||
| "visible": true, | |||
| "coveredBy": [] | |||
| }, | |||
| "keptOpacity": [], | |||
| "environmentAlignment": { | |||
| "pickedAt": [ | |||
| 8.4, | |||
| -10.31, | |||
| 34.86 | |||
| ], | |||
| "localRelief": 0.265, | |||
| "samples": 115 | |||
| }, | |||
| "sceneObjects": [ | |||
| { | |||
| "name": "野外山地训练场", | |||
| "min": [ | |||
| -78.4, | |||
| -6.84, | |||
| -92.96 | |||
| ], | |||
| "max": [ | |||
| 61.6, | |||
| 21.5, | |||
| 23.24 | |||
| ] | |||
| }, | |||
| { | |||
| "name": "架桥车前泵互救操作整车模型", | |||
| "min": [ | |||
| -0.92, | |||
| 1.42, | |||
| -4 | |||
| ], | |||
| "max": [ | |||
| 0.92, | |||
| 3.39, | |||
| 4 | |||
| ] | |||
| } | |||
| ], | |||
| "labelSamples": [ | |||
| { | |||
| "targetId": "scene:front-pump-rescue-vehicle", | |||
| "targetName": "", | |||
| "partName": "架桥车前泵互救操作整车模型" | |||
| }, | |||
| { | |||
| "targetId": "front-pump-hydraulic-pump", | |||
| "targetName": "", | |||
| "partName": "液压泵" | |||
| }, | |||
| { | |||
| "targetId": "front-pump-power-take-off", | |||
| "targetName": "", | |||
| "partName": "取力器" | |||
| }, | |||
| { | |||
| "targetId": "front-pump-pto-switch", | |||
| "targetName": "", | |||
| "partName": "取力器开关" | |||
| } | |||
| ] | |||
| } | |||
| } | |||
| @@ -0,0 +1 @@ | |||
| <!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>桥梁车辆贴地修复</title><style>body{font:16px/1.7 system-ui;background:#edf4f1;color:#123e35;margin:32px auto;max-width:1200px}section{background:white;padding:24px;margin:20px 0}img{max-width:100%}a{color:#008468}</style><section><h1>桥梁车辆贴地修复 · 验证通过</h1><p>仅对四套桥梁脚本的训练位置平整地形,按轮胎与桌脚接触点贴地;原 GLB 文件和其他场景不变。验证复位、重新加载,以及 005/008 原任务切换步骤后车辆保持地面位置。</p><p>006/007 的三维版使用浏览器内隔离渲染验证,未变更旧任务入口或已有记录。列表仍为 4 条,没有新增测试任务。</p><p>前端构建与贴地/正式路由测试通过。<a href="result.json">原始验证结果</a></p></section><section><h2>TASK-VIRTUAL-005</h2><p>published-task-preview</p><img src="TASK-VIRTUAL-005.png" alt="TASK-VIRTUAL-005 车辆落地"></section><section><h2>TASK-VIRTUAL-006</h2><p>isolated-v3-render-no-database-change</p><img src="TASK-VIRTUAL-006.png" alt="TASK-VIRTUAL-006 车辆落地"></section><section><h2>TASK-VIRTUAL-007</h2><p>isolated-v3-render-no-database-change</p><img src="TASK-VIRTUAL-007.png" alt="TASK-VIRTUAL-007 车辆落地"></section><section><h2>TASK-VIRTUAL-008</h2><p>published-task-preview</p><img src="TASK-VIRTUAL-008.png" alt="TASK-VIRTUAL-008 车辆落地"></section></html> | |||
| @@ -0,0 +1,162 @@ | |||
| { | |||
| "pass": true, | |||
| "checks": [ | |||
| { | |||
| "code": "TASK-VIRTUAL-005", | |||
| "pass": true, | |||
| "mode": "published-task-preview", | |||
| "grounding": [ | |||
| { | |||
| "id": "front-pump-rescue-vehicle", | |||
| "adjustment": -2.6725065689598313e-9, | |||
| "contacts": 22, | |||
| "minGap": 0, | |||
| "maxGap": 0.01902815385345835, | |||
| "position": [ | |||
| 0, | |||
| -2.6725065689598313e-9, | |||
| 0 | |||
| ], | |||
| "quaternion": [ | |||
| 1.5432791624149142e-17, | |||
| 0, | |||
| -1.284056869900529e-16, | |||
| 1 | |||
| ] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "code": "TASK-VIRTUAL-006", | |||
| "pass": true, | |||
| "mode": "isolated-v3-render-no-database-change", | |||
| "grounding": [ | |||
| { | |||
| "id": "front-pump-rescue-vehicle", | |||
| "adjustment": -2.6725065689598313e-9, | |||
| "contacts": 22, | |||
| "minGap": 0, | |||
| "maxGap": 0.01902815385345835, | |||
| "position": [ | |||
| 0, | |||
| -2.6725065689598313e-9, | |||
| 0 | |||
| ], | |||
| "quaternion": [ | |||
| 1.5432791624149142e-17, | |||
| 0, | |||
| -1.284056869900529e-16, | |||
| 1 | |||
| ] | |||
| }, | |||
| { | |||
| "id": "bridge-workshop", | |||
| "adjustment": 0.007972344742127626, | |||
| "contacts": 16, | |||
| "minGap": 0, | |||
| "maxGap": 0.007419605491776459, | |||
| "position": [ | |||
| 0, | |||
| 0.007972344742127626, | |||
| 5.2 | |||
| ], | |||
| "quaternion": [ | |||
| 0.0006589154154300379, | |||
| 0, | |||
| -0.00027284082118916614, | |||
| 0.9999997456941484 | |||
| ] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "code": "TASK-VIRTUAL-007", | |||
| "pass": true, | |||
| "mode": "isolated-v3-render-no-database-change", | |||
| "grounding": [ | |||
| { | |||
| "id": "front-pump-rescue-vehicle", | |||
| "adjustment": -2.6725065689598313e-9, | |||
| "contacts": 22, | |||
| "minGap": 0, | |||
| "maxGap": 0.01902815385345835, | |||
| "position": [ | |||
| 0, | |||
| -2.6725065689598313e-9, | |||
| 0 | |||
| ], | |||
| "quaternion": [ | |||
| 1.5432791624149142e-17, | |||
| 0, | |||
| -1.284056869900529e-16, | |||
| 1 | |||
| ] | |||
| }, | |||
| { | |||
| "id": "bridge-workshop", | |||
| "adjustment": 0.007972344742127626, | |||
| "contacts": 16, | |||
| "minGap": 0, | |||
| "maxGap": 0.007419605491776459, | |||
| "position": [ | |||
| 0, | |||
| 0.007972344742127626, | |||
| 5.2 | |||
| ], | |||
| "quaternion": [ | |||
| 0.0006589154154300379, | |||
| 0, | |||
| -0.00027284082118916614, | |||
| 0.9999997456941484 | |||
| ] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "code": "TASK-VIRTUAL-008", | |||
| "pass": true, | |||
| "mode": "published-task-preview", | |||
| "grounding": [ | |||
| { | |||
| "id": "front-pump-rescue-vehicle", | |||
| "adjustment": -2.6725065689598313e-9, | |||
| "contacts": 22, | |||
| "minGap": 0, | |||
| "maxGap": 0.01902815385345835, | |||
| "position": [ | |||
| 0, | |||
| -2.6725065689598313e-9, | |||
| 0 | |||
| ], | |||
| "quaternion": [ | |||
| 1.5432791624149142e-17, | |||
| 0, | |||
| -1.284056869900529e-16, | |||
| 1 | |||
| ] | |||
| }, | |||
| { | |||
| "id": "bridge-workshop", | |||
| "adjustment": 0.007972344742127626, | |||
| "contacts": 16, | |||
| "minGap": 0, | |||
| "maxGap": 0.007419605491776459, | |||
| "position": [ | |||
| 0, | |||
| 0.007972344742127626, | |||
| 5.2 | |||
| ], | |||
| "quaternion": [ | |||
| 0.0006589154154300379, | |||
| 0, | |||
| -0.00027284082118916614, | |||
| 0.9999997456941484 | |||
| ] | |||
| } | |||
| ] | |||
| } | |||
| ], | |||
| "errors": [], | |||
| "remainingTasks": 4, | |||
| "databaseWrites": "preview validation only; no task/run creation or migration" | |||
| } | |||
| @@ -39,12 +39,12 @@ | |||
| "code": "TASK-VIRTUAL-008", | |||
| "id": "89", | |||
| "projectId": "441", | |||
| "versionId": "518", | |||
| "versionId": "526", | |||
| "name": "重型支援桥平衡阀拆拼互换", | |||
| "scriptId": "bridge-balance-valve-swap", | |||
| "scriptVersion": "2", | |||
| "scriptVersion": "3", | |||
| "steps": 16, | |||
| "previewUrl": "/teaching/virtual-training/preview/89", | |||
| "presentation": "procedure" | |||
| "presentation": "original-3d" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,9 @@ | |||
| { | |||
| "pass": true, | |||
| "checks": [ | |||
| "TASK-VIRTUAL-006: 7 steps, 11 3D operations; preview server validation passed", | |||
| "TASK-VIRTUAL-007: 11 steps, 27 3D operations; preview server validation passed", | |||
| "TASK-VIRTUAL-008: 16 steps, 50 3D operations; preview server validation passed" | |||
| ], | |||
| "errors": [] | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| { | |||
| "result": "PASS", | |||
| "softDeletedTasks": 7, | |||
| "keptTaskIds": [ | |||
| 86, | |||
| 87, | |||
| 88, | |||
| 89 | |||
| ], | |||
| "counts": { | |||
| "ut_teaching_assignment": 7, | |||
| "ut_teaching_command_receipt": 189, | |||
| "ut_teaching_event": 210, | |||
| "ut_teaching_member": 7, | |||
| "ut_teaching_run": 7, | |||
| "ut_teaching_run_member": 7, | |||
| "ut_teaching_step_run": 66 | |||
| }, | |||
| "backupSha256": "73d20cfdb2687d1980196184d5b367adf09b6fde16576127ee32bf49b1efbf8f", | |||
| "softDeletedProjects": [ | |||
| "446", | |||
| "447", | |||
| "448", | |||
| "449", | |||
| "450", | |||
| "451", | |||
| "452" | |||
| ], | |||
| "remaining": [ | |||
| { | |||
| "id": "89", | |||
| "code": "TASK-VIRTUAL-008", | |||
| "scriptVersion": "3" | |||
| }, | |||
| { | |||
| "id": "88", | |||
| "code": "TASK-VIRTUAL-007", | |||
| "scriptVersion": "2" | |||
| }, | |||
| { | |||
| "id": "87", | |||
| "code": "TASK-VIRTUAL-006", | |||
| "scriptVersion": "2" | |||
| }, | |||
| { | |||
| "id": "86", | |||
| "code": "TASK-VIRTUAL-005", | |||
| "scriptVersion": "2" | |||
| } | |||
| ] | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| [ | |||
| { | |||
| "code": "TASK-VIRTUAL-006", | |||
| "projectId": "446", | |||
| "taskId": "106" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,19 @@ | |||
| [ | |||
| { | |||
| "code": "TASK-VIRTUAL-006", | |||
| "projectId": "447", | |||
| "taskId": "107", | |||
| "passed": true | |||
| }, | |||
| { | |||
| "code": "TASK-VIRTUAL-007", | |||
| "projectId": "448", | |||
| "taskId": "108", | |||
| "passed": true | |||
| }, | |||
| { | |||
| "code": "TASK-VIRTUAL-008", | |||
| "projectId": "449", | |||
| "taskId": "109" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,20 @@ | |||
| [ | |||
| { | |||
| "code": "TASK-VIRTUAL-006", | |||
| "projectId": "450", | |||
| "taskId": "110", | |||
| "passed": true | |||
| }, | |||
| { | |||
| "code": "TASK-VIRTUAL-007", | |||
| "projectId": "451", | |||
| "taskId": "111", | |||
| "passed": true | |||
| }, | |||
| { | |||
| "code": "TASK-VIRTUAL-008", | |||
| "projectId": "452", | |||
| "taskId": "112", | |||
| "passed": true | |||
| } | |||
| ] | |||
| @@ -0,0 +1 @@ | |||
| <!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>桥梁三套流程三维验收</title><style>body{margin:0;background:#edf3f2;color:#173d3c;font:16px/1.7 system-ui,sans-serif}main{max-width:1200px;margin:32px auto;padding:28px;background:white;border:1px solid #ccdeda;border-radius:12px}h1{font-size:28px}h2{font-size:21px;margin-top:32px}table{width:100%;border-collapse:collapse}td,th{padding:12px;border:1px solid #cbded9;text-align:left}th{background:#eef6f3}.notice{padding:16px;background:#fff5e4;border-left:4px solid #d18b2a}img{width:100%;border:1px solid #cedbd8}a{color:#007d68}li{margin:5px 0}code{font-family:monospace}small{color:#526e6b}</style><main><h1>桥梁三套流程三维验收</h1><p>2026-09-14 · 三维实现与正式训练验证通过</p><p class="notice">006、007 的原任务已有学员运行,现有接口禁止撤回覆盖。三维实现已完成;这两条原任务的入口迁移仍待确定已有试用记录的处理方式。008 原入口已更新,005 保留原流程。</p><table><tr><th>任务</th><th>三维操作</th><th>原入口状态</th></tr><tr><td>005 液压泵应急切换</td><td>原 5 步机制保留并回归验证</td><td>保持原三维入口</td></tr><tr><td>006 液压泵应急替代</td><td>7 步 / 11 项接头、软管、阀门与开关</td><td>已有提交记录,等待换版处理</td></tr><tr><td>007 前泵换件修复</td><td>11 步 / 27 项螺栓、旧泵、新泵、纸垫与加油</td><td>已有进行中记录,等待换版处理</td></tr><tr><td>008 平衡阀拆拼互换</td><td>16 步 / 50 项拆卸、封堵、互换与连接</td><td>原任务 89 已更新到脚本 v3</td></tr></table><h2>验证内容</h2><ul><li>TASK-VIRTUAL-006: 7 steps, 11 3D operations; preview server validation passed</li><li>TASK-VIRTUAL-007: 11 steps, 27 3D operations; preview server validation passed</li><li>TASK-VIRTUAL-008: 16 steps, 50 3D operations; preview server validation passed</li><li>保存失败保留当前步骤与未完成状态,允许重试</li><li>TASK-VIRTUAL-006 刷新恢复模型位置、封堵显示状态与步骤进度</li><li>TASK-VIRTUAL-006 三维操作、提交与教员评定通过</li><li>TASK-VIRTUAL-007 刷新恢复模型位置、封堵显示状态与步骤进度</li><li>TASK-VIRTUAL-007 三维操作、提交与教员评定通过</li><li>TASK-VIRTUAL-008 刷新恢复模型位置、封堵显示状态与步骤进度</li><li>TASK-VIRTUAL-008 三维操作、提交与教员评定通过</li><li>TASK-VIRTUAL-005 原任务入口通过,原运行与成绩未变化</li><li>TASK-VIRTUAL-008 原任务入口通过,原运行与成绩未变化</li><li>前端生产构建通过;后端 ut_tran 测试 422 项通过。</li><li>逐项校验零件位移、显示与缩放,验证刷新恢复、失败重试、服务端保存、提交与教员评定。</li><li>验收副本 7 条及关联运行已软删除,原四条任务与其原有运行保持保留。</li></ul><h2>实现边界</h2><p>与 005 共用 HTML+JS 三维工作区和结构树、聚焦、高亮、标签、隔离、爆炸工具。整车与液压泵使用原 GLB;原模型没有的软管、封口袋、纸垫和平衡阀等采用明确标注的补充操作示意,不能当作实车精确结构或扭矩、受力测量。</p><p>平衡阀第 11、15 步原稿指令冲突,必须记录处理依据后交教员核对;没有自动认定机械操作正确。教员预览不产生学员成绩;学员正式训练通过现有接口保存并评分。</p><h2>006:接管与应急替代</h2><a href="TASK-VIRTUAL-006-reviewed.png"><img loading="lazy" src="TASK-VIRTUAL-006-reviewed.png" alt="006:接管与应急替代"></a><h2>007:换泵与零件状态恢复</h2><a href="TASK-VIRTUAL-007-reviewed.png"><img loading="lazy" src="TASK-VIRTUAL-007-reviewed.png" alt="007:换泵与零件状态恢复"></a><h2>008:原任务三维入口</h2><a href="TASK-VIRTUAL-008-original.png"><img loading="lazy" src="TASK-VIRTUAL-008-original.png" alt="008:原任务三维入口"></a><h2>008:平衡阀互换与教员评定</h2><a href="TASK-VIRTUAL-008-reviewed.png"><img loading="lazy" src="TASK-VIRTUAL-008-reviewed.png" alt="008:平衡阀互换与教员评定"></a><h2>原始结果</h2><p><a href="acceptance.json">预览验证</a> · <a href="run-acceptance.json">学员训练验证</a> · <a href="originals.json">原入口回归</a> · <a href="cleanup.json">测试数据清理</a></p></main></html> | |||
| @@ -0,0 +1,7 @@ | |||
| { | |||
| "pass": true, | |||
| "checks": [ | |||
| "TASK-VIRTUAL-005 原任务入口通过,原运行与成绩未变化", | |||
| "TASK-VIRTUAL-008 原任务入口通过,原运行与成绩未变化" | |||
| ] | |||
| } | |||
| @@ -0,0 +1,30 @@ | |||
| { | |||
| "pass": true, | |||
| "checks": [ | |||
| "保存失败保留当前步骤与未完成状态,允许重试", | |||
| "TASK-VIRTUAL-006 刷新恢复模型位置、封堵显示状态与步骤进度", | |||
| "TASK-VIRTUAL-006 三维操作、提交与教员评定通过", | |||
| "TASK-VIRTUAL-007 刷新恢复模型位置、封堵显示状态与步骤进度", | |||
| "TASK-VIRTUAL-007 三维操作、提交与教员评定通过", | |||
| "TASK-VIRTUAL-008 刷新恢复模型位置、封堵显示状态与步骤进度", | |||
| "TASK-VIRTUAL-008 三维操作、提交与教员评定通过" | |||
| ], | |||
| "runs": [ | |||
| { | |||
| "taskId": "110", | |||
| "runId": "108", | |||
| "code": "TASK-VIRTUAL-006" | |||
| }, | |||
| { | |||
| "taskId": "111", | |||
| "runId": "109", | |||
| "code": "TASK-VIRTUAL-007" | |||
| }, | |||
| { | |||
| "taskId": "112", | |||
| "runId": "110", | |||
| "code": "TASK-VIRTUAL-008" | |||
| } | |||
| ], | |||
| "errors": [] | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| {"runs":[{"taskId":"107","runId":"105","code":"TASK-VIRTUAL-006"},{"taskId":"108","runId":"106","code":"TASK-VIRTUAL-007"}],"userId":"172"} | |||
| {"runs":[{"taskId":"110","runId":"107","code":"TASK-VIRTUAL-006"}],"userId":"173"} | |||
| {"runs":[{"taskId":"110","runId":"108","code":"TASK-VIRTUAL-006"},{"taskId":"111","runId":"109","code":"TASK-VIRTUAL-007"},{"taskId":"112","runId":"110","code":"TASK-VIRTUAL-008"}],"userId":"174"} | |||
| @@ -0,0 +1 @@ | |||
| null | |||
| @@ -0,0 +1,17 @@ | |||
| [ | |||
| { | |||
| "taskId": "110", | |||
| "runId": "108", | |||
| "code": "TASK-VIRTUAL-006" | |||
| }, | |||
| { | |||
| "taskId": "111", | |||
| "runId": "109", | |||
| "code": "TASK-VIRTUAL-007" | |||
| }, | |||
| { | |||
| "taskId": "112", | |||
| "runId": "110", | |||
| "code": "TASK-VIRTUAL-008" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,52 @@ | |||
| import fs from 'node:fs'; | |||
| import assert from 'node:assert/strict'; | |||
| import { chromium, expect } from '@playwright/test'; | |||
| import { login, call } from './role-permissions-live.mjs'; | |||
| import { scripts, scriptDefinition } from '../../unreal_tran_web/src/features/virtual-training-scripts/definition.js'; | |||
| const dir='reports/bridge-grounding-20260914';fs.mkdirSync(dir,{recursive:true}); | |||
| const session=await login('root'),checks=[],errors=[],writes=[]; | |||
| const api=async path=>{const r=await call('tran/v1/'+path,session.accessToken);assert.equal(r.status,200);return r.data;}; | |||
| const before=await api('teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100'); | |||
| const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); | |||
| const page=await browser.newPage({viewport:{width:1920,height:1000}}); | |||
| await page.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'military'}));},session); | |||
| page.on('pageerror',e=>errors.push(e.message)); | |||
| page.on('request',r=>{if(r.method()!=='GET'&&r.url().includes('/api/tran/'))writes.push(r.url());}); | |||
| const diag=()=>page.evaluate(()=>globalThis.__trainingSceneDiagnostics()); | |||
| const ready=async()=>{await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:120000});await page.waitForTimeout(1300);}; | |||
| const verify=d=>{ | |||
| assert.ok(d.bridgeGrounding?.length);assert.ok(d.framing.environmentAlignment.workArea.changedVertices>0); | |||
| for(const p of d.bridgeGrounding){assert.ok(Math.abs(p.minGap)<1e-6);assert.ok(p.maxGap<0.03,JSON.stringify(p));} | |||
| const vehicle=d.framing.sceneObjects[1];assert.ok(Math.abs(vehicle.min[1])<0.03,JSON.stringify(vehicle));return vehicle; | |||
| }; | |||
| try{ | |||
| for(const id of [86,87,88,89]){ | |||
| const task=before.records.find(t=>Number(t.id)===id),script=scripts.find(s=>s.code===task.code); | |||
| const isolated=task.definitionSnapshot.runtime.scriptVersion!==script.version; | |||
| // Older published tasks remain untouched: only this browser's GET response is replaced for rendering QA. | |||
| if(isolated)await page.route(`**/api/tran/v1/teaching/assignments/${id}`,async route=>{ | |||
| const response=await route.fetch(),body=await response.json();body.data.definitionSnapshot=scriptDefinition(script);await route.fulfill({response,json:body}); | |||
| }); | |||
| await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${id}`);await ready(); | |||
| const initial=await diag(),pose=verify(initial); | |||
| await page.locator('[data-action="viewer-reset-view"]').click();await page.waitForTimeout(700); | |||
| assert.deepEqual(verify(await diag()),pose); | |||
| await page.screenshot({path:`${dir}/${task.code}.png`}); | |||
| if(!isolated){ | |||
| const response=page.waitForResponse(r=>r.url().includes('/preview/validate')); | |||
| await page.locator('[data-role="part-label"]').click();assert.equal((await response).status(),200); | |||
| await expect(page.locator('[data-action="next"]')).toBeEnabled();await page.locator('[data-action="next"]').click(); | |||
| await page.locator('[data-action="viewer-reset-view"]').click();await page.waitForTimeout(700); | |||
| assert.deepEqual(verify(await diag()),pose); | |||
| } | |||
| await page.reload();await ready();assert.deepEqual(verify(await diag()),pose); | |||
| checks.push({code:task.code,pass:true,mode:isolated?'isolated-v3-render-no-database-change':'published-task-preview',grounding:initial.bridgeGrounding}); | |||
| await page.unrouteAll({behavior:'wait'}); | |||
| } | |||
| assert.deepEqual(errors,[]);assert.ok(writes.every(url=>url.endsWith('/preview/validate'))); | |||
| const after=await api('teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100'); | |||
| assert.equal(after.total,before.total);assert.deepEqual(after.records.map(t=>[t.id,t.version,t.contentVersionId]),before.records.map(t=>[t.id,t.version,t.contentVersionId])); | |||
| fs.writeFileSync(`${dir}/result.json`,JSON.stringify({pass:true,checks,errors,remainingTasks:after.total,databaseWrites:'preview validation only; no task/run creation or migration'},null,2)); | |||
| fs.writeFileSync(`${dir}/index.html`,`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>桥梁车辆贴地修复</title><style>body{font:16px/1.7 system-ui;background:#edf4f1;color:#123e35;margin:32px auto;max-width:1200px}section{background:white;padding:24px;margin:20px 0}img{max-width:100%}a{color:#008468}</style><section><h1>桥梁车辆贴地修复 · 验证通过</h1><p>仅对四套桥梁脚本的训练位置平整地形,按轮胎与桌脚接触点贴地;原 GLB 文件和其他场景不变。验证复位、重新加载,以及 005/008 原任务切换步骤后车辆保持地面位置。</p><p>006/007 的三维版使用浏览器内隔离渲染验证,未变更旧任务入口或已有记录。列表仍为 ${after.total} 条,没有新增测试任务。</p><p>前端构建与贴地/正式路由测试通过。<a href="result.json">原始验证结果</a></p></section>${checks.map(c=>`<section><h2>${c.code}</h2><p>${c.mode}</p><img src="${c.code}.png" alt="${c.code} 车辆落地"></section>`).join('')}</html>`); | |||
| }catch(error){await page.screenshot({path:`${dir}/failure.png`}).catch(()=>{});fs.writeFileSync(`${dir}/result.json`,JSON.stringify({pass:false,error:error.message,checks,errors},null,2));throw error;} | |||
| finally{await browser.close();} | |||
| @@ -0,0 +1,14 @@ | |||
| import assert from 'node:assert/strict'; | |||
| import { expect } from '@playwright/test'; | |||
| export async function verifyVisualEffects(page, operation) { | |||
| await expect.poll(async()=>page.evaluate(()=>globalThis.__trainingSceneDiagnostics().bridgeWorkshop.pending)).toBe(false); | |||
| const targets = await page.evaluate(()=>globalThis.__trainingSceneDiagnostics().bridgeWorkshop.targets); | |||
| for (const effect of operation.effects || []) { | |||
| const target = targets.find(item=>item.id===effect.targetId); | |||
| assert.ok(target, `Missing visual target ${effect.targetId}`); | |||
| if (typeof effect.visible==='boolean') assert.equal(target.visible,effect.visible,`${effect.targetId} visibility`); | |||
| if (effect.offset) assert.ok(target.position.every((v,i)=>Math.abs(v-target.home[i]-effect.offset[i])<0.001),`${effect.targetId} displacement`); | |||
| if (effect.scale) assert.ok(target.scale.every((v,i)=>Math.abs(v-target.homeScale[i]*effect.scale[i])<0.001),`${effect.targetId} scale`); | |||
| } | |||
| } | |||
| @@ -0,0 +1,69 @@ | |||
| import { chromium, expect } from '@playwright/test'; | |||
| import fs from 'node:fs'; | |||
| import assert from 'node:assert/strict'; | |||
| import crypto from 'node:crypto'; | |||
| import { login, call } from './role-permissions-live.mjs'; | |||
| import { scripts, scriptDefinition } from '../../unreal_tran_web/src/features/virtual-training-scripts/definition.js'; | |||
| import { verifyVisualEffects } from './bridge-three-dimensional-assertions.mjs'; | |||
| const dir='reports/bridge-three-dimensional-20260913';fs.mkdirSync(dir,{recursive:true}); | |||
| const session=await login('root'), fixtures=[], checks=[], errors=[]; | |||
| const api=async(path,data,method)=>{const r=await call(path,session.accessToken,data,method);assert.equal(r.status,200,`${path}: ${r.message}`);return r.data;}; | |||
| const fixtureFile=`${dir}/fixtures-${Date.now()}.json`; | |||
| const saveFixtures=()=>fs.writeFileSync(fixtureFile,JSON.stringify(fixtures,null,2)); | |||
| const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); | |||
| const page=await browser.newPage({viewport:{width:1600,height:1000}}); | |||
| await page.addInitScript(s=>sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken),session); | |||
| page.on('pageerror',e=>errors.push(e.message)); | |||
| const waitReady=async()=>{await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:120000});await page.waitForFunction(()=>globalThis.__trainingSceneDiagnostics()?.bridgeWorkshop?.targets.length>0);await page.waitForTimeout(1800);}; | |||
| try { | |||
| for(const script of scripts.filter(s=>s.version==='3' && (!process.env.BRIDGE_CODE || process.env.BRIDGE_CODE===s.code))) { | |||
| const content=await api('tran/v1/content/projects',{type:'TRAINING',name:`三维验收·${script.title}`,description:'Temporary 3D acceptance fixture',categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content:scriptDefinition(script),dependencies:[],assets:[]}); | |||
| const fixture={code:script.code,projectId:content.project.id};fixtures.push(fixture);saveFixtures(); | |||
| const project=(await api(`tran/v1/content/projects/${fixture.projectId}/publish`,{version:content.project.version})).project; | |||
| let task=await api('tran/v1/teaching/assignments',{code:`CHECK-3D-${Date.now()}-${script.code}`,name:`三维验收·${script.title}`,description:'Temporary acceptance fixture',channel:'VIRTUAL',assignmentKind:'TRAINING',executionMode:'PRACTICE',audienceType:'COMMON',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:false,contentProjectId:project.id,contentVersionId:project.publishedVersionId,members:[]}); | |||
| fixture.taskId=task.id;saveFixtures(); | |||
| task=await api(`tran/v1/teaching/assignments/${task.id}/publish`,{version:task.version}); | |||
| await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${task.id}`);await waitReady(); | |||
| await expect(page.locator('.bridge-procedure')).toHaveCount(0); | |||
| const bar=page.locator('[data-role="model-commandbar"]'); | |||
| for(const action of ['toggle-structure-tree','focus-selected-part','viewer-reset-view','toggle-part-labels','toggle-issue-highlight','toggle-hide-others','toggle-explosion'])await expect(bar.locator(`[data-action="${action}"]`)).toBeVisible(); | |||
| await page.screenshot({path:`${dir}/${script.code}-start.png`}); | |||
| fs.writeFileSync(`${dir}/${script.code}-diagnostic.json`,JSON.stringify(await page.evaluate(()=>globalThis.__trainingSceneDiagnostics()),null,2)); | |||
| for(let index=0;index<script.definition.steps.length;index++) { | |||
| const step=script.definition.steps[index]; | |||
| await expect(page.locator('[data-role="step-count"]')).toHaveText(`${index+1} / ${script.steps.length}`); | |||
| if(step.mode==='measure'){ | |||
| await page.locator('[data-open-preparation]').click(); | |||
| await page.locator('[data-measurement]').fill('按原稿连接管路,矛盾描述交教员核对。'); | |||
| await page.locator('[data-close-preparation]').click(); | |||
| } | |||
| for(let k=0;k<step.action.mechanism.steps.length;k++) { | |||
| const op=step.action.mechanism.steps[k]; | |||
| await expect.poll(async()=>page.evaluate(()=>globalThis.__trainingSceneDiagnostics().bridgeWorkshop.operationIndex)).toBe(k); | |||
| if(k===step.action.mechanism.steps.length-1){ | |||
| const response=page.waitForResponse(r=>r.url().includes(`/assignments/${task.id}/preview/validate`)); | |||
| await page.locator('[data-role="part-label"]').click(); | |||
| assert.equal((await response).status(),200); | |||
| }else{ | |||
| await page.locator('[data-role="part-label"]').click(); | |||
| await expect.poll(async()=>page.evaluate(()=>globalThis.__trainingSceneDiagnostics().bridgeWorkshop.operationIndex)).toBe(k+1); | |||
| } | |||
| await verifyVisualEffects(page,op); | |||
| console.log(script.code,index+1,k+1,op.targetId); | |||
| } | |||
| await expect(page.locator('[data-action="next"]')).toBeEnabled(); | |||
| if([1,3,4,7,10,15].includes(index))await page.screenshot({path:`${dir}/${script.code}-step-${index+1}.png`}); | |||
| if(index<script.steps.length-1)await page.locator('[data-action="next"]').click(); | |||
| } | |||
| assert.equal((await api(`tran/v1/teaching/runs?assignmentId=${task.id}&size=100`)).total,0); | |||
| checks.push(`${script.code}: ${script.steps.length} steps, ${script.definition.steps.flatMap(s=>s.action.mechanism.steps).length} 3D operations; preview server validation passed`); | |||
| fixture.passed=true;saveFixtures(); | |||
| } | |||
| assert.deepEqual(errors,[]); | |||
| fs.writeFileSync(`${dir}/acceptance.json`,JSON.stringify({pass:true,checks,errors},null,2)); | |||
| } catch(error){ | |||
| console.log('Feedback',await page.locator('.el-message').allTextContents()); | |||
| await page.screenshot({path:`${dir}/failure.png`}).catch(()=>{}); | |||
| fs.writeFileSync(`${dir}/acceptance.json`,JSON.stringify({pass:false,error:error.message,checks,errors},null,2));throw error; | |||
| }finally{await browser.close();saveFixtures();} | |||
| @@ -0,0 +1,30 @@ | |||
| import {chromium,expect} from '@playwright/test'; | |||
| import assert from 'node:assert/strict'; | |||
| import fs from 'node:fs'; | |||
| import {login,call} from './role-permissions-live.mjs'; | |||
| const dir='reports/bridge-three-dimensional-20260913', s=await login('root'),checks=[]; | |||
| const api=async p=>{const r=await call('tran/v1/'+p,s.accessToken);assert.equal(r.status,200);return r.data;}; | |||
| const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); | |||
| try { | |||
| const page=await browser.newPage({viewport:{width:1600,height:1000}}); | |||
| await page.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'military'}));},s); | |||
| for(const id of [86,89]) { | |||
| const task=await api('teaching/assignments/'+id), before=await api('teaching/runs?assignmentId='+id+'&size=100'); | |||
| await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/'+id); | |||
| await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:120000});await page.waitForTimeout(1500); | |||
| const steps=id===86?task.definitionSnapshot.steps:task.definitionSnapshot.steps.slice(0,1); | |||
| for(let i=0;i<steps.length;i++) { | |||
| for(let j=0;j<steps[i].action.mechanism.steps.length;j++) { | |||
| if(j===steps[i].action.mechanism.steps.length-1){const response=page.waitForResponse(r=>r.url().includes('/preview/validate'));await page.locator('[data-role="part-label"]').click();assert.equal((await response).status(),200);} | |||
| else{await page.locator('[data-role="part-label"]').click();await page.waitForTimeout(1000);} | |||
| } | |||
| await expect(page.locator('[data-action="next"]')).toBeEnabled(); | |||
| if(i<steps.length-1)await page.locator('[data-action="next"]').click(); | |||
| } | |||
| await page.screenshot({path:`${dir}/${task.code}-original.png`}); | |||
| const stable=value=>JSON.parse(JSON.stringify(value,(key,item)=>key==='serverNow'?undefined:item)); | |||
| assert.deepEqual(stable((await api('teaching/runs?assignmentId='+id+'&size=100')).records),stable(before.records)); | |||
| checks.push(`${task.code} 原任务入口通过,原运行与成绩未变化`); | |||
| } | |||
| fs.writeFileSync(`${dir}/originals.json`,JSON.stringify({pass:true,checks},null,2)); | |||
| }finally{await browser.close();} | |||
| @@ -0,0 +1,77 @@ | |||
| import {chromium,expect} from '@playwright/test'; | |||
| import assert from 'node:assert/strict'; | |||
| import crypto from 'node:crypto'; | |||
| import fs from 'node:fs'; | |||
| import {login,call} from './role-permissions-live.mjs'; | |||
| import { scripts } from '../../unreal_tran_web/src/features/virtual-training-scripts/definition.js'; | |||
| import { verifyVisualEffects } from './bridge-three-dimensional-assertions.mjs'; | |||
| const dir='reports/bridge-three-dimensional-20260913'; | |||
| const root=await login('root'); | |||
| const api=async(path,data,method)=>{const r=await call(path,root.accessToken,data,method);assert.equal(r.status,200,`${path}: ${r.message}`);return r.data;}; | |||
| const roles=await api('auth/v1/roles/all'),role=(roles.records||roles).find(r=>r.code==='student'); | |||
| const password='Br!'+crypto.randomBytes(8).toString('hex'); | |||
| const created=await api('auth/v1/users',{username:'bridge3d_'+Date.now().toString(36),password,displayName:'三维流程验收',departmentId:'5',roleIds:[role.id],defaultRoleId:role.id,enabled:true,remark:'临时验收账号'}); | |||
| const userId=(created.user||created).id; | |||
| const signed=await call('auth/v1/auth/login',null,{userId,password,roleCode:'student',rememberMe:false});assert.equal(signed.status,200); | |||
| const learner=async(path,data)=>{const r=await call('tran/v1/'+path,signed.data.accessToken,data);assert.equal(r.status,200,`${path}: ${r.message}`);return r.data;}; | |||
| const file=fs.readdirSync(dir).filter(n=>/^fixtures-\d+\.json$/.test(n)).sort().at(-1); | |||
| const fixtures=JSON.parse(fs.readFileSync(`${dir}/${file}`,'utf8')); | |||
| const checks=[],runs=[],errors=[]; | |||
| const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); | |||
| const page=await browser.newPage({viewport:{width:1600,height:1000}}); | |||
| await page.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'military'}));},signed.data); | |||
| page.on('pageerror',e=>errors.push(e.message)); | |||
| const ready=async()=>{await page.waitForFunction(()=>globalThis.__trainingSceneDiagnostics?.()?.bridgeWorkshop?.targets.length>0,{},{timeout:120000});await page.waitForTimeout(1200);}; | |||
| const diag=()=>page.evaluate(()=>globalThis.__trainingSceneDiagnostics()); | |||
| try{ | |||
| for(const fixture of fixtures.filter(f=>!process.env.BRIDGE_CODE||process.env.BRIDGE_CODE===f.code)){ | |||
| const task=await learner(`teaching/assignments/${fixture.taskId}`); | |||
| const script=scripts.find(s=>s.code===fixture.code); | |||
| assert.deepEqual(task.definitionSnapshot.steps.map(s=>s.action.mechanism.steps),script.definition.steps.map(s=>s.action.mechanism.steps),'Learner projection must retain visual actions'); | |||
| let run=await learner(`teaching/assignments/${task.id}/accept`,{assignmentVersion:task.version,teamCode:'NEUTRAL',positionCode:'LEARNER'}); | |||
| runs.push({taskId:task.id,runId:run.id,code:fixture.code});fs.writeFileSync(`${dir}/run-fixtures.json`,JSON.stringify(runs,null,2)); | |||
| await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/runs/${run.id}`);await ready(); | |||
| for(let index=0;index<task.definitionSnapshot.steps.length;index++){ | |||
| const step=task.definitionSnapshot.steps[index],ops=step.action.mechanism.steps; | |||
| await expect(page.locator('[data-role="step-count"]')).toHaveText(`${index+1} / ${task.definitionSnapshot.steps.length}`); | |||
| if(step.mode==='measure'){ | |||
| await page.locator('[data-open-preparation]').click();await page.locator('[data-measurement]').fill('根据安装段连接管路,原稿矛盾描述待教员核对。');await page.locator('[data-close-preparation]').click(); | |||
| } | |||
| for(let k=0;k<ops.length;k++){ | |||
| await expect(page.locator('[data-action="next"]')).toBeDisabled(); | |||
| await expect.poll(async()=>(await diag()).bridgeWorkshop.operation.targetId).toBe(ops[k].targetId); | |||
| if(index===0&&k===ops.length-1&&fixture.code==='TASK-VIRTUAL-006'){ | |||
| await page.route(`**/teaching/runs/${run.id}/progress`,route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'SERVICE_UNAVAILABLE',message:'验收模拟保存失败'})}),{times:1}); | |||
| const response=page.waitForResponse(r=>r.url().includes(`/runs/${run.id}/progress`)); | |||
| await page.locator('[data-role="part-label"]').click();assert.equal((await response).status(),503); | |||
| await expect.poll(async()=>(await diag()).bridgeWorkshop.pending).toBe(false); | |||
| assert.equal((await learner(`teaching/runs/${run.id}`)).progressPercent,0); | |||
| await expect(page.locator('[data-action="next"]')).toBeDisabled();checks.push('保存失败保留当前步骤与未完成状态,允许重试'); | |||
| } | |||
| if(k===ops.length-1){const r=page.waitForResponse(r=>r.url().includes(`/runs/${run.id}/progress`));await page.locator('[data-role="part-label"]').click();assert.equal((await r).status(),200);} | |||
| else{await page.locator('[data-role="part-label"]').click();await expect.poll(async()=>(await diag()).bridgeWorkshop.operationIndex).toBe(k+1);} | |||
| await verifyVisualEffects(page,ops[k]); | |||
| } | |||
| await expect(page.locator('[data-action="next"]')).toBeEnabled(); | |||
| const saved=await learner(`teaching/runs/${run.id}/steps`);assert.equal(saved.filter(s=>s.statusCode==='COMPLETED').length,index+1); | |||
| if(index===3){ | |||
| const before=(await diag()).bridgeWorkshop.targets; | |||
| await page.reload();await ready(); | |||
| const after=(await diag()).bridgeWorkshop.targets; | |||
| for(const b of before){const a=after.find(v=>v.id===b.id);assert.equal(a.visible,b.visible);assert.ok(a.position.every((v,i)=>Math.abs(v-b.position[i])<0.001),`Restore ${b.id}`);} | |||
| checks.push(`${fixture.code} 刷新恢复模型位置、封堵显示状态与步骤进度`); | |||
| }else if(index<task.definitionSnapshot.steps.length-1)await page.locator('[data-action="next"]').click(); | |||
| console.log(fixture.code,index+1,'saved'); | |||
| } | |||
| await page.locator('[data-action="next"]').click();await expect.poll(async()=>(await learner(`teaching/runs/${run.id}`)).status).toBe('SUBMITTED'); | |||
| run=await learner(`teaching/runs/${run.id}`);assert.equal(run.score,null); | |||
| run=await api(`tran/v1/teaching/runs/${run.id}/review`,{version:run.version,commandId:crypto.randomUUID(),score:85,passed:true,feedback:'自动化验收示例评分',rubric:{testFixture:true}}); | |||
| assert.equal((await learner(`teaching/runs/${run.id}`)).score,85); | |||
| await page.reload();await ready();await expect(page.locator('.virtual-server-status')).toContainText('85'); | |||
| await page.screenshot({path:`${dir}/${fixture.code}-reviewed.png`}); | |||
| checks.push(`${fixture.code} 三维操作、提交与教员评定通过`); | |||
| } | |||
| assert.deepEqual(errors,[]);fs.writeFileSync(`${dir}/run-acceptance.json`,JSON.stringify({pass:true,checks,runs,errors},null,2)); | |||
| }catch(error){await page.screenshot({path:`${dir}/run-failure.png`}).catch(()=>{});fs.writeFileSync(`${dir}/run-diagnostic.json`,JSON.stringify(await diag().catch(()=>null),null,2));fs.writeFileSync(`${dir}/run-acceptance.json`,JSON.stringify({pass:false,error:error.message,checks,runs,errors},null,2));throw error;} | |||
| finally{fs.appendFileSync(`${dir}/run-attempts.jsonl`,JSON.stringify({runs,userId})+'\n');await browser.close();const user=await api(`auth/v1/users/${userId}`);await api(`auth/v1/users/${userId}`,{version:user.version},'DELETE');} | |||
| @@ -10,7 +10,7 @@ const api = async (endpoint, body, method) => { | |||
| assert.equal(r.status, 200, `${endpoint}: ${r.message}`); return r.data; | |||
| }; | |||
| const registered = []; | |||
| for (const script of scripts) { | |||
| for (const script of scripts.filter(item=>!process.env.BRIDGE_CODE || item.code===process.env.BRIDGE_CODE)) { | |||
| const tasks = await api(`teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&keyword=${script.code}&size=100`); | |||
| let task = tasks.records.find(item => item.code === script.code); | |||
| if (task) assert.equal(task.definitionSnapshot.runtime?.scriptId, script.id, 'Refuse to replace an unrelated task'); | |||
| @@ -25,10 +25,10 @@ for (const script of scripts) { | |||
| const correct = task?.definitionSnapshot.runtime?.definitionSource === 'SCRIPT' | |||
| && task?.definitionSnapshot.runtime?.scriptVersion === script.version && task.executionMode === 'PRACTICE'; | |||
| if (task?.status === 'PUBLISHED' && !correct) task = await api(`teaching/assignments/${task.id}/withdraw`, | |||
| { version: task.version, reason: '静态 JS 作为流程来源,恢复普通训练与成绩评定' }); | |||
| { version: task.version, reason: `更新脚本 v${script.version},使用对应三维流程与正式训练记录` }); | |||
| const projectCommand = { type: 'TRAINING', name: script.title, description: `原稿:${script.source}`, | |||
| categoryCode: 'MAINTENANCE', trainingMode: 'VIRTUAL', content: scriptDefinition(script), dependencies: [], assets: [], | |||
| changeSummary: '脚本来源 v2:复用正式教学运行、步骤记录与成绩评定' }; | |||
| changeSummary: `脚本来源 v${script.version}:复用三维工作区、正式教学运行、步骤记录与成绩评定` }; | |||
| if (!detail) detail = await api('content/projects', projectCommand); | |||
| else if (detail.currentVersion.content?.runtime?.definitionSource !== 'SCRIPT' | |||
| || detail.currentVersion.content?.runtime?.scriptVersion !== script.version) { | |||
| @@ -56,4 +56,6 @@ for (const script of scripts) { | |||
| console.log(JSON.stringify(registered.at(-1))); | |||
| } | |||
| fs.mkdirSync('reports/bridge-scripted-tasks-20260913', { recursive: true }); | |||
| fs.writeFileSync('reports/bridge-scripted-tasks-20260913/registered.json', JSON.stringify(registered, null, 2)); | |||
| const reportPath='reports/bridge-scripted-tasks-20260913/registered.json'; | |||
| const previous=process.env.BRIDGE_CODE && fs.existsSync(reportPath) ? JSON.parse(fs.readFileSync(reportPath,'utf8')).filter(item=>!registered.some(updated=>updated.code===item.code)) : []; | |||
| fs.writeFileSync(reportPath, JSON.stringify([...previous,...registered].sort((a,b)=>a.code.localeCompare(b.code)), null, 2)); | |||
| @@ -0,0 +1,9 @@ | |||
| import fs from 'node:fs'; | |||
| const dir='reports/bridge-three-dimensional-20260913'; | |||
| const read=name=>JSON.parse(fs.readFileSync(`${dir}/${name}.json`,'utf8')); | |||
| const preview=read('acceptance'),run=read('run-acceptance'),originals=read('originals'),cleanup=read('cleanup'); | |||
| const esc=value=>String(value).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>'); | |||
| const checks=[...preview.checks,...run.checks,...originals.checks]; | |||
| const codePass=preview.pass&&run.pass&&originals.pass; | |||
| const images=[['TASK-VIRTUAL-006-reviewed.png','006:接管与应急替代'],['TASK-VIRTUAL-007-reviewed.png','007:换泵与零件状态恢复'],['TASK-VIRTUAL-008-original.png','008:原任务三维入口'],['TASK-VIRTUAL-008-reviewed.png','008:平衡阀互换与教员评定']]; | |||
| fs.writeFileSync(`${dir}/index.html`,`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>桥梁三套流程三维验收</title><style>body{margin:0;background:#edf3f2;color:#173d3c;font:16px/1.7 system-ui,sans-serif}main{max-width:1200px;margin:32px auto;padding:28px;background:white;border:1px solid #ccdeda;border-radius:12px}h1{font-size:28px}h2{font-size:21px;margin-top:32px}table{width:100%;border-collapse:collapse}td,th{padding:12px;border:1px solid #cbded9;text-align:left}th{background:#eef6f3}.notice{padding:16px;background:#fff5e4;border-left:4px solid #d18b2a}img{width:100%;border:1px solid #cedbd8}a{color:#007d68}li{margin:5px 0}code{font-family:monospace}small{color:#526e6b}</style><main><h1>桥梁三套流程三维验收</h1><p>2026-09-14 · ${codePass?'三维实现与正式训练验证通过':'存在未通过项,请查阅原始结果'}</p><p class="notice">006、007 的原任务已有学员运行,现有接口禁止撤回覆盖。三维实现已完成;这两条原任务的入口迁移仍待确定已有试用记录的处理方式。008 原入口已更新,005 保留原流程。</p><table><tr><th>任务</th><th>三维操作</th><th>原入口状态</th></tr><tr><td>005 液压泵应急切换</td><td>原 5 步机制保留并回归验证</td><td>保持原三维入口</td></tr><tr><td>006 液压泵应急替代</td><td>7 步 / 11 项接头、软管、阀门与开关</td><td>已有提交记录,等待换版处理</td></tr><tr><td>007 前泵换件修复</td><td>11 步 / 27 项螺栓、旧泵、新泵、纸垫与加油</td><td>已有进行中记录,等待换版处理</td></tr><tr><td>008 平衡阀拆拼互换</td><td>16 步 / 50 项拆卸、封堵、互换与连接</td><td>原任务 89 已更新到脚本 v3</td></tr></table><h2>验证内容</h2><ul>${checks.map(c=>`<li>${esc(c)}</li>`).join('')}<li>前端生产构建通过;后端 ut_tran 测试 422 项通过。</li><li>逐项校验零件位移、显示与缩放,验证刷新恢复、失败重试、服务端保存、提交与教员评定。</li><li>验收副本 ${cleanup.softDeletedTasks} 条及关联运行已软删除,原四条任务与其原有运行保持保留。</li></ul><h2>实现边界</h2><p>与 005 共用 HTML+JS 三维工作区和结构树、聚焦、高亮、标签、隔离、爆炸工具。整车与液压泵使用原 GLB;原模型没有的软管、封口袋、纸垫和平衡阀等采用明确标注的补充操作示意,不能当作实车精确结构或扭矩、受力测量。</p><p>平衡阀第 11、15 步原稿指令冲突,必须记录处理依据后交教员核对;没有自动认定机械操作正确。教员预览不产生学员成绩;学员正式训练通过现有接口保存并评分。</p>${images.map(([file,title])=>`<h2>${title}</h2><a href="${file}"><img loading="lazy" src="${file}" alt="${title}"></a>`).join('')}<h2>原始结果</h2><p><a href="acceptance.json">预览验证</a> · <a href="run-acceptance.json">学员训练验证</a> · <a href="originals.json">原入口回归</a> · <a href="cleanup.json">测试数据清理</a></p></main></html>`); | |||