Browse Source

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

main
leiyun 6 days ago
parent
commit
10c50fb5c7
56 changed files with 6887 additions and 7 deletions
  1. +491
    -0
      reports/bridge-grounding-20260914/86.json
  2. BIN
     
  3. +1677
    -0
      reports/bridge-grounding-20260914/89.json
  4. BIN
     
  5. BIN
     
  6. BIN
     
  7. BIN
     
  8. BIN
     
  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
     
  13. +1109
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-006-diagnostic.json
  14. BIN
     
  15. BIN
     
  16. BIN
     
  17. BIN
     
  18. BIN
     
  19. +1401
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-007-diagnostic.json
  20. BIN
     
  21. BIN
     
  22. BIN
     
  23. BIN
     
  24. BIN
     
  25. BIN
     
  26. BIN
     
  27. +1621
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-008-diagnostic.json
  28. BIN
     
  29. BIN
     
  30. BIN
     
  31. BIN
     
  32. BIN
     
  33. BIN
     
  34. BIN
     
  35. BIN
     
  36. BIN
     
  37. +9
    -0
      reports/bridge-three-dimensional-20260913/acceptance.json
  38. +51
    -0
      reports/bridge-three-dimensional-20260913/cleanup.json
  39. BIN
     
  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
     
  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 View File

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


+ 1677
- 0
reports/bridge-grounding-20260914/89.json
File diff suppressed because it is too large
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 1
- 0
reports/bridge-grounding-20260914/index.html View File

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

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

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


+ 1109
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-006-diagnostic.json
File diff suppressed because it is too large
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 1401
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-007-diagnostic.json
File diff suppressed because it is too large
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 1621
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-008-diagnostic.json
File diff suppressed because it is too large
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 9
- 0
reports/bridge-three-dimensional-20260913/acceptance.json View File

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

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


+ 7
- 0
reports/bridge-three-dimensional-20260913/fixtures-1789314723229.json View File

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

+ 19
- 0
reports/bridge-three-dimensional-20260913/fixtures-1789314864375.json View File

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

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

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

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

+ 30
- 0
reports/bridge-three-dimensional-20260913/run-acceptance.json View File

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

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

@@ -0,0 +1 @@
null

BIN
View File


+ 17
- 0
reports/bridge-three-dimensional-20260913/run-fixtures.json View File

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

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

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

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

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

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

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

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

Loading…
Cancel
Save