Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

92 řádky
9.3 KiB

  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import assert from 'node:assert/strict';
  5. import {chromium} from '@playwright/test';
  6. import {login,call} from './role-permissions-live.mjs';
  7. import {scriptTrainingDefinition} from '../../unreal_tran_web/src/features/virtual-training-scripts/registry.js';
  8. import {collectActions,applyActionBindings} from '../../unreal_tran_web/src/features/content-production/document.js';
  9. const dir='reports/bridge-content-authoring-20260924';fs.mkdirSync(dir,{recursive:true});
  10. const session=await login('root');
  11. const api=async(ep,body,method)=>{const r=await call('tran/v1/'+ep,session.accessToken,body,method);assert.equal(r.status,200,ep+': '+r.message);return r.data;};
  12. const write=(name,value)=>fs.writeFileSync(`${dir}/${name}.json`,JSON.stringify(value,null,2));
  13. const read=name=>JSON.parse(fs.readFileSync(`${dir}/${name}.json`,'utf8'));
  14. const backup=(name,value)=>{if(!fs.existsSync(`${dir}/${name}.json`))write(name,value);};
  15. const fixture=fs.existsSync(`${dir}/fixture.json`)?read('fixture'):{items:[]};
  16. const stripVolatile=t=>{const d=structuredClone(t);delete d.serverNow;return d;};
  17. const bytesAsset=(object,index)=>{const bytes=fs.readFileSync(path.resolve('../unreal_tran_web/public',object.resourceUrl.slice(1)));return {code:`PRODUCTION-MODEL-${index+1}`,name:object.sourceName||object.name,type:'MODEL_FILE',storageUri:object.resourceUrl,mimeType:'model/gltf-binary',sizeBytes:bytes.length,sha256:crypto.createHash('sha256').update(bytes).digest('hex'),metadata:{builtin:true},status:'READY',sortOrder:index};};
  18. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  19. try{
  20. for(const taskId of [86,87,88,89]){
  21. const task=await api('teaching/assignments/'+taskId);backup('before-task-'+taskId,task);
  22. const original=await api('content/projects/'+task.contentProjectId);backup('before-project-'+task.contentProjectId,original);
  23. let item=fixture.items.find(i=>i.taskId===String(taskId));
  24. if(item?.complete){console.log('Already prepared',item.taskCode);continue;}
  25. const definition=scriptTrainingDefinition(task);
  26. if(taskId===86){const pilot=await api('teaching/assignments/116');backup('before-task-116',pilot);definition.scenario.sceneResource=structuredClone(pilot.definitionSnapshot.scenario.sceneResource);delete definition.runtime.definitionSource;delete definition.runtime.scriptId;delete definition.runtime.scriptVersion;}
  27. definition.name=task.name;definition.task.title=task.name;definition.introduction=structuredClone(task.definitionSnapshot.introduction);
  28. for(const step of definition.steps)step.interaction||='rule-check';
  29. let actions=collectActions(definition);
  30. const page=await browser.newPage({viewport:{width:1500,height:1000}});
  31. await page.goto('http://127.0.0.1:6180/');
  32. const captured=await page.evaluate(async({definition,actions})=>{
  33. const host=document.createElement('div');host.style.cssText='position:fixed;inset:0;background:white;z-index:100000';document.body.append(host);
  34. const {createProductionPreview}=await import('/src/features/content-production/preview.js');
  35. const viewer=await createProductionPreview(host,definition);
  36. try{return {actions:viewer.configure(actions)};}finally{viewer.destroy();host.remove();}
  37. },{definition,actions});
  38. actions=captured.actions;await page.close();
  39. assert.equal(actions.length,definition.steps.reduce((n,s)=>n+s.action.mechanism.steps.length,0));
  40. assert.ok(actions.every(a=>a.binding?.sourceNode&&a.motion));
  41. write('actions-'+task.code,actions);
  42. const snapshot=structuredClone(definition.scenario.sceneResource.snapshot);
  43. const assets=snapshot.objects.map(bytesAsset);
  44. const resource=snapshot.objects.find(o=>o.assetRole!=='environment'&&!/地形|terrain/i.test(o.sourceName));assert.ok(resource);
  45. const primaryAsset=assets[snapshot.objects.indexOf(resource)];
  46. const production={schemaVersion:1,taskCode:task.code,taskId:String(taskId),trainingProjectId:task.contentProjectId,
  47. sourceContentVersionId:task.contentVersionId,scriptId:task.definitionSnapshot.runtime.scriptId,scriptVersion:task.definitionSnapshot.runtime.scriptVersion};
  48. const modelName=task.name+'机构模型',sceneName=task.name+'训练场景';
  49. if(!item){item={taskId:String(taskId),taskCode:task.code,trainingProjectId:task.contentProjectId};fixture.items.push(item);write('fixture',fixture);}
  50. const modelContent={schema:'digital-twin-editor-project',version:4,name:modelName,modelId:resource.modelId||resource.id,
  51. modelResource:{...structuredClone(resource),id:resource.modelId||resource.id,assetCode:primaryAsset.code,storageUri:primaryAsset.storageUri,kind:'builtin'},
  52. camera:{position:[4,3,5],target:[0,1,0]},model:null,environment:null,
  53. timeline:{version:1,duration:Math.max(1,actions.length),currentTime:0,loop:false,zoom:1,tracks:[],clips:[]},blueprint:{version:1,nodes:[],edges:[]},
  54. production:{...production},productionDefinition:definition,productionActions:actions,assemblyObjects:snapshot.objects};
  55. const getOrCreate=async(type,name,content,projectId)=>{
  56. if(projectId)return api('content/projects/'+projectId);
  57. const list=await api(`content/projects?type=${type}&keyword=${encodeURIComponent(name)}&size=100`);
  58. const found=list.records.find(p=>p.name===name);if(found)return api('content/projects/'+found.id);
  59. return api('content/projects',{type,name,description:'重型支援桥装备训练内容',categoryCode:type==='MODEL'?'EQUIPMENT':'OUTDOOR',coverUri:task.coverUri||'/models/step-model-front-pump-thumb.png',content,assets,dependencies:[]});
  60. };
  61. let model=await getOrCreate('MODEL',modelName,modelContent,item.modelProjectId);item.modelProjectId=model.project.id;write('fixture',fixture);
  62. let scene=await getOrCreate('SCENE',sceneName,{...snapshot,schema:'edit3dv4.scene',version:'1.5',name:sceneName},item.sceneProjectId);item.sceneProjectId=scene.project.id;write('fixture',fixture);
  63. Object.assign(production,{modelProjectId:model.project.id,sceneProjectId:scene.project.id});
  64. modelContent.production={...production};
  65. model=await api('content/projects/'+model.project.id,{...model.project,name:modelName,content:modelContent,assets,dependencies:[],version:model.project.version,changeSummary:'建立部件动作库、旋转轴心与场景制作关联'},'PUT');
  66. model=await api('content/projects/'+model.project.id+'/publish',{version:model.project.version});
  67. item.modelVersionId=model.project.publishedVersionId;production.modelVersionId=item.modelVersionId;
  68. const sceneObjects=snapshot.objects.map((o,i)=>({...o,assetCode:assets[i].code,targetProjectId:model.project.id,targetVersionId:item.modelVersionId}));
  69. const sceneContent={...snapshot,schema:'edit3dv4.scene',version:'1.5',name:sceneName,objects:sceneObjects,
  70. production:{...production},productionDefinition:structuredClone(definition),productionActions:actions};
  71. sceneContent.productionDefinition.scenario.sceneResource.snapshot.objects=structuredClone(sceneObjects);
  72. scene=await api('content/projects/'+scene.project.id,{...scene.project,name:sceneName,content:sceneContent,assets,
  73. dependencies:[{targetProjectId:model.project.id,targetVersionId:item.modelVersionId,relationType:'SCENE_MODEL',required:true}],version:scene.project.version,changeSummary:'通过模型固定版本组成训练场景'},'PUT');
  74. scene=await api('content/projects/'+scene.project.id+'/publish',{version:scene.project.version});
  75. item.sceneVersionId=scene.project.publishedVersionId;production.sceneVersionId=item.sceneVersionId;
  76. const content=structuredClone(original.currentVersion.content);
  77. Object.assign(content,{production:{...production},productionDefinition:applyActionBindings(definition,actions,model.project.id,item.modelVersionId),productionActions:actions});
  78. content.steps=content.steps.map((s,i)=>({...s,modelActions:content.productionDefinition.steps[i].modelActions}));
  79. content.scenario.sceneProjectId=scene.project.id;content.scenario.sceneVersionId=item.sceneVersionId;
  80. let training=await api('content/projects/'+original.project.id,{...original.project,description:'重型支援桥装备操作训练',content,assets:original.currentVersion.assets,
  81. dependencies:[{targetProjectId:scene.project.id,targetVersionId:item.sceneVersionId,relationType:'TRAINING_SCENE',required:true}],version:original.project.version,changeSummary:'关联模型动作和场景固定版本,保持现有教学任务不变'},'PUT');
  82. training=await api('content/projects/'+training.project.id+'/publish',{version:training.project.version});
  83. item.trainingVersionId=training.project.publishedVersionId;item.steps=definition.steps.length;item.actions=actions.length;item.complete=true;write('fixture',fixture);
  84. for(const [stage,data] of [['model',model],['scene',scene],['training',training]])write('after-'+stage+'-'+task.code,data);
  85. assert.deepEqual(stripVolatile(await api('teaching/assignments/'+taskId)),stripVolatile(read('before-task-'+taskId)));
  86. console.log(JSON.stringify(item));
  87. }
  88. for(const id of [86,87,88,89,116])assert.deepEqual(stripVolatile(await api('teaching/assignments/'+id)),stripVolatile(read('before-task-'+id)));
  89. write('registration',{pass:true,items:fixture.items,unchangedAssignments:[86,87,88,89,116],checkedAt:new Date().toISOString()});
  90. }finally{await browser.close();}