Просмотр исходного кода

test(teaching): 验证桥梁脚本训练和教员预览闭环

main
leiyun 6 дней назад
Родитель
Сommit
77fa68b5ef
50 измененных файлов: 2086 добавлений и 0 удалений
  1. Двоичные данные
     
  2. Двоичные данные
     
  3. Двоичные данные
     
  4. Двоичные данные
     
  5. Двоичные данные
     
  6. Двоичные данные
     
  7. Двоичные данные
     
  8. Двоичные данные
     
  9. +51
    -0
      reports/bridge-scripted-tasks-20260913/acceptance.json
  10. +11
    -0
      reports/bridge-scripted-tasks-20260913/idempotence.json
  11. +24
    -0
      reports/bridge-scripted-tasks-20260913/index.html
  12. Двоичные данные
     
  13. Двоичные данные
     
  14. Двоичные данные
     
  15. Двоичные данные
     
  16. Двоичные данные
     
  17. Двоичные данные
     
  18. Двоичные данные
     
  19. Двоичные данные
     
  20. +42
    -0
      reports/bridge-scripted-tasks-20260913/normal-acceptance.json
  21. +7
    -0
      reports/bridge-scripted-tasks-20260913/normal-attempts.jsonl
  22. Двоичные данные
     
  23. +26
    -0
      reports/bridge-scripted-tasks-20260913/normal-fixtures.json
  24. +15
    -0
      reports/bridge-scripted-tasks-20260913/normal-preview.json
  25. Двоичные данные
     
  26. +50
    -0
      reports/bridge-scripted-tasks-20260913/registered.json
  27. +15
    -0
      reports/bridge-scripted-tasks-20260913/registration-cleanup.json
  28. +20
    -0
      reports/bridge-scripted-tasks-20260913/run-list-performance.json
  29. +7
    -0
      reports/bridge-scripted-tasks-20260913/student-acceptance.json
  30. Двоичные данные
     
  31. Двоичные данные
     
  32. Двоичные данные
     
  33. Двоичные данные
     
  34. +1072
    -0
      reports/front-pump-recovery-20260913/TASK-VIRTUAL-005-front-pump-original.json
  35. +39
    -0
      reports/front-pump-recovery-20260913/acceptance.json
  36. +7
    -0
      reports/front-pump-recovery-20260913/failure.json
  37. Двоичные данные
     
  38. +7
    -0
      reports/front-pump-recovery-20260913/index.html
  39. +451
    -0
      reports/front-pump-recovery-20260913/inspect.json
  40. Двоичные данные
     
  41. Двоичные данные
     
  42. Двоичные данные
     
  43. Двоичные данные
     
  44. Двоичные данные
     
  45. +149
    -0
      tools/bridge-script-normal-live.mjs
  46. +27
    -0
      tools/bridge-script-preview-live.mjs
  47. +2
    -0
      tools/bridge-scripted-student-live.mjs
  48. +2
    -0
      tools/bridge-scripted-tasks-live.mjs
  49. +3
    -0
      tools/front-pump-recovery-check.mjs
  50. +59
    -0
      tools/register-bridge-scripted-tasks.mjs

Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


+ 51
- 0
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": []
}

+ 11
- 0
reports/bridge-scripted-tasks-20260913/idempotence.json Просмотреть файл

@@ -0,0 +1,11 @@
{
"pass": true,
"before": {
"projects": 37,
"tasks": 29
},
"after": {
"projects": 37,
"tasks": 29
}
}

+ 24
- 0
reports/bridge-scripted-tasks-20260913/index.html Просмотреть файл

@@ -0,0 +1,24 @@
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>桥梁脚本普通训练闭环验收</title>
<style>body{font:16px/1.7 system-ui,"Microsoft YaHei";margin:0;background:#edf4f2;color:#173f36}main{max-width:1200px;margin:auto;padding:28px}section{background:white;border:1px solid #cce0da;border-radius:10px;padding:20px;margin:18px 0}h1{font-size:28px}h2{font-size:20px}table{border-collapse:collapse;width:100%}td,th{border:1px solid #d7e5e0;padding:10px;text-align:left}th{background:#eef6f3}a{color:#087b65}img{width:100%;height:auto;border:1px solid #d7e5e0}.warning{background:#fff5e6;padding:12px;border-left:4px solid #ba7b17}code{background:#f0f5f3;padding:2px 4px}</style>
<main><h1>桥梁脚本普通训练闭环验收</h1><p>2026-09-13 · 本机开发环境 · 修正“静态流程不计成绩”的错误设计。</p>
<section><h2>统一规则</h2><p>静态 JS 只提供步骤及交互定义。任务使用现有接口与数据库,领取、开始、准备记录、步骤保存、提交、教员评定及统计均与其他普通训练一致。学员训练保存成绩;教员预览按现有规则不写入学员成绩。</p>
<table><tr><th>任务 / ID</th><th>流程</th><th>固定版本</th><th>验收</th></tr>
<tr><td>TASK-VIRTUAL-005 / 86</td><td>液压泵应急切换,5步</td><td>515</td><td>五步六次机构操作、恢复、提交、评分、统计通过</td></tr>
<tr><td>TASK-VIRTUAL-006 / 87</td><td>液压泵应急替代,7步</td><td>516</td><td>全部步骤、保存失败重试、恢复、提交、评分、统计通过</td></tr>
<tr><td>TASK-VIRTUAL-007 / 88</td><td>前泵换件修复,11步</td><td>517</td><td>全部步骤、恢复、提交、评分、统计通过</td></tr>
<tr><td>TASK-VIRTUAL-008 / 89</td><td>平衡阀拆拼互换,16步</td><td>518</td><td>全部步骤、必填核对记录、恢复、提交、评分、统计通过</td></tr></table>
<p>原任务编号和 ID 保留,当前均为 PUBLISHED / PRACTICE / COMMON;runtime.definitionSource=SCRIPT,scriptVersion="2"。版本通过 HTTP 内容修订及教学任务更新生成,没有直接执行 SQL。</p>
<p>入口:虚实训练 → 虚拟仿真 → 领取任务 / 进入任务。已评定运行再次进入可查看教员评分与反馈。原四条任务没有写入测试成绩,可正常领取。</p></section>
<section><h2>验证证据</h2><ul>
<li>前端 pnpm run build 通过,包括 vue-tsc 与发布资源校验。</li>
<li>后端 Java 17 mvn test 通过;新增后续脚本操作证据检查后,ScriptedTrainingContractTest 与 VirtualRunContractTest 共8项通过。</li>
<li>四个任务副本39步均经过浏览器实际交互,API创建运行、保存步骤、刷新恢复、提交;教员API评定85分,学员可读且统计可查询。85分只属于自动化测试副本。</li>
<li>模拟progress返回503时不推进,重试成功;平衡阀两处待核对步骤无记录不能推进。</li>
<li>四个原任务的教员预览调用preview/validate,未创建学员运行;右侧结构树、聚焦、高亮、标签、隐藏与爆炸工具保留。</li></ul>
<p><a href="normal-acceptance.json">正式训练整轮验收</a> · <a href="normal-preview.json">教员预览隔离验收</a> · <a href="registered.json">四条任务登记</a> · <a href="normal-attempts.jsonl">测试尝试与副本归档</a> · <a href="build.log">前端构建日志</a></p>
<p>测试副本任务90~105及其运行85~100均已归档,临时学员账号已删除;保留归档记录供审计。任务90/运行85为首次失败清理,后续尝试详见记录文件。</p>
<div class="warning">三套后续流程目前提供脚本步骤操作与记录,对应三维拆装、软管和加油动画尚未制作。平衡阀原稿第11、15步有相反指令,必须记录依据交教员核对,不自动认定机械操作正确。一次与本地构建并行的验收遇到认证服务503,重新独立执行完整回归通过;不能据此宣称系统所有历史访问异常已解决。</div>
<p>此前 acceptance.json、student-acceptance.json 和旧截图为已被本页替代的历史演示验收,不代表当前评分规则。</p></section>
<section><h2>原四条任务的正常入口</h2><img src="normal-task-list.png" alt="正常领取入口"></section>
<section><h2>三维脚本提交与评定</h2><img src="normal-TASK-VIRTUAL-005-submitted.png" alt="原五步三维操作已提交"><img src="normal-TASK-VIRTUAL-005-reviewed.png" alt="教员85分测试评定"></section>
<section><h2>其他三套普通训练</h2><img src="normal-TASK-VIRTUAL-006-reviewed.png" alt="液压泵应急替代评定"><img src="normal-TASK-VIRTUAL-007-reviewed.png" alt="前泵换件修复评定"><img src="normal-TASK-VIRTUAL-008-reviewed.png" alt="平衡阀拆拼互换评定"></section></main></html>

Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


+ 42
- 0
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": []
}

+ 7
- 0
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}]}

Двоичные данные
Просмотреть файл


+ 26
- 0
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
}
]

+ 15
- 0
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"
]
}

Двоичные данные
Просмотреть файл


+ 50
- 0
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"
}
]

+ 15
- 0
reports/bridge-scripted-tasks-20260913/registration-cleanup.json Просмотреть файл

@@ -0,0 +1,15 @@
{
"removedUnreferencedVerificationDuplicates": [
437,
442,
443,
444,
445
],
"retainedProjects": [
438,
439,
440,
441
]
}

+ 20
- 0
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
}
]
}

+ 7
- 0
reports/bridge-scripted-tasks-20260913/student-acceptance.json Просмотреть файл

@@ -0,0 +1,7 @@
{
"pass": true,
"visibleScripts": 4,
"ungradedMetadataPreserved": true,
"studentCanEnter": true,
"anonymousRejected": true
}

Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


+ 1072
- 0
reports/front-pump-recovery-20260913/TASK-VIRTUAL-005-front-pump-original.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 39
- 0
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": []
}

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

Двоичные данные
Просмотреть файл


+ 7
- 0
reports/front-pump-recovery-20260913/index.html Просмотреть файл

@@ -0,0 +1,7 @@
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>前泵互救原流程恢复验收</title>
<style>body{max-width:1200px;margin:32px auto;padding:0 20px;background:#f4f8f7;color:#153f39;font:16px/1.7 system-ui,sans-serif}section{background:white;padding:24px;margin:18px 0;border:1px solid #c8ddd7;border-radius:10px}a{color:#07836b}img{max-width:100%;border:1px solid #c8ddd7}code{background:#edf4f1;padding:2px 5px}</style>
<h1>前泵互救原流程恢复验收</h1><p>2026-09-13 · 本机前端 6180 · 独立静态演示,无登录账号、无数据库写入和正式成绩。</p>
<section><h2>找回内容</h2><p><code>TASK-VIRTUAL-005</code> 架桥车前泵故障互救处置训练:确认故障液压泵 → 断开取力器 → 打开互救球阀 → 重新联通取力器 → 打开辅助控制盒盖及互救开关。</p><p>五步原定义、步骤模型和地形模型与 yaozijun 的 <code>ba747df</code> 提交完全一致。<code>1576728</code> 将主列表切到正式接口,<code>4cb8788</code> 将旧兼容地址重定向,导致静态任务失去入口;原数据未删除。</p><p><a href="http://127.0.0.1:6180/front-pump-demo.html">打开恢复的演示</a> · <a href="TASK-VIRTUAL-005-front-pump-original.json">原流程 JSON</a> · <a href="acceptance.json">机器验证结果</a></p></section>
<section><h2>已通过检查</h2><ul><li>真实整车和地形加载,44 个结构树节点,七个业务部件与 GLB 节点契约一致。</li><li>原静态气泡操作完成全部五步;第五步只完成盒盖时不能结束,继续操作互救开关后才可完成。</li><li>结构树、聚焦、标签、高亮、隐藏其他、爆炸与复位入口;重启回到第一步;原定义下载。</li><li>浏览器未出现脚本异常、HTTP 失败或教学 API 请求。</li><li>两个前端契约检查与生产构建通过。</li></ul><p>气泡确认使用原静态到位逻辑。本次没有把此方式当作真实考核证据,也未运行正式教学接口写入测试。报告目录保留调试阶段的 failure 文件;最终结论以 acceptance.json 为准。</p></section>
<section><h2>恢复画面</h2><img src="01-loaded.png" alt="原五步、整车和数字教员"><h2>完成五步</h2><img src="03-complete.png" alt="五步静态演示完成"></section>
</html>

+ 451
- 0
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": []
}

Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


Двоичные данные
Просмотреть файл


+ 149
- 0
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');
}

+ 27
- 0
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()}

+ 2
- 0
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';

+ 2
- 0
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';

+ 3
- 0
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';

+ 59
- 0
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));

Загрузка…
Отмена
Сохранить