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。

+

入口:虚实训练 → 虚拟仿真 → 领取任务 / 进入任务。已评定运行再次进入可查看教员评分与反馈。原四条任务没有写入测试成绩,可正常领取。

+

验证证据

+

正式训练整轮验收 · 教员预览隔离验收 · 四条任务登记 · 测试尝试与副本归档 · 前端构建日志

+

测试副本任务90~105及其运行85~100均已归档,临时学员账号已删除;保留归档记录供审计。任务90/运行85为首次失败清理,后续尝试详见记录文件。

+
三套后续流程目前提供脚本步骤操作与记录,对应三维拆装、软管和加油动画尚未制作。平衡阀原稿第11、15步有相反指令,必须记录依据交教员核对,不自动认定机械操作正确。一次与本地构建并行的验收遇到认证服务503,重新独立执行完整回归通过;不能据此宣称系统所有历史访问异常已解决。
+

此前 acceptance.json、student-acceptance.json 和旧截图为已被本页替代的历史演示验收,不代表当前评分规则。

+

原四条任务的正常入口

正常领取入口
+

三维脚本提交与评定

原五步三维操作已提交教员85分测试评定
+

其他三套普通训练

液压泵应急替代评定前泵换件修复评定平衡阀拆拼互换评定
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 · 机器验证结果

+

已通过检查

气泡确认使用原静态到位逻辑。本次没有把此方式当作真实考核证据,也未运行正式教学接口写入测试。报告目录保留调试阶段的 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));