Sfoglia il codice sorgente

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

main
leiyun 6 giorni fa
parent
commit
77fa68b5ef
50 ha cambiato i file con 2086 aggiunte e 0 eliminazioni
  1. BIN
     
  2. BIN
     
  3. BIN
     
  4. BIN
     
  5. BIN
     
  6. BIN
     
  7. BIN
     
  8. BIN
     
  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. BIN
     
  13. BIN
     
  14. BIN
     
  15. BIN
     
  16. BIN
     
  17. BIN
     
  18. BIN
     
  19. BIN
     
  20. +42
    -0
      reports/bridge-scripted-tasks-20260913/normal-acceptance.json
  21. +7
    -0
      reports/bridge-scripted-tasks-20260913/normal-attempts.jsonl
  22. BIN
     
  23. +26
    -0
      reports/bridge-scripted-tasks-20260913/normal-fixtures.json
  24. +15
    -0
      reports/bridge-scripted-tasks-20260913/normal-preview.json
  25. BIN
     
  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. BIN
     
  31. BIN
     
  32. BIN
     
  33. BIN
     
  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. BIN
     
  38. +7
    -0
      reports/front-pump-recovery-20260913/index.html
  39. +451
    -0
      reports/front-pump-recovery-20260913/inspect.json
  40. BIN
     
  41. BIN
     
  42. BIN
     
  43. BIN
     
  44. BIN
     
  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

BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


+ 51
- 0
reports/bridge-scripted-tasks-20260913/acceptance.json Vedi File

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

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

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

BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


+ 42
- 0
reports/bridge-scripted-tasks-20260913/normal-acceptance.json Vedi File

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

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

BIN
Vedi File


+ 26
- 0
reports/bridge-scripted-tasks-20260913/normal-fixtures.json Vedi File

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

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

BIN
Vedi File


+ 50
- 0
reports/bridge-scripted-tasks-20260913/registered.json Vedi File

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

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

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

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

BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


+ 1072
- 0
reports/front-pump-recovery-20260913/TASK-VIRTUAL-005-front-pump-original.json
File diff soppresso perché troppo grande
Vedi File


+ 39
- 0
reports/front-pump-recovery-20260913/acceptance.json Vedi File

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

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

BIN
Vedi File


+ 7
- 0
reports/front-pump-recovery-20260913/index.html Vedi File

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

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

BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


BIN
Vedi File


+ 149
- 0
tools/bridge-script-normal-live.mjs Vedi File

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

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

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

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

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

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

Caricamento…
Annulla
Salva