Bläddra i källkod

test(training): verify bridge 3D flows and terrain grounding

main
leiyun 6 dagar sedan
förälder
incheckning
10c50fb5c7
56 ändrade filer med 6887 tillägg och 7 borttagningar
  1. +491
    -0
      reports/bridge-grounding-20260914/86.json
  2. Binär
     
  3. +1677
    -0
      reports/bridge-grounding-20260914/89.json
  4. Binär
     
  5. Binär
     
  6. Binär
     
  7. Binär
     
  8. Binär
     
  9. +1
    -0
      reports/bridge-grounding-20260914/index.html
  10. +162
    -0
      reports/bridge-grounding-20260914/result.json
  11. +3
    -3
      reports/bridge-scripted-tasks-20260913/registered.json
  12. Binär
     
  13. +1109
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-006-diagnostic.json
  14. Binär
     
  15. Binär
     
  16. Binär
     
  17. Binär
     
  18. Binär
     
  19. +1401
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-007-diagnostic.json
  20. Binär
     
  21. Binär
     
  22. Binär
     
  23. Binär
     
  24. Binär
     
  25. Binär
     
  26. Binär
     
  27. +1621
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-008-diagnostic.json
  28. Binär
     
  29. Binär
     
  30. Binär
     
  31. Binär
     
  32. Binär
     
  33. Binär
     
  34. Binär
     
  35. Binär
     
  36. Binär
     
  37. +9
    -0
      reports/bridge-three-dimensional-20260913/acceptance.json
  38. +51
    -0
      reports/bridge-three-dimensional-20260913/cleanup.json
  39. Binär
     
  40. +7
    -0
      reports/bridge-three-dimensional-20260913/fixtures-1789314723229.json
  41. +19
    -0
      reports/bridge-three-dimensional-20260913/fixtures-1789314864375.json
  42. +20
    -0
      reports/bridge-three-dimensional-20260913/fixtures-1789315822880.json
  43. +1
    -0
      reports/bridge-three-dimensional-20260913/index.html
  44. +7
    -0
      reports/bridge-three-dimensional-20260913/originals.json
  45. +30
    -0
      reports/bridge-three-dimensional-20260913/run-acceptance.json
  46. +3
    -0
      reports/bridge-three-dimensional-20260913/run-attempts.jsonl
  47. +1
    -0
      reports/bridge-three-dimensional-20260913/run-diagnostic.json
  48. Binär
     
  49. +17
    -0
      reports/bridge-three-dimensional-20260913/run-fixtures.json
  50. +52
    -0
      tools/bridge-grounding-live.mjs
  51. +14
    -0
      tools/bridge-three-dimensional-assertions.mjs
  52. +69
    -0
      tools/bridge-three-dimensional-live.mjs
  53. +30
    -0
      tools/bridge-three-dimensional-originals.mjs
  54. +77
    -0
      tools/bridge-three-dimensional-run.mjs
  55. +6
    -4
      tools/register-bridge-scripted-tasks.mjs
  56. +9
    -0
      tools/render-bridge-three-dimensional-report.mjs

+ 491
- 0
reports/bridge-grounding-20260914/86.json Visa fil

@@ -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": "取力器开关"
}
]
}
}

Binär
Visa fil


+ 1677
- 0
reports/bridge-grounding-20260914/89.json
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


+ 1
- 0
reports/bridge-grounding-20260914/index.html Visa fil

@@ -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>

+ 162
- 0
reports/bridge-grounding-20260914/result.json Visa fil

@@ -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"
}

+ 3
- 3
reports/bridge-scripted-tasks-20260913/registered.json Visa fil

@@ -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"
}
]

Binär
Visa fil


+ 1109
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-006-diagnostic.json
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


+ 1401
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-007-diagnostic.json
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


+ 1621
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-008-diagnostic.json
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


Binär
Visa fil


+ 9
- 0
reports/bridge-three-dimensional-20260913/acceptance.json Visa fil

@@ -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": []
}

+ 51
- 0
reports/bridge-three-dimensional-20260913/cleanup.json Visa fil

@@ -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"
}
]
}

Binär
Visa fil


+ 7
- 0
reports/bridge-three-dimensional-20260913/fixtures-1789314723229.json Visa fil

@@ -0,0 +1,7 @@
[
{
"code": "TASK-VIRTUAL-006",
"projectId": "446",
"taskId": "106"
}
]

+ 19
- 0
reports/bridge-three-dimensional-20260913/fixtures-1789314864375.json Visa fil

@@ -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"
}
]

+ 20
- 0
reports/bridge-three-dimensional-20260913/fixtures-1789315822880.json Visa fil

@@ -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
}
]

+ 1
- 0
reports/bridge-three-dimensional-20260913/index.html Visa fil

@@ -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>

+ 7
- 0
reports/bridge-three-dimensional-20260913/originals.json Visa fil

@@ -0,0 +1,7 @@
{
"pass": true,
"checks": [
"TASK-VIRTUAL-005 原任务入口通过,原运行与成绩未变化",
"TASK-VIRTUAL-008 原任务入口通过,原运行与成绩未变化"
]
}

+ 30
- 0
reports/bridge-three-dimensional-20260913/run-acceptance.json Visa fil

@@ -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": []
}

+ 3
- 0
reports/bridge-three-dimensional-20260913/run-attempts.jsonl Visa fil

@@ -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"}

+ 1
- 0
reports/bridge-three-dimensional-20260913/run-diagnostic.json Visa fil

@@ -0,0 +1 @@
null

Binär
Visa fil


+ 17
- 0
reports/bridge-three-dimensional-20260913/run-fixtures.json Visa fil

@@ -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"
}
]

+ 52
- 0
tools/bridge-grounding-live.mjs Visa fil

@@ -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();}

+ 14
- 0
tools/bridge-three-dimensional-assertions.mjs Visa fil

@@ -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`);
}
}

+ 69
- 0
tools/bridge-three-dimensional-live.mjs Visa fil

@@ -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();}

+ 30
- 0
tools/bridge-three-dimensional-originals.mjs Visa fil

@@ -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();}

+ 77
- 0
tools/bridge-three-dimensional-run.mjs Visa fil

@@ -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');}

+ 6
- 4
tools/register-bridge-scripted-tasks.mjs Visa fil

@@ -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));

+ 9
- 0
tools/render-bridge-three-dimensional-report.mjs Visa fil

@@ -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('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;');
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>`);

Laddar…
Avbryt
Spara