浏览代码

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

main
leiyun 6 天前
父节点
当前提交
10c50fb5c7
共有 56 个文件被更改,包括 6887 次插入7 次删除
  1. +491
    -0
      reports/bridge-grounding-20260914/86.json
  2. 二进制
     
  3. +1677
    -0
      reports/bridge-grounding-20260914/89.json
  4. 二进制
     
  5. 二进制
     
  6. 二进制
     
  7. 二进制
     
  8. 二进制
     
  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. 二进制
     
  13. +1109
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-006-diagnostic.json
  14. 二进制
     
  15. 二进制
     
  16. 二进制
     
  17. 二进制
     
  18. 二进制
     
  19. +1401
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-007-diagnostic.json
  20. 二进制
     
  21. 二进制
     
  22. 二进制
     
  23. 二进制
     
  24. 二进制
     
  25. 二进制
     
  26. 二进制
     
  27. +1621
    -0
      reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-008-diagnostic.json
  28. 二进制
     
  29. 二进制
     
  30. 二进制
     
  31. 二进制
     
  32. 二进制
     
  33. 二进制
     
  34. 二进制
     
  35. 二进制
     
  36. 二进制
     
  37. +9
    -0
      reports/bridge-three-dimensional-20260913/acceptance.json
  38. +51
    -0
      reports/bridge-three-dimensional-20260913/cleanup.json
  39. 二进制
     
  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. 二进制
     
  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 查看文件

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

二进制
查看文件


+ 1677
- 0
reports/bridge-grounding-20260914/89.json
文件差异内容过多而无法显示
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


+ 1
- 0
reports/bridge-grounding-20260914/index.html 查看文件

@@ -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 查看文件

@@ -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 查看文件

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

二进制
查看文件


+ 1109
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-006-diagnostic.json
文件差异内容过多而无法显示
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


+ 1401
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-007-diagnostic.json
文件差异内容过多而无法显示
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


+ 1621
- 0
reports/bridge-three-dimensional-20260913/TASK-VIRTUAL-008-diagnostic.json
文件差异内容过多而无法显示
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


二进制
查看文件


+ 9
- 0
reports/bridge-three-dimensional-20260913/acceptance.json 查看文件

@@ -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 查看文件

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

二进制
查看文件


+ 7
- 0
reports/bridge-three-dimensional-20260913/fixtures-1789314723229.json 查看文件

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

+ 19
- 0
reports/bridge-three-dimensional-20260913/fixtures-1789314864375.json 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

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

+ 30
- 0
reports/bridge-three-dimensional-20260913/run-acceptance.json 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -0,0 +1 @@
null

二进制
查看文件


+ 17
- 0
reports/bridge-three-dimensional-20260913/run-fixtures.json 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

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

正在加载...
取消
保存