diff --git a/reports/bridge-scripted-tasks-20260913/01-task-list.png b/reports/bridge-scripted-tasks-20260913/01-task-list.png
new file mode 100644
index 0000000..be84d5e
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/01-task-list.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/02-pump-3d.png b/reports/bridge-scripted-tasks-20260913/02-pump-3d.png
new file mode 100644
index 0000000..bbb53fd
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/02-pump-3d.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/03-five-steps-complete.png b/reports/bridge-scripted-tasks-20260913/03-five-steps-complete.png
new file mode 100644
index 0000000..5139f12
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/03-five-steps-complete.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/04-small-screen.png b/reports/bridge-scripted-tasks-20260913/04-small-screen.png
new file mode 100644
index 0000000..bf0e907
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/04-small-screen.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/05-student.png b/reports/bridge-scripted-tasks-20260913/05-student.png
new file mode 100644
index 0000000..25764ac
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/05-student.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-006.png b/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-006.png
new file mode 100644
index 0000000..7bf545a
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-006.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-007.png b/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-007.png
new file mode 100644
index 0000000..41208fe
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-007.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-008.png b/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-008.png
new file mode 100644
index 0000000..721bc9e
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/TASK-VIRTUAL-008.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/acceptance.json b/reports/bridge-scripted-tasks-20260913/acceptance.json
new file mode 100644
index 0000000..dcb0102
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/acceptance.json
@@ -0,0 +1,51 @@
+{
+ "pass": true,
+ "checks": [
+ {
+ "name": "四个接口任务在现有列表呈现",
+ "ms": 4453
+ },
+ {
+ "name": "运行记录请求失败时任务入口保留"
+ },
+ {
+ "name": "重型支援桥液压泵应急替代",
+ "steps": 7,
+ "reset": true
+ },
+ {
+ "name": "重型支援桥前泵换件修复",
+ "steps": 11,
+ "reset": true
+ },
+ {
+ "name": "重型支援桥平衡阀拆拼互换",
+ "steps": 16,
+ "reset": true
+ },
+ {
+ "name": "TASK-VIRTUAL-005 五步六次操作完整执行,工具栏保留,结束不显示评分"
+ },
+ {
+ "name": "刷新重置本轮预演,未调用运行写入接口,未增加评分运行"
+ },
+ {
+ "name": "服务端拒绝将脚本演示领取为评分运行"
+ }
+ ],
+ "errors": [],
+ "failures": [],
+ "externalFailures": [
+ {
+ "origin": "http://1.14.103.234:7180",
+ "path": "/api/v1/files/0d8864a4fe8f46c2bf4fd4902f1b3646/content",
+ "status": 404
+ },
+ {
+ "origin": "http://1.14.103.234:7180",
+ "path": "/api/v1/files/0d8864a4fe8f46c2bf4fd4902f1b3646/content",
+ "status": 404
+ }
+ ],
+ "writes": []
+}
\ No newline at end of file
diff --git a/reports/bridge-scripted-tasks-20260913/idempotence.json b/reports/bridge-scripted-tasks-20260913/idempotence.json
new file mode 100644
index 0000000..40413f3
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/idempotence.json
@@ -0,0 +1,11 @@
+{
+ "pass": true,
+ "before": {
+ "projects": 37,
+ "tasks": 29
+ },
+ "after": {
+ "projects": 37,
+ "tasks": 29
+ }
+}
\ No newline at end of file
diff --git a/reports/bridge-scripted-tasks-20260913/index.html b/reports/bridge-scripted-tasks-20260913/index.html
new file mode 100644
index 0000000..a01b95e
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/index.html
@@ -0,0 +1,24 @@
+
桥梁脚本普通训练闭环验收
+
+桥梁脚本普通训练闭环验收
2026-09-13 · 本机开发环境 · 修正“静态流程不计成绩”的错误设计。
+统一规则
静态 JS 只提供步骤及交互定义。任务使用现有接口与数据库,领取、开始、准备记录、步骤保存、提交、教员评定及统计均与其他普通训练一致。学员训练保存成绩;教员预览按现有规则不写入学员成绩。
+| 任务 / ID | 流程 | 固定版本 | 验收 |
+| TASK-VIRTUAL-005 / 86 | 液压泵应急切换,5步 | 515 | 五步六次机构操作、恢复、提交、评分、统计通过 |
+| TASK-VIRTUAL-006 / 87 | 液压泵应急替代,7步 | 516 | 全部步骤、保存失败重试、恢复、提交、评分、统计通过 |
+| TASK-VIRTUAL-007 / 88 | 前泵换件修复,11步 | 517 | 全部步骤、恢复、提交、评分、统计通过 |
+| TASK-VIRTUAL-008 / 89 | 平衡阀拆拼互换,16步 | 518 | 全部步骤、必填核对记录、恢复、提交、评分、统计通过 |
+原任务编号和 ID 保留,当前均为 PUBLISHED / PRACTICE / COMMON;runtime.definitionSource=SCRIPT,scriptVersion="2"。版本通过 HTTP 内容修订及教学任务更新生成,没有直接执行 SQL。
+入口:虚实训练 → 虚拟仿真 → 领取任务 / 进入任务。已评定运行再次进入可查看教员评分与反馈。原四条任务没有写入测试成绩,可正常领取。
+验证证据
+- 前端 pnpm run build 通过,包括 vue-tsc 与发布资源校验。
+- 后端 Java 17 mvn test 通过;新增后续脚本操作证据检查后,ScriptedTrainingContractTest 与 VirtualRunContractTest 共8项通过。
+- 四个任务副本39步均经过浏览器实际交互,API创建运行、保存步骤、刷新恢复、提交;教员API评定85分,学员可读且统计可查询。85分只属于自动化测试副本。
+- 模拟progress返回503时不推进,重试成功;平衡阀两处待核对步骤无记录不能推进。
+- 四个原任务的教员预览调用preview/validate,未创建学员运行;右侧结构树、聚焦、高亮、标签、隐藏与爆炸工具保留。
+正式训练整轮验收 · 教员预览隔离验收 · 四条任务登记 · 测试尝试与副本归档 · 前端构建日志
+测试副本任务90~105及其运行85~100均已归档,临时学员账号已删除;保留归档记录供审计。任务90/运行85为首次失败清理,后续尝试详见记录文件。
+三套后续流程目前提供脚本步骤操作与记录,对应三维拆装、软管和加油动画尚未制作。平衡阀原稿第11、15步有相反指令,必须记录依据交教员核对,不自动认定机械操作正确。一次与本地构建并行的验收遇到认证服务503,重新独立执行完整回归通过;不能据此宣称系统所有历史访问异常已解决。
+此前 acceptance.json、student-acceptance.json 和旧截图为已被本页替代的历史演示验收,不代表当前评分规则。
+原四条任务的正常入口
+三维脚本提交与评定

+
diff --git a/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-005-reviewed.png b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-005-reviewed.png
new file mode 100644
index 0000000..5829ac6
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-005-reviewed.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-005-submitted.png b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-005-submitted.png
new file mode 100644
index 0000000..b5eb0ca
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-005-submitted.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-006-reviewed.png b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-006-reviewed.png
new file mode 100644
index 0000000..7ecd6e7
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-006-reviewed.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-006-submitted.png b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-006-submitted.png
new file mode 100644
index 0000000..0cf24e8
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-006-submitted.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-007-reviewed.png b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-007-reviewed.png
new file mode 100644
index 0000000..0a33f5b
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-007-reviewed.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-007-submitted.png b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-007-submitted.png
new file mode 100644
index 0000000..e12df4d
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-007-submitted.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-008-reviewed.png b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-008-reviewed.png
new file mode 100644
index 0000000..87cb21b
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-008-reviewed.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-008-submitted.png b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-008-submitted.png
new file mode 100644
index 0000000..edb2cc9
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-TASK-VIRTUAL-008-submitted.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-acceptance.json b/reports/bridge-scripted-tasks-20260913/normal-acceptance.json
new file mode 100644
index 0000000..7435467
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/normal-acceptance.json
@@ -0,0 +1,42 @@
+{
+ "pass": true,
+ "checks": [
+ "原四条任务保留正常领取入口,不显示静态演示或禁评分标签",
+ "步骤保存失败不前进,允许幂等重试",
+ "TASK-VIRTUAL-006 刷新从服务端恢复已完成步骤",
+ "TASK-VIRTUAL-006 7 步:领取、开始、保存、恢复、提交、教员85分评定、统计记录通过",
+ "TASK-VIRTUAL-007 刷新从服务端恢复已完成步骤",
+ "TASK-VIRTUAL-007 11 步:领取、开始、保存、恢复、提交、教员85分评定、统计记录通过",
+ "TASK-VIRTUAL-008 刷新从服务端恢复已完成步骤",
+ "TASK-VIRTUAL-008 16 步:领取、开始、保存、恢复、提交、教员85分评定、统计记录通过",
+ "TASK-VIRTUAL-005 刷新从服务端恢复已完成步骤",
+ "TASK-VIRTUAL-005 5 步:领取、开始、保存、恢复、提交、教员85分评定、统计记录通过"
+ ],
+ "fixtures": [
+ {
+ "taskId": "102",
+ "code": "TASK-VIRTUAL-006",
+ "runId": "97",
+ "archived": true
+ },
+ {
+ "taskId": "103",
+ "code": "TASK-VIRTUAL-007",
+ "runId": "98",
+ "archived": true
+ },
+ {
+ "taskId": "104",
+ "code": "TASK-VIRTUAL-008",
+ "runId": "99",
+ "archived": true
+ },
+ {
+ "taskId": "105",
+ "code": "TASK-VIRTUAL-005",
+ "runId": "100",
+ "archived": true
+ }
+ ],
+ "errors": []
+}
\ No newline at end of file
diff --git a/reports/bridge-scripted-tasks-20260913/normal-attempts.jsonl b/reports/bridge-scripted-tasks-20260913/normal-attempts.jsonl
new file mode 100644
index 0000000..6ed4706
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/normal-attempts.jsonl
@@ -0,0 +1,7 @@
+{"time":"2026-09-13T15:02:55.842Z","fixtures":[{"taskId":"91","code":"TASK-VIRTUAL-006","runId":"86","archived":true}]}
+{"time":"2026-09-13T15:04:35.752Z","fixtures":[{"taskId":"92","code":"TASK-VIRTUAL-006","runId":"87","archived":true},{"taskId":"93","code":"TASK-VIRTUAL-007","runId":"88","archived":true},{"taskId":"94","code":"TASK-VIRTUAL-008","runId":"89","archived":true},{"taskId":"95","code":"TASK-VIRTUAL-005","runId":"90","archived":true}]}
+{"time":"2026-09-13T15:07:38.165Z","fixtures":[{"taskId":"96","code":"TASK-VIRTUAL-006","runId":"91","archived":true},{"taskId":"97","code":"TASK-VIRTUAL-007","runId":"92","archived":true},{"taskId":"98","code":"TASK-VIRTUAL-008","runId":"93","archived":true},{"taskId":"99","code":"TASK-VIRTUAL-005","runId":"94","archived":true}]}
+{"time":"2026-09-13T15:09:12.152Z","fixtures":[{"taskId":"100","code":"TASK-VIRTUAL-005","runId":"95","archived":true}]}
+{"time":"2026-09-13T15:11:18.615Z","fixtures":[{"taskId":"101","code":"TASK-VIRTUAL-005","runId":"96","archived":true}]}
+{"time":"2026-09-13T15:12:41.105Z","fixtures":[]}
+{"time":"2026-09-13T15:15:29.140Z","fixtures":[{"taskId":"102","code":"TASK-VIRTUAL-006","runId":"97","archived":true},{"taskId":"103","code":"TASK-VIRTUAL-007","runId":"98","archived":true},{"taskId":"104","code":"TASK-VIRTUAL-008","runId":"99","archived":true},{"taskId":"105","code":"TASK-VIRTUAL-005","runId":"100","archived":true}]}
diff --git a/reports/bridge-scripted-tasks-20260913/normal-failure.png b/reports/bridge-scripted-tasks-20260913/normal-failure.png
new file mode 100644
index 0000000..2ce8eb8
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-failure.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/normal-fixtures.json b/reports/bridge-scripted-tasks-20260913/normal-fixtures.json
new file mode 100644
index 0000000..5a29a8e
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/normal-fixtures.json
@@ -0,0 +1,26 @@
+[
+ {
+ "taskId": "102",
+ "code": "TASK-VIRTUAL-006",
+ "runId": "97",
+ "archived": true
+ },
+ {
+ "taskId": "103",
+ "code": "TASK-VIRTUAL-007",
+ "runId": "98",
+ "archived": true
+ },
+ {
+ "taskId": "104",
+ "code": "TASK-VIRTUAL-008",
+ "runId": "99",
+ "archived": true
+ },
+ {
+ "taskId": "105",
+ "code": "TASK-VIRTUAL-005",
+ "runId": "100",
+ "archived": true
+ }
+]
\ No newline at end of file
diff --git a/reports/bridge-scripted-tasks-20260913/normal-preview.json b/reports/bridge-scripted-tasks-20260913/normal-preview.json
new file mode 100644
index 0000000..5df7c7c
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/normal-preview.json
@@ -0,0 +1,15 @@
+{
+ "pass": true,
+ "checks": [
+ "TASK-VIRTUAL-005 教员预览使用服务端校验,未创建训练运行",
+ "TASK-VIRTUAL-006 教员预览使用服务端校验,未创建训练运行",
+ "TASK-VIRTUAL-007 教员预览使用服务端校验,未创建训练运行",
+ "TASK-VIRTUAL-008 教员预览使用服务端校验,未创建训练运行"
+ ],
+ "writes": [
+ "/api/tran/v1/teaching/assignments/86/preview/validate",
+ "/api/tran/v1/teaching/assignments/87/preview/validate",
+ "/api/tran/v1/teaching/assignments/88/preview/validate",
+ "/api/tran/v1/teaching/assignments/89/preview/validate"
+ ]
+}
\ No newline at end of file
diff --git a/reports/bridge-scripted-tasks-20260913/normal-task-list.png b/reports/bridge-scripted-tasks-20260913/normal-task-list.png
new file mode 100644
index 0000000..a24de35
Binary files /dev/null and b/reports/bridge-scripted-tasks-20260913/normal-task-list.png differ
diff --git a/reports/bridge-scripted-tasks-20260913/registered.json b/reports/bridge-scripted-tasks-20260913/registered.json
new file mode 100644
index 0000000..7c7181f
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/registered.json
@@ -0,0 +1,50 @@
+[
+ {
+ "code": "TASK-VIRTUAL-005",
+ "id": "86",
+ "projectId": "438",
+ "versionId": "515",
+ "name": "重型支援桥液压泵应急切换",
+ "scriptId": "bridge-pump-switch",
+ "scriptVersion": "2",
+ "steps": 5,
+ "previewUrl": "/teaching/virtual-training/preview/86",
+ "presentation": "original-3d"
+ },
+ {
+ "code": "TASK-VIRTUAL-006",
+ "id": "87",
+ "projectId": "439",
+ "versionId": "516",
+ "name": "重型支援桥液压泵应急替代",
+ "scriptId": "bridge-pump-substitute",
+ "scriptVersion": "2",
+ "steps": 7,
+ "previewUrl": "/teaching/virtual-training/preview/87",
+ "presentation": "procedure"
+ },
+ {
+ "code": "TASK-VIRTUAL-007",
+ "id": "88",
+ "projectId": "440",
+ "versionId": "517",
+ "name": "重型支援桥前泵换件修复",
+ "scriptId": "bridge-pump-replace",
+ "scriptVersion": "2",
+ "steps": 11,
+ "previewUrl": "/teaching/virtual-training/preview/88",
+ "presentation": "procedure"
+ },
+ {
+ "code": "TASK-VIRTUAL-008",
+ "id": "89",
+ "projectId": "441",
+ "versionId": "518",
+ "name": "重型支援桥平衡阀拆拼互换",
+ "scriptId": "bridge-balance-valve-swap",
+ "scriptVersion": "2",
+ "steps": 16,
+ "previewUrl": "/teaching/virtual-training/preview/89",
+ "presentation": "procedure"
+ }
+]
\ No newline at end of file
diff --git a/reports/bridge-scripted-tasks-20260913/registration-cleanup.json b/reports/bridge-scripted-tasks-20260913/registration-cleanup.json
new file mode 100644
index 0000000..1765d6a
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/registration-cleanup.json
@@ -0,0 +1,15 @@
+{
+ "removedUnreferencedVerificationDuplicates": [
+ 437,
+ 442,
+ 443,
+ 444,
+ 445
+ ],
+ "retainedProjects": [
+ 438,
+ 439,
+ 440,
+ 441
+ ]
+}
\ No newline at end of file
diff --git a/reports/bridge-scripted-tasks-20260913/run-list-performance.json b/reports/bridge-scripted-tasks-20260913/run-list-performance.json
new file mode 100644
index 0000000..f969a01
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/run-list-performance.json
@@ -0,0 +1,20 @@
+{
+ "beforeMs": 13814,
+ "after": [
+ {
+ "status": 200,
+ "ms": 965,
+ "records": 30
+ },
+ {
+ "status": 200,
+ "ms": 1378,
+ "records": 30
+ },
+ {
+ "status": 200,
+ "ms": 1373,
+ "records": 30
+ }
+ ]
+}
\ No newline at end of file
diff --git a/reports/bridge-scripted-tasks-20260913/student-acceptance.json b/reports/bridge-scripted-tasks-20260913/student-acceptance.json
new file mode 100644
index 0000000..fcd9813
--- /dev/null
+++ b/reports/bridge-scripted-tasks-20260913/student-acceptance.json
@@ -0,0 +1,7 @@
+{
+ "pass": true,
+ "visibleScripts": 4,
+ "ungradedMetadataPreserved": true,
+ "studentCanEnter": true,
+ "anonymousRejected": true
+}
\ No newline at end of file
diff --git a/reports/front-pump-recovery-20260913/01-loaded.png b/reports/front-pump-recovery-20260913/01-loaded.png
new file mode 100644
index 0000000..725b6a9
Binary files /dev/null and b/reports/front-pump-recovery-20260913/01-loaded.png differ
diff --git a/reports/front-pump-recovery-20260913/02-viewer-tools.png b/reports/front-pump-recovery-20260913/02-viewer-tools.png
new file mode 100644
index 0000000..7a23bc2
Binary files /dev/null and b/reports/front-pump-recovery-20260913/02-viewer-tools.png differ
diff --git a/reports/front-pump-recovery-20260913/03-complete.png b/reports/front-pump-recovery-20260913/03-complete.png
new file mode 100644
index 0000000..8da4739
Binary files /dev/null and b/reports/front-pump-recovery-20260913/03-complete.png differ
diff --git a/reports/front-pump-recovery-20260913/04-1366.png b/reports/front-pump-recovery-20260913/04-1366.png
new file mode 100644
index 0000000..b2b862e
Binary files /dev/null and b/reports/front-pump-recovery-20260913/04-1366.png differ
diff --git a/reports/front-pump-recovery-20260913/TASK-VIRTUAL-005-front-pump-original.json b/reports/front-pump-recovery-20260913/TASK-VIRTUAL-005-front-pump-original.json
new file mode 100644
index 0000000..23f5eca
--- /dev/null
+++ b/reports/front-pump-recovery-20260913/TASK-VIRTUAL-005-front-pump-original.json
@@ -0,0 +1,1072 @@
+{
+ "taskCode": "TASK-VIRTUAL-005",
+ "sourceCommit": "ba747df",
+ "sourceAuthor": "yaozijun",
+ "sourceDate": "2026-09-03",
+ "scope": "原始静态演示定义,未经正式训练发布",
+ "definition": {
+ "schema": "digital-workshop-training-project",
+ "version": 1,
+ "id": "front-pump-rescue-operation-v1",
+ "projectId": "front-pump-rescue-operation-v1",
+ "name": "架桥车前泵故障互救处置训练(虚拟仿真)",
+ "status": "published",
+ "trainingMode": "virtual",
+ "resources": {
+ "models": [
+ {
+ "id": "front-pump-step-vehicle",
+ "modelId": "front-pump-step-vehicle",
+ "name": "架桥车前泵互救操作整车模型",
+ "baseModelName": "架桥车前泵互救操作整车模型",
+ "sourceName": "步骤模型.glb",
+ "resourceUrl": "/models/步骤模型.glb",
+ "assetKey": "",
+ "mimeType": "model/gltf-binary",
+ "sizeBytes": 32195288,
+ "revision": 1,
+ "materialPreset": null,
+ "parts": [
+ {
+ "editorId": "",
+ "interactionId": "front-pump-hydraulic-pump",
+ "nodeName": "1液压泵",
+ "sourceNodeName": "1液压泵",
+ "nodePath": "",
+ "name": "液压泵",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "visible": true
+ },
+ {
+ "editorId": "",
+ "interactionId": "front-pump-power-take-off",
+ "nodeName": "2取力器",
+ "sourceNodeName": "2取力器",
+ "nodePath": "",
+ "name": "取力器",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "visible": true
+ },
+ {
+ "editorId": "",
+ "interactionId": "front-pump-pto-switch",
+ "nodeName": "2取力器开关",
+ "sourceNodeName": "2取力器开关",
+ "nodePath": "",
+ "name": "取力器开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "visible": true
+ },
+ {
+ "editorId": "",
+ "interactionId": "front-pump-ball-valve-switch",
+ "nodeName": "3球阀开关",
+ "sourceNodeName": "3球阀开关",
+ "nodePath": "",
+ "name": "球阀开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "visible": true
+ },
+ {
+ "editorId": "",
+ "interactionId": "front-pump-aux-control-box",
+ "nodeName": "5辅助控制盒",
+ "sourceNodeName": "5辅助控制盒",
+ "nodePath": "",
+ "name": "辅助控制盒",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "visible": true
+ },
+ {
+ "editorId": "",
+ "interactionId": "front-pump-aux-control-box-cover",
+ "nodeName": "5辅助控制盒盖",
+ "sourceNodeName": "5辅助控制盒盖",
+ "nodePath": "",
+ "name": "辅助控制盒盖",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "visible": true
+ },
+ {
+ "editorId": "",
+ "interactionId": "front-pump-rescue-switch",
+ "nodeName": "5互救开关",
+ "sourceNodeName": "5互救开关",
+ "nodePath": "",
+ "name": "互救开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "visible": true
+ }
+ ]
+ }
+ ],
+ "animations": []
+ },
+ "task": {
+ "id": "TR-FRONT-PUMP-RESCUE-001",
+ "code": "TR-FRONT-PUMP-RESCUE-001",
+ "title": "架桥车前泵故障互救处置训练(虚拟仿真)",
+ "categoryId": "minor-repair",
+ "difficulty": "标准",
+ "level": "标准",
+ "duration": 10,
+ "description": "前泵损坏的情况下如何操作。",
+ "image": "/models/step-model-front-pump-thumb.png",
+ "equipment": "架桥车",
+ "skills": [
+ "液压泵故障识别",
+ "取力器断开与联通",
+ "球阀开启",
+ "互救开关操作"
+ ]
+ },
+ "scenario": {
+ "id": "front-pump-rescue-scenario",
+ "publishedSceneId": "SCENE-FRONT-PUMP-RESCUE-01",
+ "publishedSceneVersion": "1.0",
+ "baseVisualKey": "front-pump-rescue",
+ "visualKey": "front-pump-rescue",
+ "environmentName": "架桥车互救操作训练场",
+ "sceneLayout": "架桥车整车操作位",
+ "equipmentState": "前泵故障 · 待执行互救处置",
+ "tools": [],
+ "faults": [],
+ "environment": {
+ "type": "outdoor",
+ "mode": "builtin",
+ "sourceName": "内置架桥车互救操作训练场",
+ "resourceUrl": "",
+ "sizeBytes": 0,
+ "lastModified": 0,
+ "embeddedDataUrl": ""
+ },
+ "sceneResource": {
+ "sceneId": "SCENE-FRONT-PUMP-RESCUE-01",
+ "version": "1.0",
+ "sourceKind": "published",
+ "status": "已发布",
+ "name": "架桥车前泵故障互救处置训练场景",
+ "presetId": "outdoor-1",
+ "type": "outdoor",
+ "objectCount": 2,
+ "objects": [
+ {
+ "id": "front-pump-training-yard",
+ "sceneId": "front-pump-training-yard",
+ "interactionId": "scene:front-pump-training-yard",
+ "type": "imported",
+ "name": "野外山地训练场",
+ "semantic": "场景地形",
+ "assetRole": "environment",
+ "operable": false,
+ "visible": true,
+ "modelId": "front-pump-field-terrain",
+ "modelEditorAssetId": "front-pump-field-terrain",
+ "modelEditorModelId": "front-pump-field-terrain",
+ "modelEditorRevision": 1,
+ "modelOverrides": {},
+ "materialPreset": null,
+ "sourceName": "地形.glb",
+ "resourceUrl": "/models/地形.glb",
+ "assetKey": "",
+ "mimeType": "model/gltf-binary",
+ "sizeBytes": 10726472,
+ "fitToScene": true,
+ "fitSize": 140,
+ "groundLevelY": 0,
+ "position": [
+ 0,
+ 0,
+ 0
+ ],
+ "rotation": [
+ 0,
+ 0,
+ 0
+ ],
+ "scale": [
+ 1,
+ 1,
+ 1
+ ]
+ },
+ {
+ "id": "front-pump-rescue-vehicle",
+ "sceneId": "front-pump-rescue-vehicle",
+ "interactionId": "scene:front-pump-rescue-vehicle",
+ "type": "imported",
+ "name": "架桥车前泵互救操作整车模型",
+ "semantic": "实训装备",
+ "assetRole": "training-content",
+ "operable": true,
+ "visible": true,
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "modelId": "front-pump-step-vehicle",
+ "modelEditorAssetId": "front-pump-step-vehicle",
+ "modelEditorModelId": "front-pump-step-vehicle",
+ "modelEditorRevision": 1,
+ "modelOverrides": {},
+ "materialPreset": null,
+ "modelPartTargets": [
+ {
+ "interactionId": "front-pump-hydraulic-pump",
+ "sourceNodeName": "1液压泵",
+ "name": "液压泵",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-power-take-off",
+ "sourceNodeName": "2取力器",
+ "name": "取力器",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-pto-switch",
+ "sourceNodeName": "2取力器开关",
+ "name": "取力器开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-ball-valve-switch",
+ "sourceNodeName": "3球阀开关",
+ "name": "球阀开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-aux-control-box",
+ "sourceNodeName": "5辅助控制盒",
+ "name": "辅助控制盒",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-aux-control-box-cover",
+ "sourceNodeName": "5辅助控制盒盖",
+ "name": "辅助控制盒盖",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "pivotEdge": {
+ "space": "world",
+ "axis": [
+ 0,
+ 1,
+ 0
+ ],
+ "edge": "nearest-vertical-edge"
+ },
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-rescue-switch",
+ "sourceNodeName": "5互救开关",
+ "name": "互救开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ }
+ ],
+ "sourceName": "步骤模型.glb",
+ "resourceUrl": "/models/步骤模型.glb",
+ "assetKey": "",
+ "mimeType": "model/gltf-binary",
+ "sizeBytes": 32195288,
+ "fitToScene": true,
+ "fitSize": 8,
+ "position": [
+ 0,
+ 0,
+ 0
+ ],
+ "rotation": [
+ 0,
+ 0,
+ 0
+ ],
+ "scale": [
+ 1,
+ 1,
+ 1
+ ]
+ }
+ ],
+ "snapshot": {
+ "schema": "edit3dv4.scene",
+ "version": "1.0",
+ "name": "架桥车前泵故障互救处置训练场景",
+ "presetId": "outdoor-1",
+ "type": "outdoor",
+ "environment": {
+ "presetId": "outdoor-1",
+ "type": "outdoor"
+ },
+ "rendering": {
+ "background": 11062241,
+ "fog": {
+ "color": 11062241,
+ "near": 70,
+ "far": 240
+ },
+ "toneMappingExposure": 1.05,
+ "environment": {
+ "type": "room",
+ "sigma": 0.04
+ },
+ "camera": {
+ "fov": 45,
+ "near": 0.1,
+ "far": 800,
+ "position": [
+ 10.6,
+ 6.4,
+ 13.2
+ ],
+ "target": [
+ 0,
+ 1.9,
+ 0
+ ]
+ }
+ },
+ "objects": [
+ {
+ "id": "front-pump-training-yard",
+ "sceneId": "front-pump-training-yard",
+ "interactionId": "scene:front-pump-training-yard",
+ "type": "imported",
+ "name": "野外山地训练场",
+ "semantic": "场景地形",
+ "assetRole": "environment",
+ "operable": false,
+ "visible": true,
+ "modelId": "front-pump-field-terrain",
+ "modelEditorAssetId": "front-pump-field-terrain",
+ "modelEditorModelId": "front-pump-field-terrain",
+ "modelEditorRevision": 1,
+ "modelOverrides": {},
+ "materialPreset": null,
+ "sourceName": "地形.glb",
+ "resourceUrl": "/models/地形.glb",
+ "assetKey": "",
+ "mimeType": "model/gltf-binary",
+ "sizeBytes": 10726472,
+ "fitToScene": true,
+ "fitSize": 140,
+ "groundLevelY": 0,
+ "position": [
+ 0,
+ 0,
+ 0
+ ],
+ "rotation": [
+ 0,
+ 0,
+ 0
+ ],
+ "scale": [
+ 1,
+ 1,
+ 1
+ ]
+ },
+ {
+ "id": "front-pump-rescue-vehicle",
+ "sceneId": "front-pump-rescue-vehicle",
+ "interactionId": "scene:front-pump-rescue-vehicle",
+ "type": "imported",
+ "name": "架桥车前泵互救操作整车模型",
+ "semantic": "实训装备",
+ "assetRole": "training-content",
+ "operable": true,
+ "visible": true,
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "modelId": "front-pump-step-vehicle",
+ "modelEditorAssetId": "front-pump-step-vehicle",
+ "modelEditorModelId": "front-pump-step-vehicle",
+ "modelEditorRevision": 1,
+ "modelOverrides": {},
+ "materialPreset": null,
+ "modelPartTargets": [
+ {
+ "interactionId": "front-pump-hydraulic-pump",
+ "sourceNodeName": "1液压泵",
+ "name": "液压泵",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-power-take-off",
+ "sourceNodeName": "2取力器",
+ "name": "取力器",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-pto-switch",
+ "sourceNodeName": "2取力器开关",
+ "name": "取力器开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-ball-valve-switch",
+ "sourceNodeName": "3球阀开关",
+ "name": "球阀开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-aux-control-box",
+ "sourceNodeName": "5辅助控制盒",
+ "name": "辅助控制盒",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-aux-control-box-cover",
+ "sourceNodeName": "5辅助控制盒盖",
+ "name": "辅助控制盒盖",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "pivotEdge": {
+ "space": "world",
+ "axis": [
+ 0,
+ 1,
+ 0
+ ],
+ "edge": "nearest-vertical-edge"
+ },
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ },
+ {
+ "interactionId": "front-pump-rescue-switch",
+ "sourceNodeName": "5互救开关",
+ "name": "互救开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "sceneId": "front-pump-rescue-vehicle",
+ "operable": true,
+ "visible": true
+ }
+ ],
+ "sourceName": "步骤模型.glb",
+ "resourceUrl": "/models/步骤模型.glb",
+ "assetKey": "",
+ "mimeType": "model/gltf-binary",
+ "sizeBytes": 32195288,
+ "fitToScene": true,
+ "fitSize": 8,
+ "position": [
+ 0,
+ 0,
+ 0
+ ],
+ "rotation": [
+ 0,
+ 0,
+ 0
+ ],
+ "scale": [
+ 1,
+ 1,
+ 1
+ ]
+ }
+ ]
+ },
+ "assetReferences": [
+ {
+ "resourceUrl": "/models/步骤模型.glb",
+ "sourceName": "步骤模型.glb",
+ "sizeBytes": 32195288,
+ "mimeType": "model/gltf-binary",
+ "assetRole": "training-content"
+ }
+ ]
+ },
+ "accent": "#2d86d8",
+ "background": 11062241,
+ "fog": 11062241,
+ "ground": 7110499,
+ "camera": {
+ "position": [
+ 10.6,
+ 6.4,
+ 13.2
+ ],
+ "target": [
+ 0,
+ 1.9,
+ 0
+ ]
+ },
+ "retainStoredParts": true,
+ "sceneTags": [
+ "真实整车步骤模型",
+ "前泵故障互救",
+ "机构顺序操作"
+ ],
+ "sensorFeeds": []
+ },
+ "tools": [],
+ "faults": [],
+ "steps": [
+ {
+ "id": "front-pump-step-01",
+ "code": "FRONT-PUMP-STEP-01",
+ "stepCode": "FRONT-PUMP-STEP-01",
+ "actionCode": "IDENTIFY_FAILED_HYDRAULIC_PUMP",
+ "number": 1,
+ "title": "根据故障现象确定是哪一个液压泵发生故障",
+ "targetId": "front-pump-hydraulic-pump",
+ "targetName": "液压泵",
+ "targetRef": {
+ "sceneId": "front-pump-rescue-vehicle",
+ "sceneObjectId": "front-pump-rescue-vehicle",
+ "interactionId": "front-pump-hydraulic-pump",
+ "sourceNodeName": "1液压泵",
+ "capabilities": [
+ "select",
+ "inspect"
+ ],
+ "visible": true
+ },
+ "mode": "inspect",
+ "interaction": "mechanism-operation",
+ "instruction": "根据故障现象确定是哪一个液压泵发生故障(注:模型_1液压泵标记出来。例如显红色)",
+ "guidance": "根据故障现象确定是哪一个液压泵发生故障(注:模型_1液压泵标记出来。例如显红色)",
+ "warning": "",
+ "destination": "液压泵",
+ "success": "根据故障现象确定是哪一个液压泵发生故障",
+ "cameraPolicy": "exterior",
+ "action": {
+ "kind": "mechanism-operation",
+ "targetId": "front-pump-hydraulic-pump",
+ "mode": "inspect",
+ "durationMs": 900,
+ "tolerance": 0,
+ "requiresPlacement": false,
+ "cameraPolicy": "exterior",
+ "mechanism": {
+ "type": "identify",
+ "axisSpace": "world",
+ "highlightColor": "#e53935",
+ "steps": [
+ {
+ "targetId": "front-pump-hydraulic-pump",
+ "axis": [
+ 0,
+ 1,
+ 0
+ ],
+ "toleranceDegrees": 6,
+ "directionHint": "点击并确认发生故障的液压泵"
+ }
+ ]
+ }
+ },
+ "toolId": "",
+ "targetNodeId": "front-pump-hydraulic-pump",
+ "targetNodeName": "1液压泵",
+ "measurement": "",
+ "audio": {
+ "narrationSource": "offline",
+ "narrationText": "根据故障现象确定是哪一个液压泵发生故障(注:模型_1液压泵标记出来。例如显红色)",
+ "narrationUrl": "",
+ "safetyText": "",
+ "safetyUrl": "",
+ "successCue": "ui",
+ "errorCue": "collision-error"
+ },
+ "faultIds": [],
+ "assessment": {
+ "weight": 20,
+ "requiredDimensions": [
+ "action",
+ "sequence"
+ ],
+ "passPolicy": "all-required"
+ }
+ },
+ {
+ "id": "front-pump-step-02",
+ "code": "FRONT-PUMP-STEP-02",
+ "stepCode": "FRONT-PUMP-STEP-02",
+ "actionCode": "DISENGAGE_POWER_TAKE_OFF",
+ "number": 2,
+ "title": "断开架桥车取力器",
+ "targetId": "front-pump-pto-switch",
+ "targetName": "取力器开关",
+ "targetRef": {
+ "sceneId": "front-pump-rescue-vehicle",
+ "sceneObjectId": "front-pump-rescue-vehicle",
+ "interactionId": "front-pump-pto-switch",
+ "sourceNodeName": "2取力器开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "visible": true
+ },
+ "mode": "inspect",
+ "interaction": "mechanism-operation",
+ "instruction": "断开架桥车取力器。(注:显示模型2取力器和2取力器开关,摇动模型2取力器开关。特别说明这个这边是早上随手补的不准确,车外看就行,不要拉近)",
+ "guidance": "断开架桥车取力器。(注:显示模型2取力器和2取力器开关,摇动模型2取力器开关。特别说明这个这边是早上随手补的不准确,车外看就行,不要拉近)",
+ "warning": "",
+ "destination": "取力器开关",
+ "success": "断开架桥车取力器",
+ "cameraPolicy": "exterior",
+ "action": {
+ "kind": "mechanism-operation",
+ "targetId": "front-pump-pto-switch",
+ "mode": "inspect",
+ "durationMs": 900,
+ "tolerance": 0,
+ "requiresPlacement": false,
+ "cameraPolicy": "exterior",
+ "mechanism": {
+ "type": "rotate",
+ "axisSpace": "world",
+ "visualTargetIds": [
+ "front-pump-power-take-off"
+ ],
+ "steps": [
+ {
+ "targetId": "front-pump-pto-switch",
+ "axis": [
+ 1,
+ 0,
+ 0
+ ],
+ "deltaDegrees": -30,
+ "toleranceDegrees": 6,
+ "directionHint": "向断开方向摇动"
+ }
+ ]
+ }
+ },
+ "toolId": "",
+ "targetNodeId": "front-pump-pto-switch",
+ "targetNodeName": "2取力器开关",
+ "measurement": "",
+ "audio": {
+ "narrationSource": "offline",
+ "narrationText": "断开架桥车取力器。(注:显示模型2取力器和2取力器开关,摇动模型2取力器开关。特别说明这个这边是早上随手补的不准确,车外看就行,不要拉近)",
+ "narrationUrl": "",
+ "safetyText": "",
+ "safetyUrl": "",
+ "successCue": "ui",
+ "errorCue": "collision-error"
+ },
+ "faultIds": [],
+ "assessment": {
+ "weight": 20,
+ "requiredDimensions": [
+ "action",
+ "sequence"
+ ],
+ "passPolicy": "all-required"
+ }
+ },
+ {
+ "id": "front-pump-step-03",
+ "code": "FRONT-PUMP-STEP-03",
+ "stepCode": "FRONT-PUMP-STEP-03",
+ "actionCode": "OPEN_RESCUE_BALL_VALVE",
+ "number": 3,
+ "title": "将架桥车备胎斜上方链接两个红色滤油器的球阀打开",
+ "targetId": "front-pump-ball-valve-switch",
+ "targetName": "球阀开关",
+ "targetRef": {
+ "sceneId": "front-pump-rescue-vehicle",
+ "sceneObjectId": "front-pump-rescue-vehicle",
+ "interactionId": "front-pump-ball-valve-switch",
+ "sourceNodeName": "3球阀开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "visible": true
+ },
+ "mode": "inspect",
+ "interaction": "mechanism-operation",
+ "instruction": "将架桥车备胎斜上方链接两个红色滤油器的球阀打开。(注;把模型3球阀开关由垂直与官道方向旋转至平行于管道方向)",
+ "guidance": "将架桥车备胎斜上方链接两个红色滤油器的球阀打开。(注;把模型3球阀开关由垂直与官道方向旋转至平行于管道方向)",
+ "warning": "",
+ "destination": "球阀开关",
+ "success": "将架桥车备胎斜上方链接两个红色滤油器的球阀打开",
+ "cameraPolicy": "focus",
+ "action": {
+ "kind": "mechanism-operation",
+ "targetId": "front-pump-ball-valve-switch",
+ "mode": "inspect",
+ "durationMs": 900,
+ "tolerance": 0,
+ "requiresPlacement": false,
+ "cameraPolicy": "focus",
+ "mechanism": {
+ "type": "rotate",
+ "axisSpace": "world",
+ "steps": [
+ {
+ "targetId": "front-pump-ball-valve-switch",
+ "axis": [
+ 0,
+ 1,
+ 0
+ ],
+ "deltaDegrees": 90,
+ "toleranceDegrees": 8,
+ "directionHint": "由垂直与官道方向旋转至平行于管道方向"
+ }
+ ]
+ }
+ },
+ "toolId": "",
+ "targetNodeId": "front-pump-ball-valve-switch",
+ "targetNodeName": "3球阀开关",
+ "measurement": "",
+ "audio": {
+ "narrationSource": "offline",
+ "narrationText": "将架桥车备胎斜上方链接两个红色滤油器的球阀打开。(注;把模型3球阀开关由垂直与官道方向旋转至平行于管道方向)",
+ "narrationUrl": "",
+ "safetyText": "",
+ "safetyUrl": "",
+ "successCue": "ui",
+ "errorCue": "collision-error"
+ },
+ "faultIds": [],
+ "assessment": {
+ "weight": 20,
+ "requiredDimensions": [
+ "action",
+ "sequence"
+ ],
+ "passPolicy": "all-required"
+ }
+ },
+ {
+ "id": "front-pump-step-04",
+ "code": "FRONT-PUMP-STEP-04",
+ "stepCode": "FRONT-PUMP-STEP-04",
+ "actionCode": "REENGAGE_POWER_TAKE_OFF",
+ "number": 4,
+ "title": "重新联通取力器",
+ "targetId": "front-pump-pto-switch",
+ "targetName": "取力器开关",
+ "targetRef": {
+ "sceneId": "front-pump-rescue-vehicle",
+ "sceneObjectId": "front-pump-rescue-vehicle",
+ "interactionId": "front-pump-pto-switch",
+ "sourceNodeName": "2取力器开关",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "visible": true
+ },
+ "mode": "inspect",
+ "interaction": "mechanism-operation",
+ "instruction": "重新联通取力器(注:把步骤2的2取力器开关摇回去:还是不要拉近)",
+ "guidance": "重新联通取力器(注:把步骤2的2取力器开关摇回去:还是不要拉近)",
+ "warning": "",
+ "destination": "取力器开关",
+ "success": "重新联通取力器",
+ "cameraPolicy": "exterior",
+ "action": {
+ "kind": "mechanism-operation",
+ "targetId": "front-pump-pto-switch",
+ "mode": "inspect",
+ "durationMs": 900,
+ "tolerance": 0,
+ "requiresPlacement": false,
+ "cameraPolicy": "exterior",
+ "mechanism": {
+ "type": "rotate",
+ "axisSpace": "world",
+ "visualTargetIds": [
+ "front-pump-power-take-off"
+ ],
+ "steps": [
+ {
+ "targetId": "front-pump-pto-switch",
+ "axis": [
+ 1,
+ 0,
+ 0
+ ],
+ "deltaDegrees": 30,
+ "toleranceDegrees": 6,
+ "directionHint": "按步骤2的相反方向摇回去"
+ }
+ ]
+ }
+ },
+ "toolId": "",
+ "targetNodeId": "front-pump-pto-switch",
+ "targetNodeName": "2取力器开关",
+ "measurement": "",
+ "audio": {
+ "narrationSource": "offline",
+ "narrationText": "重新联通取力器(注:把步骤2的2取力器开关摇回去:还是不要拉近)",
+ "narrationUrl": "",
+ "safetyText": "",
+ "safetyUrl": "",
+ "successCue": "ui",
+ "errorCue": "collision-error"
+ },
+ "faultIds": [],
+ "assessment": {
+ "weight": 20,
+ "requiredDimensions": [
+ "action",
+ "sequence"
+ ],
+ "passPolicy": "all-required"
+ }
+ },
+ {
+ "id": "front-pump-step-05",
+ "code": "FRONT-PUMP-STEP-05",
+ "stepCode": "FRONT-PUMP-STEP-05",
+ "actionCode": "ENABLE_AUXILIARY_RESCUE_SWITCH",
+ "number": 5,
+ "title": "打开辅助控制盒内的“互救”开关",
+ "targetId": "front-pump-aux-control-box-cover",
+ "targetName": "辅助控制盒盖",
+ "targetRef": {
+ "sceneId": "front-pump-rescue-vehicle",
+ "sceneObjectId": "front-pump-rescue-vehicle",
+ "interactionId": "front-pump-aux-control-box-cover",
+ "sourceNodeName": "5辅助控制盒盖",
+ "capabilities": [
+ "select",
+ "inspect",
+ "mechanism"
+ ],
+ "visible": true
+ },
+ "mode": "inspect",
+ "interaction": "mechanism-operation",
+ "instruction": "打开辅助控制盒内的“互救”开关(注:打开5辅助控制盒盖在5辅助控制盒面板上,把5互救开关往上搬)",
+ "guidance": "打开辅助控制盒内的“互救”开关(注:打开5辅助控制盒盖在5辅助控制盒面板上,把5互救开关往上搬)",
+ "warning": "",
+ "destination": "辅助控制盒盖",
+ "success": "打开辅助控制盒内的“互救”开关",
+ "cameraPolicy": "focus",
+ "action": {
+ "kind": "mechanism-operation",
+ "targetId": "front-pump-aux-control-box-cover",
+ "mode": "inspect",
+ "durationMs": 1600,
+ "tolerance": 0,
+ "requiresPlacement": false,
+ "cameraPolicy": "focus",
+ "mechanism": {
+ "type": "sequence",
+ "axisSpace": "world",
+ "visualTargetIds": [
+ "front-pump-aux-control-box"
+ ],
+ "steps": [
+ {
+ "targetId": "front-pump-aux-control-box-cover",
+ "axis": [
+ 0,
+ 1,
+ 0
+ ],
+ "deltaDegrees": 100,
+ "toleranceDegrees": 8,
+ "directionHint": "沿控制盒盖边缘铰链打开",
+ "pivotEdge": {
+ "space": "world",
+ "axis": [
+ 0,
+ 1,
+ 0
+ ],
+ "edge": "nearest-vertical-edge"
+ }
+ },
+ {
+ "targetId": "front-pump-rescue-switch",
+ "axis": [
+ 0,
+ 0,
+ 1
+ ],
+ "deltaDegrees": 30,
+ "toleranceDegrees": 6,
+ "directionHint": "往上搬"
+ }
+ ]
+ }
+ },
+ "toolId": "",
+ "targetNodeId": "front-pump-aux-control-box-cover",
+ "targetNodeName": "5辅助控制盒盖",
+ "measurement": "",
+ "audio": {
+ "narrationSource": "offline",
+ "narrationText": "打开辅助控制盒内的“互救”开关(注:打开5辅助控制盒盖在5辅助控制盒面板上,把5互救开关往上搬)",
+ "narrationUrl": "",
+ "safetyText": "",
+ "safetyUrl": "",
+ "successCue": "ui",
+ "errorCue": "collision-error"
+ },
+ "faultIds": [],
+ "assessment": {
+ "weight": 20,
+ "requiredDimensions": [
+ "action",
+ "sequence"
+ ],
+ "passPolicy": "all-required"
+ }
+ }
+ ],
+ "executionPlan": {
+ "schema": "digital-workshop.training-execution-plan",
+ "version": 1,
+ "entryNodeId": "front-pump-step-01",
+ "traversalOrder": [
+ "front-pump-step-01",
+ "front-pump-step-02",
+ "front-pump-step-03",
+ "front-pump-step-04",
+ "front-pump-step-05"
+ ]
+ },
+ "assessment": {
+ "passScore": 80,
+ "maxScore": 100,
+ "totalScore": 100,
+ "scoringMode": "weighted-rules",
+ "allowRetry": true,
+ "recordProcess": true
+ },
+ "runtime": {
+ "startMode": "practice",
+ "offline": true,
+ "teacherEnabled": true,
+ "teacherVoiceEnabled": true,
+ "actionSoundEnabled": true,
+ "showStepPanel": true,
+ "showProgress": true,
+ "allowReplay": true,
+ "language": "zh-CN"
+ }
+ }
+}
\ No newline at end of file
diff --git a/reports/front-pump-recovery-20260913/acceptance.json b/reports/front-pump-recovery-20260913/acceptance.json
new file mode 100644
index 0000000..789eebb
--- /dev/null
+++ b/reports/front-pump-recovery-20260913/acceptance.json
@@ -0,0 +1,39 @@
+{
+ "pass": true,
+ "stages": [
+ {
+ "step": 1,
+ "target": "front-pump-hydraulic-pump",
+ "status": "当前步骤操作正确,可以进入下一步"
+ },
+ {
+ "step": 2,
+ "target": "front-pump-pto-switch",
+ "status": "当前步骤操作正确,可以进入下一步"
+ },
+ {
+ "step": 3,
+ "target": "front-pump-ball-valve-switch",
+ "status": "当前步骤操作正确,可以进入下一步"
+ },
+ {
+ "step": 4,
+ "target": "front-pump-pto-switch",
+ "status": "当前步骤操作正确,可以进入下一步"
+ },
+ {
+ "step": 5,
+ "target": "front-pump-aux-control-box-cover",
+ "status": "当前步骤操作正确,可以进入下一步"
+ }
+ ],
+ "actualModels": 2,
+ "structureNodes": 44,
+ "originalBubbleFlow": true,
+ "twoActionsRequiredInStep5": true,
+ "restart": true,
+ "definitionDownload": true,
+ "errors": [],
+ "failed": [],
+ "api": []
+}
\ No newline at end of file
diff --git a/reports/front-pump-recovery-20260913/failure.json b/reports/front-pump-recovery-20260913/failure.json
new file mode 100644
index 0000000..ef1f954
--- /dev/null
+++ b/reports/front-pump-recovery-20260913/failure.json
@@ -0,0 +1,7 @@
+{
+ "message": "expect(received).toBe(expected) // Object.is equality\n\nExpected: \"front-pump-rescue-switch\"\nReceived: \"front-pump-aux-control-box-cover\"\n\nCall Log:\n- Timeout 5000ms exceeded while waiting on the predicate",
+ "text": "液压泵应急切换 · 原流程演示\nTASK-VIRTUAL-005 · 本地演示,不写入训练成绩\n重新开始\n下载原流程数据\n返回数字车间\n装备数字车间\n架桥车前泵故障互救处置训练(虚拟仿真)\n教员预览 · 前泵互救五步原流程 · 不计成绩\n本地静态演示\n训练模式\n过程记录\n29\n帮助说明\n退出训练\n任务步骤\n5 / 5\n根据故障现象确定是哪一个液压泵发生故障\n已完成\n断开架桥车取力器\n已完成\n将架桥车备胎斜上方链接两个红色滤油器的球阀打开\n已完成\n重新联通取力器\n已完成\n5\n打开辅助控制盒内的“互救”开关\n进行中\n三维实时交互\n结构树\n聚焦\n复位视角\n标签\n高亮问题设备\n隐藏其他\n爆炸视图\n展开 0%\n当前步骤\n第 5 步:打开辅助控制盒内的“互救”开关\n\n第 2/2 项:拖动互救开关,按往上搬旋转 30° 后释放。\n\n操作帮助\n判定详情\n中修 · 独立 3D 科目\n架桥车互救操作训练场\n前泵故障 · 待执行互救处置 · 已加载编辑场景 2 个对象\n真实整车步骤模型\n前泵故障互救\n机构顺序操作\n本地数字教员\n数字教员\n周教员 · 维修训练指导\n男性教员 · 本地离线讲解\n步骤讲解\n\n拖动高亮机构,按沿控制盒盖边缘铰链打开旋转 100°,到位后释放。系统将自动记录三维交互、顺序与判定证据。\n\n请确认目标零件为辅助控制盒盖,并按标准顺序操作。\n播放讲解\n自动语音\n提示音\n拖动旋转 · 互救开关\n装配位置\n操作状态\n请拖动高亮的互救开关旋转到 30°(2/2)\n规则演示\n操作演示\n完成训练\n已完成第 1 项,请继续操作互救开关\n上一步\n训练列表\n步骤演示\n复位视角",
+ "errors": [],
+ "failed": [],
+ "api": []
+}
\ No newline at end of file
diff --git a/reports/front-pump-recovery-20260913/failure.png b/reports/front-pump-recovery-20260913/failure.png
new file mode 100644
index 0000000..dfc62b7
Binary files /dev/null and b/reports/front-pump-recovery-20260913/failure.png differ
diff --git a/reports/front-pump-recovery-20260913/index.html b/reports/front-pump-recovery-20260913/index.html
new file mode 100644
index 0000000..1c702b9
--- /dev/null
+++ b/reports/front-pump-recovery-20260913/index.html
@@ -0,0 +1,7 @@
+前泵互救原流程恢复验收
+
+前泵互救原流程恢复验收
2026-09-13 · 本机前端 6180 · 独立静态演示,无登录账号、无数据库写入和正式成绩。
+找回内容
TASK-VIRTUAL-005 架桥车前泵故障互救处置训练:确认故障液压泵 → 断开取力器 → 打开互救球阀 → 重新联通取力器 → 打开辅助控制盒盖及互救开关。
五步原定义、步骤模型和地形模型与 yaozijun 的 ba747df 提交完全一致。1576728 将主列表切到正式接口,4cb8788 将旧兼容地址重定向,导致静态任务失去入口;原数据未删除。
打开恢复的演示 · 原流程 JSON · 机器验证结果
+已通过检查
- 真实整车和地形加载,44 个结构树节点,七个业务部件与 GLB 节点契约一致。
- 原静态气泡操作完成全部五步;第五步只完成盒盖时不能结束,继续操作互救开关后才可完成。
- 结构树、聚焦、标签、高亮、隐藏其他、爆炸与复位入口;重启回到第一步;原定义下载。
- 浏览器未出现脚本异常、HTTP 失败或教学 API 请求。
- 两个前端契约检查与生产构建通过。
气泡确认使用原静态到位逻辑。本次没有把此方式当作真实考核证据,也未运行正式教学接口写入测试。报告目录保留调试阶段的 failure 文件;最终结论以 acceptance.json 为准。
+恢复画面

完成五步
+
diff --git a/reports/front-pump-recovery-20260913/inspect.json b/reports/front-pump-recovery-20260913/inspect.json
new file mode 100644
index 0000000..823c3ad
--- /dev/null
+++ b/reports/front-pump-recovery-20260913/inspect.json
@@ -0,0 +1,451 @@
+{
+ "scene": {
+ "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.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "Cai1",
+ "color": "3d4b3b",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "HeiSe",
+ "color": "4f4f4f",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "JinShu",
+ "color": "929292",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "LanSe",
+ "color": "3b76d0",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "LvSe",
+ "color": "3d4b3b",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "Mat_black",
+ "color": "322e2e",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "Mat_supportBridgeBridge_01",
+ "color": "ffffff",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": true,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "Mat_supportBridgeBridge_02",
+ "color": "ffffff",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": true,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "Mat_supportBridge_05",
+ "color": "ffffff",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": true,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "Mat_white",
+ "color": "9da3a4",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "Material__1676",
+ "color": "d96e6e",
+ "metalness": 0.8,
+ "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.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": true,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "adskMatMat_supportBridge_05",
+ "color": "ffffff",
+ "metalness": 0.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": true,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "adskMatMat_white",
+ "color": "9da3a4",
+ "metalness": 0.8,
+ "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.8,
+ "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.8,
+ "roughness": 0.5,
+ "envMapIntensity": 1,
+ "hasMap": true,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "QianlvSe2",
+ "color": "749f62",
+ "metalness": 0.8,
+ "roughness": 0.6,
+ "envMapIntensity": 1,
+ "hasMap": false,
+ "preset": null,
+ "rule": null,
+ "authored": null
+ },
+ {
+ "name": "WaiQiang",
+ "color": "ffffff",
+ "metalness": 0.8,
+ "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": null,
+ "publishedContentMounted": true,
+ "publishedContentName": "架桥车前泵故障互救处置训练场景",
+ "publishedTrainingContent": {
+ "sceneId": "SCENE-FRONT-PUMP-RESCUE-01",
+ "objectCount": 2
+ },
+ "proceduralMachineVisible": false,
+ "publishedModelRootCount": 2,
+ "framing": {
+ "cameraPosition": [
+ 4.51,
+ 2.78,
+ 8.51
+ ],
+ "controlsTarget": [
+ -0.66,
+ 0.59,
+ 2.08
+ ],
+ "distance": 8.54,
+ "minDistance": 1.88,
+ "maxDistance": 24,
+ "fov": 45,
+ "near": 0.43,
+ "far": 300,
+ "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": 725,
+ "localY": 390,
+ "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,
+ 0,
+ -4
+ ],
+ "max": [
+ 0.92,
+ 1.97,
+ 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": "取力器开关"
+ }
+ ]
+ }
+ },
+ "text": "液压泵应急切换 · 原流程演示\nTASK-VIRTUAL-005 · 本地演示,不写入训练成绩\n重新开始\n下载原流程数据\n返回数字车间\n装备数字车间\n架桥车前泵故障互救处置训练(虚拟仿真)\n教员预览 · 前泵互救五步原流程 · 不计成绩\n本地静态演示\n训练模式\n过程记录\n1\n帮助说明\n退出训练\n任务步骤\n1 / 5\n1\n根据故障现象确定是哪一个液压泵发生故障\n进行中\n2\n断开架桥车取力器\n待进行\n3\n将架桥车备胎斜上方链接两个红色滤油器的球阀打开\n待进行\n4\n重新联通取力器\n待进行\n5\n打开辅助控制盒内的“互救”开关\n待进行\n三维实时交互\n结构树\n聚焦\n复位视角\n标签\n高亮问题设备\n隐藏其他\n爆炸视图\n展开 0%\n当前步骤\n第 1 步:根据故障现象确定是哪一个液压泵发生故障\n\n点击高亮的液压泵完成确认。\n\n操作帮助\n判定详情\n中修 · 独立 3D 科目\n架桥车互救操作训练场\n前泵故障 · 待执行互救处置 · 已加载编辑场景 2 个对象\n真实整车步骤模型\n前泵故障互救\n机构顺序操作\n本地数字教员\n数字教员\n周教员 · 维修训练指导\n男性教员 · 本地离线讲解\n步骤讲解\n\n点击高亮机构目标,完成识别确认。系统将自动记录三维交互、顺序与判定证据。\n\n请确认目标零件为液压泵,并按标准顺序操作。\n播放讲解\n自动语音\n提示音\n点击确认 · 液压泵\n装配位置\n操作状态\n请点击高亮的液压泵\n规则演示\n操作演示\n下一步\n位置不正确,请重新放置\n上一步\n训练列表\n步骤演示\n复位视角",
+ "errors": [],
+ "failed": [],
+ "api": []
+}
\ No newline at end of file
diff --git a/reports/front-pump-recovery-20260913/step-1.png b/reports/front-pump-recovery-20260913/step-1.png
new file mode 100644
index 0000000..b9415c2
Binary files /dev/null and b/reports/front-pump-recovery-20260913/step-1.png differ
diff --git a/reports/front-pump-recovery-20260913/step-2.png b/reports/front-pump-recovery-20260913/step-2.png
new file mode 100644
index 0000000..7232cc9
Binary files /dev/null and b/reports/front-pump-recovery-20260913/step-2.png differ
diff --git a/reports/front-pump-recovery-20260913/step-3.png b/reports/front-pump-recovery-20260913/step-3.png
new file mode 100644
index 0000000..44000d7
Binary files /dev/null and b/reports/front-pump-recovery-20260913/step-3.png differ
diff --git a/reports/front-pump-recovery-20260913/step-4.png b/reports/front-pump-recovery-20260913/step-4.png
new file mode 100644
index 0000000..ac1d600
Binary files /dev/null and b/reports/front-pump-recovery-20260913/step-4.png differ
diff --git a/reports/front-pump-recovery-20260913/step-5.png b/reports/front-pump-recovery-20260913/step-5.png
new file mode 100644
index 0000000..ff8aad9
Binary files /dev/null and b/reports/front-pump-recovery-20260913/step-5.png differ
diff --git a/tools/bridge-script-normal-live.mjs b/tools/bridge-script-normal-live.mjs
new file mode 100644
index 0000000..aff3eb2
--- /dev/null
+++ b/tools/bridge-script-normal-live.mjs
@@ -0,0 +1,149 @@
+import { chromium, expect } from '@playwright/test';
+import crypto from 'node:crypto';
+import fs from 'node:fs';
+import assert from 'node:assert/strict';
+import { login, call } from './role-permissions-live.mjs';
+const dir = 'reports/bridge-scripted-tasks-20260913';
+const admin = await login('root'), checks = [], fixtures = [], errors = [];
+const adminApi = async (url, data, method) => { const r = await call(url, admin.accessToken, data, method); assert.equal(r.status, 200, `${url}: ${r.message}`); return r.data; };
+const roles = await adminApi('auth/v1/roles/all'), role = (roles.records || roles).find(item => item.code === 'student');
+const suffix = Date.now().toString(36), password = 'Br!' + crypto.randomBytes(8).toString('hex');
+const created = await adminApi('auth/v1/users', {username: 'bridge_' + suffix, password, displayName: '桥梁脚本闭环验收', departmentId: '5', roleIds: [role.id], defaultRoleId: role.id, enabled: true, remark: '临时验收账号'});
+const userId = (created.user || created).id;
+const browser = await chromium.launch({channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader']});
+let page;
+try {
+ const signed = await call('auth/v1/auth/login', null, {userId, password, roleCode: 'student', rememberMe: false});
+ assert.equal(signed.status, 200);
+ const api = async (url, data) => { const r = await call(`tran/v1/${url}`, signed.data.accessToken, data); assert.equal(r.status, 200, `${url}: ${r.message}`); return r.data; };
+ const registered = JSON.parse(fs.readFileSync(`${dir}/registered.json`, 'utf8'));
+ const context = await browser.newContext({viewport: {width: 1600, height: 1000}});
+ await context.addInitScript(s => {
+ sessionStorage.setItem('unreal-tran:web:access-token:v1', s.accessToken);
+ sessionStorage.setItem('unreal-tran:web:refresh-token:v1', s.refreshToken);
+ localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({themeMode: 'military'}));
+ }, signed.data);
+ page = await context.newPage(); page.on('pageerror', error => errors.push(error.message));
+ page.on('response', async response => {
+ if (response.status() >= 400 && response.url().includes('/api/tran/')) {
+ const body = await response.json().catch(() => ({}));
+ console.log(JSON.stringify({path: new URL(response.url()).pathname, status: response.status(), message: body.message}));
+ }
+ });
+ await page.goto('http://127.0.0.1:6180/teaching/virtual-training');
+ await page.getByPlaceholder('请输入任务名称或编码').fill('TASK-VIRTUAL-00');
+ await page.getByPlaceholder('请输入任务名称或编码').press('Enter');
+ for (const item of registered) {
+ const card = page.getByTestId(`teaching-assignment-${item.code}`);
+ await expect(card).toBeVisible({timeout:30000});
+ await expect(card.getByRole('button',{name:'领取任务',exact:true})).toBeEnabled();
+ }
+ await expect(page.getByText('静态流程 · 不计成绩',{exact:true})).toHaveCount(0);
+ await page.screenshot({path:`${dir}/normal-task-list.png`});
+ checks.push('原四条任务保留正常领取入口,不显示静态演示或禁评分标签');
+ // Test copies keep the four delivery tasks free of test runs and test grades.
+ for (const item of [...registered.slice(1), registered[0]].filter(item => !process.env.BRIDGE_CODE || item.code === process.env.BRIDGE_CODE)) {
+ const original = await api(`teaching/assignments/${item.id}`);
+ assert.equal(original.executionMode, 'PRACTICE'); assert.equal(original.definitionSnapshot.runtime.definitionSource, 'SCRIPT');
+ let task = await adminApi('tran/v1/teaching/assignments', {code: `CHECK-${suffix}-${item.code}`, name: `验收·${item.name}`,
+ description: '脚本训练闭环自动化验证,完成后归档', channel: 'VIRTUAL', assignmentKind: 'TRAINING', executionMode: 'PRACTICE',
+ audienceType: 'COMMON', collaborationMode: 'INDIVIDUAL', digitalHumanAllowed: false, contentProjectId: item.projectId, contentVersionId: item.versionId, members: []});
+ const fixture = {taskId: task.id, code: item.code}; fixtures.push(fixture);
+ task = await adminApi(`tran/v1/teaching/assignments/${task.id}/publish`, {version: task.version});
+ let run = await api(`teaching/assignments/${task.id}/accept`, {assignmentVersion: task.version, teamCode: 'NEUTRAL', positionCode: 'LEARNER'});
+ fixture.runId = run.id;
+ await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/runs/${run.id}`);
+ const procedure = item.presentation === 'procedure';
+ if (procedure) await expect(page.locator('.bridge-procedure h2')).toContainText(item.name);
+ else await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/, {timeout: 120000});
+ run = await api(`teaching/runs/${run.id}`); assert.equal(run.status, 'IN_PROGRESS');
+ for (let i = 0; i < item.steps; i++) {
+ if (procedure) {
+ await expect(page.locator('.bridge-procedure-current > small')).toHaveText(`第 ${i + 1} / ${item.steps} 步`);
+ if (item.code === 'TASK-VIRTUAL-008' && [10,14].includes(i)) {
+ await page.getByRole('button', {name: '完成本步并保存', exact: true}).click();
+ await expect(page.locator('.bridge-procedure-progress')).toContainText(`已保存 ${i} /`);
+ await page.getByRole('textbox', {name: '操作记录'}).fill('验收记录:原稿相反指令已标记,连接状态与处理依据交教员核对。');
+ }
+ // A rejected save must leave both UI and server at the same step.
+ if (i === 0 && item.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});
+ await page.getByRole('button', {name:'完成本步并保存', exact:true}).click();
+ await expect(page.locator('.bridge-procedure-progress')).toContainText('已保存 0 /');
+ assert.equal((await api(`teaching/runs/${run.id}`)).progressPercent, 0);
+ checks.push('步骤保存失败不前进,允许幂等重试');
+ }
+ await page.getByRole('button', {name:'完成本步并保存', exact:true}).click();
+ await expect(page.getByRole('button', {name:'本步已保存', exact:true})).toBeDisabled();
+ } else {
+ await expect(page.locator('[data-role="step-count"]')).toHaveText(`${i + 1} / 5`);
+ await page.locator('[data-role="part-label"]').click();
+ if (i === 4) {
+ await expect(page.locator('[data-role="part-label"]')).toContainText('互救开关');
+ await page.locator('[data-role="part-label"]').click();
+ }
+ await expect(page.locator('[data-action="next"]')).toBeEnabled();
+ }
+ const saved = await api(`teaching/runs/${run.id}`);
+ assert.ok(saved.progressPercent > (i / item.steps) * 100 - 1);
+ const steps = await api(`teaching/runs/${run.id}/steps`);
+ assert.equal(steps.filter(s => s.statusCode === 'COMPLETED').length, i + 1);
+ if (i === 0) {
+ await page.reload();
+ if (procedure) await expect(page.locator('.bridge-procedure-progress')).toContainText('已保存 1 /');
+ else {
+ await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/, {timeout:120000});
+ await expect(page.locator('[data-role="step-count"]')).toHaveText('2 / 5');
+ 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();
+ }
+ checks.push(`${item.code} 刷新从服务端恢复已完成步骤`);
+ } else if (i < item.steps - 1) {
+ await (procedure ? page.getByRole('button', {name:'下一步', exact:true}) : page.locator('[data-action="next"]')).click();
+ }
+ }
+ await (procedure ? page.getByRole('button', {name:'提交训练', exact:true}) : page.locator('[data-action="next"]')).click();
+ await expect.poll(async () => (await api(`teaching/runs/${run.id}`)).status).toBe('SUBMITTED');
+ run = await api(`teaching/runs/${run.id}`); assert.equal(run.score, null);
+ await page.screenshot({path:`${dir}/normal-${item.code}-submitted.png`});
+ run = await adminApi(`tran/v1/teaching/runs/${run.id}/review`, {version:run.version,commandId:crypto.randomUUID(),score:85,passed:true,feedback:'自动化验收样例评分,验证教员评定链路',rubric:{testFixture:true}});
+ assert.equal(run.status,'REVIEWED'); assert.equal(run.score,85);
+ const visible = await api(`teaching/runs/${run.id}`); assert.equal(visible.score,85);
+ await page.reload();
+ await expect(page.locator('.virtual-server-status')).toContainText('教员评定:85 分');
+ if (!procedure) await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/, {timeout:120000});
+ await page.screenshot({path:`${dir}/normal-${item.code}-reviewed.png`});
+ const analytics = await adminApi(`tran/v1/teaching/analytics/records?userId=${userId}&channel=VIRTUAL&assignmentKind=TRAINING&size=100`);
+ assert.ok(analytics.records.some(r => String(r.runId || r.id) === run.id));
+ checks.push(`${item.code} ${item.steps} 步:领取、开始、保存、恢复、提交、教员85分评定、统计记录通过`);
+ await adminApi(`tran/v1/teaching/runs/${run.id}/archive`, {version:run.version});
+ task = await adminApi(`tran/v1/teaching/assignments/${task.id}`);
+ await adminApi(`tran/v1/teaching/assignments/${task.id}/archive`, {version:task.version, reason:'闭环验收完成,归档测试副本'});
+ fixture.archived = true;
+ fs.writeFileSync(`${dir}/normal-fixtures.json`,JSON.stringify(fixtures,null,2));
+ console.log(checks.at(-1));
+ }
+ assert.deepEqual(errors,[]);
+ fs.writeFileSync(`${dir}/normal-acceptance.json`,JSON.stringify({pass:true,checks,fixtures,errors},null,2));
+} catch (error) {
+ console.log('Visible feedback:', await page?.locator('.el-message').allTextContents());
+ await page?.screenshot({path:`${dir}/normal-failure.png`}).catch(()=>{});
+ fs.writeFileSync(`${dir}/normal-acceptance.json`,JSON.stringify({pass:false,error:error.message,checks,fixtures,errors},null,2));
+ throw error;
+} finally {
+ await browser.close();
+ for (const fixture of fixtures.filter(item => !item.archived)) {
+ if (fixture.runId) {
+ let run = await adminApi(`tran/v1/teaching/runs/${fixture.runId}`);
+ if (['IN_PROGRESS','ACCEPTED','PENDING'].includes(run.status)) run = await adminApi(`tran/v1/teaching/runs/${run.id}/terminate`, {version:run.version,commandId:crypto.randomUUID(),reason:'自动化验收中止,清理测试副本'});
+ if (run.status === 'SUBMITTED') run = await adminApi(`tran/v1/teaching/runs/${run.id}/review`, {version:run.version,commandId:crypto.randomUUID(),score:0,passed:false,feedback:'未完成的自动化验收副本'});
+ if (run.status === 'REVIEWED') await adminApi(`tran/v1/teaching/runs/${run.id}/archive`, {version:run.version});
+ }
+ const task = await adminApi(`tran/v1/teaching/assignments/${fixture.taskId}`);
+ await adminApi(`tran/v1/teaching/assignments/${task.id}/archive`, {version:task.version,reason:'自动化测试副本归档'});
+ fixture.archived = true;
+ }
+ fs.appendFileSync(`${dir}/normal-attempts.jsonl`, JSON.stringify({time:new Date().toISOString(),fixtures})+'\n');
+ const user = await adminApi(`auth/v1/users/${userId}`);
+ await adminApi(`auth/v1/users/${userId}`, {version:user.version}, 'DELETE');
+}
diff --git a/tools/bridge-script-preview-live.mjs b/tools/bridge-script-preview-live.mjs
new file mode 100644
index 0000000..5c518b7
--- /dev/null
+++ b/tools/bridge-script-preview-live.mjs
@@ -0,0 +1,27 @@
+import { chromium, expect } from '@playwright/test';
+import fs from 'node:fs';
+import assert from 'node:assert/strict';
+import { login, call } from './role-permissions-live.mjs';
+const dir='reports/bridge-scripted-tasks-20260913', session=await login('root');
+const tasks=JSON.parse(fs.readFileSync(`${dir}/registered.json`,'utf8')), checks=[], writes=[];
+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),session);
+ page.on('request',r=>{if(r.method()!=='GET'&&new URL(r.url()).pathname.startsWith('/api/tran/'))writes.push(new URL(r.url()).pathname)});
+ for(const task of tasks){
+ await page.goto('http://127.0.0.1:6180'+task.previewUrl);
+ const procedure=task.presentation==='procedure';
+ if(procedure)await expect(page.locator('.bridge-procedure h2')).toHaveText(task.name);
+ else await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:120000});
+ const response=page.waitForResponse(r=>r.url().includes(`/assignments/${task.id}/preview/validate`));
+ await (procedure?page.getByRole('button',{name:'完成本步并保存',exact:true}):page.locator('[data-role="part-label"]')).click();
+ assert.equal((await response).status(),200);
+ const runs=await call(`tran/v1/teaching/runs?channel=VIRTUAL&assignmentKind=TRAINING&assignmentId=${task.id}&size=100`,session.accessToken);
+ assert.equal(runs.status,200);assert.equal(runs.data.total,0);
+ checks.push(`${task.code} 教员预览使用服务端校验,未创建训练运行`);
+ }
+ assert.ok(writes.every(path=>path.endsWith('/preview/validate')));
+ fs.writeFileSync(`${dir}/normal-preview.json`,JSON.stringify({pass:true,checks,writes},null,2));
+ console.log('PASS: four previews validate without writing learner progress or scores');
+} finally {await browser.close()}
diff --git a/tools/bridge-scripted-student-live.mjs b/tools/bridge-scripted-student-live.mjs
new file mode 100644
index 0000000..15f73e7
--- /dev/null
+++ b/tools/bridge-scripted-student-live.mjs
@@ -0,0 +1,2 @@
+// Student permissions and execution are covered by the normal lifecycle acceptance.
+import './bridge-script-normal-live.mjs';
diff --git a/tools/bridge-scripted-tasks-live.mjs b/tools/bridge-scripted-tasks-live.mjs
new file mode 100644
index 0000000..3e0cea5
--- /dev/null
+++ b/tools/bridge-scripted-tasks-live.mjs
@@ -0,0 +1,2 @@
+// Compatibility entry: scripted tasks use the normal graded training lifecycle.
+import './bridge-script-normal-live.mjs';
diff --git a/tools/front-pump-recovery-check.mjs b/tools/front-pump-recovery-check.mjs
new file mode 100644
index 0000000..3b9c78b
--- /dev/null
+++ b/tools/front-pump-recovery-check.mjs
@@ -0,0 +1,3 @@
+// The recovered flow now enters through authenticated database tasks.
+// Run register-bridge-scripted-tasks.mjs once before this compatibility test.
+import './bridge-scripted-tasks-live.mjs';
diff --git a/tools/register-bridge-scripted-tasks.mjs b/tools/register-bridge-scripted-tasks.mjs
new file mode 100644
index 0000000..bffa6df
--- /dev/null
+++ b/tools/register-bridge-scripted-tasks.mjs
@@ -0,0 +1,59 @@
+import fs from 'node:fs';
+import assert from 'node:assert/strict';
+import { login, call } from './role-permissions-live.mjs';
+import { scripts, scriptDefinition } from '../../unreal_tran_web/src/features/virtual-training-scripts/definition.js';
+
+// Explicitly run to register/migrate these four tasks. Existing task codes and IDs are preserved.
+const session = await login('root');
+const api = async (endpoint, body, method) => {
+ const r = await call(`tran/v1/${endpoint}`, session.accessToken, body, method);
+ assert.equal(r.status, 200, `${endpoint}: ${r.message}`); return r.data;
+};
+const registered = [];
+for (const script of scripts) {
+ 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');
+ let detail = task ? await api(`content/projects/${task.contentProjectId}`) : null;
+ if (!detail) {
+ const found = await api(`content/projects?type=TRAINING&keyword=${encodeURIComponent(script.title)}&size=100`);
+ for (const candidate of found.records) {
+ const item = await api(`content/projects/${candidate.id}`);
+ if (item.currentVersion.content?.runtime?.scriptId === script.id) { detail = item; break; }
+ }
+ }
+ 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 作为流程来源,恢复普通训练与成绩评定' });
+ const projectCommand = { type: 'TRAINING', name: script.title, description: `原稿:${script.source}`,
+ categoryCode: 'MAINTENANCE', trainingMode: 'VIRTUAL', content: scriptDefinition(script), dependencies: [], assets: [],
+ changeSummary: '脚本来源 v2:复用正式教学运行、步骤记录与成绩评定' };
+ if (!detail) detail = await api('content/projects', projectCommand);
+ else if (detail.currentVersion.content?.runtime?.definitionSource !== 'SCRIPT'
+ || detail.currentVersion.content?.runtime?.scriptVersion !== script.version) {
+ detail = await api(`content/projects/${detail.project.id}`, { ...projectCommand, version: detail.project.version }, 'PUT');
+ }
+ let project = detail.project;
+ if (project.publishedVersionId !== detail.currentVersion.id) {
+ project = (await api(`content/projects/${project.id}/publish`, { version: project.version })).project;
+ }
+ const command = { code: script.code, name: script.title, description: `原稿:${script.source}`,
+ channel: 'VIRTUAL', assignmentKind: 'TRAINING', executionMode: 'PRACTICE', audienceType: 'COMMON',
+ collaborationMode: 'INDIVIDUAL', digitalHumanAllowed: false, contentProjectId: project.id,
+ contentVersionId: project.publishedVersionId, members: [] };
+ if (!task) task = await api('teaching/assignments', command);
+ else if (!correct || task.contentVersionId !== project.publishedVersionId) {
+ assert.ok(['DRAFT', 'WITHDRAWN'].includes(task.status));
+ task = await api(`teaching/assignments/${task.id}`, { ...command, version: task.version }, 'PUT');
+ }
+ if (task.status !== 'PUBLISHED') task = await api(`teaching/assignments/${task.id}/publish`, { version: task.version });
+ assert.equal(task.executionMode, 'PRACTICE');
+ assert.equal(task.definitionSnapshot.runtime.definitionSource, 'SCRIPT');
+ registered.push({ code: script.code, id: task.id, projectId: project.id, versionId: task.contentVersionId,
+ name: script.title, scriptId: script.id, scriptVersion: script.version, steps: script.steps.length,
+ previewUrl: `/teaching/virtual-training/preview/${task.id}`, presentation: script.presentation });
+ 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));