|
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import assert from 'node:assert/strict';
- import {chromium} from '@playwright/test';
- import {login,call} from './role-permissions-live.mjs';
- import speech from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-manifest.js';
- import videos from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-video-manifest.js';
- const dir='reports/bridge-005-authoring-20260916';fs.mkdirSync(dir,{recursive:true});
- const file=dir+'/pilot.json', fixture=fs.existsSync(file)?JSON.parse(fs.readFileSync(file,'utf8')):{};
- const save=()=>fs.writeFileSync(file,JSON.stringify(fixture,null,2));
- const signed=await login('root');
- const api=async(endpoint,body,method)=>{const r=await call('tran/v1/'+endpoint,signed.accessToken,body,method);assert.equal(r.status,200,endpoint+': '+r.message);return r.data;};
- const placements=JSON.parse(fs.readFileSync(dir+'/model-placement.json','utf8'));
- const original=await api('content/projects/438');
- assert.equal(original.currentVersion.id,'533','The source must be the verified published version');
- const source=structuredClone(original.currentVersion.content);
- const snapshot=source.scenario.sceneResource.snapshot;
- const prefix='BRIDGE-005-AUTHORING-PILOT';
- async function create(key,type,content,assets=[],dependencies=[]){
- if(fixture[key]){
- const current=await api('content/projects/'+fixture[key]);
- if(type==='SCENE' && !current.project.publishedVersionId)return api('content/projects/'+current.project.id,{...current.project,content,assets,dependencies,version:current.project.version},'PUT');
- return current;
- }
- const code=prefix+'-'+type;
- const pilotName='005 液压泵应急切换(编排验证)';
- const found=await api('content/projects?type='+type+'&keyword='+encodeURIComponent(pilotName)+'&size=100');
- assert.ok(!found.records.some(p=>p.name===pilotName),'Untracked pilot already exists; inspect before adopting');
- const detail=await api('content/projects',{code,type,name:'005 液压泵应急切换(编排验证)',description:'独立验证工程;不替换 TASK-VIRTUAL-005 及其发布版本。',categoryCode:'MAINTENANCE',trainingMode:type==='TRAINING'?'VIRTUAL':undefined,coverUri:'/models/step-model-front-pump-thumb.png',content,assets,dependencies});
- fixture[key]=detail.project.id;save();return detail;
- }
- async function publish(detail){
- if(detail.project.publishedVersionId===detail.currentVersion.id)return detail;
- return api('content/projects/'+detail.project.id+'/publish',{version:detail.project.version});
- }
- const asset=(code,url,type)=>{const b=fs.readFileSync(path.resolve('../unreal_tran_web/public',url.replace(/^\//,'')));return {code,name:code,type,storageUri:url,mimeType:'model/gltf-binary',sizeBytes:b.length,sha256:crypto.createHash('sha256').update(b).digest('hex'),metadata:{builtin:true},status:'READY',sortOrder:0};};
- const sceneObjects=snapshot.objects.map((o,i)=>({id:o.sceneId,type:'model',name:o.name,sourceName:o.sourceName,assetId:o.modelId,assetCode:i?'PILOT-VEHICLE':'PILOT-TERRAIN',assetUrl:o.resourceUrl,...placements.objects[i],visible:true,opacity:1,color:'#ffffff',semantic:{category:i?'实训装备':'环境地形',deviceId:o.sceneId,modelId:o.modelId,interactionId:o.interactionId,operable:false,capabilities:['select','inspect']}}));
- const vehicle=snapshot.objects[1];
- for(const p of vehicle.modelPartTargets)sceneObjects.push({id:p.interactionId,parentId:vehicle.sceneId,type:'model-part',name:p.name,sourceNodeName:p.sourceNodeName,sourceInteractionId:p.interactionId,position:[0,0,0],rotation:[0,0,0],scale:[1,1,1],visible:true,opacity:1,semantic:{category:'液压机构',deviceId:vehicle.sceneId,modelId:vehicle.modelId,interactionId:p.interactionId,operable:true,capabilities:p.capabilities}});
- const sceneContent={id:prefix+'-SCENE',settings:{translateSnap:0,rotateSnap:0,scaleSnap:0,snapEnabled:false},schema:'unreal-tran.scene',version:'2.0',name:'005 液压泵应急切换(编排验证)',templateId:'outdoor-1',sceneType:'outdoor',objects:sceneObjects,rendering:{background:'#a8cbe1',transparentBackground:false,fogColor:'#a8cbe1',fogNear:34,fogFar:88,gridSize:40,gridDivisions:80,fogEnabled:false,exposure:1.05,hemisphereSkyColor:'#edf4f7',hemisphereGroundColor:'#697276',hemisphereIntensity:1.45,directionalColor:'#ffffff',directionalIntensity:2,directionalPosition:[12,18,10],environmentIntensity:1,gridVisible:false,cameraFov:45,cameraNear:0.05,cameraFar:800,cameraPosition:placements.camera.position,cameraTarget:placements.camera.target}};
- for(const o of sceneObjects){Object.assign(o,{deleted:false,locked:false,color:'#ffffff',castShadow:true,receiveShadow:true,components:[]});o.semantic.deviceId=o.id;o.semantic.tags=[];}
- let scene=await create('sceneId','SCENE',sceneContent,[asset('PILOT-TERRAIN','/models/地形.glb','MODEL_FILE'),asset('PILOT-VEHICLE','/models/步骤模型.glb','MODEL_FILE')]);
- scene=await publish(scene);fixture.sceneVersionId=scene.project.publishedVersionId;save();
- const content=structuredClone(source);
- delete content.scenario.sceneResource;delete content.resources;delete content.runtime.definitionSource;delete content.runtime.scriptId;delete content.runtime.scriptVersion;
- content.task.title='005 液压泵应急切换(编排验证)';content.task.code=prefix;content.scenario={sceneProjectId:scene.project.id,sceneVersionId:scene.project.publishedVersionId};
- content.steps.forEach(s=>{s.interaction='rule-check';s.targetRef={sceneObjectId:s.targetId,interactionId:s.targetId,sourceNodeName:s.targetNodeName,capabilities:['select','inspect','mechanism'],visible:true};s.audio={narrationSource:'offline',narrationText:'',narrationUrl:'',safetyUrl:'',successCue:'ui',errorCue:'collision-error'};});
- const browser=await chromium.launch({channel:'msedge',headless:true});
- let normalized;
- try{const page=await browser.newPage();await page.goto('http://127.0.0.1:6180');normalized=await page.evaluate(async content=>{const m=await import('/src/features/training/trainingDocument.ts');return m.normalizeTrainingContent(content,'VIRTUAL');},content);}finally{await browser.close();}
- const dependencies=[{targetProjectId:scene.project.id,targetVersionId:scene.project.publishedVersionId,relationType:'TRAINING_SCENE',required:true}];
- let training=await create('trainingId','TRAINING',normalized,[],dependencies);
- if(!fixture.mediaUploaded){
- const assets=[];
- const pack=speech.scripts.find(p=>p.id==='bridge-pump-switch'),vp=videos.scripts.find(p=>p.id==='bridge-pump-switch');
- for(let i=0;i<normalized.steps.length;i++){
- const step=normalized.steps[i],clip=pack.steps[i],video=vp.steps[i];
- assert.equal(clip.text,video.text);
- for(const [kind,url,type]of [['VIDEO',video.url,'VIDEO'],['AUDIO',clip.url,'AUDIO']]){
- const code='PILOT-005-'+(i+1)+'-'+kind,form=new FormData();
- form.append('file',new Blob([fs.readFileSync(path.resolve('../unreal_tran_web/public',url.slice(1)))]),path.basename(url));
- for(const [k,v]of Object.entries({code,name:'步骤 '+(i+1)+' '+kind,type,metadata:JSON.stringify({source:'bridge-005-authoring-pilot'}),sortOrder:String(i),projectVersion:String(training.project.version)}))form.append(k,v);
- const r=await fetch('http://127.0.0.1:6180/api/tran/v1/content/projects/'+training.project.id+'/assets/upload',{method:'POST',headers:{Authorization:'Bearer '+signed.accessToken},body:form});const result=await r.json();assert.equal(r.status,200,result.message);assets.push(result.data);
- step.audio[kind==='VIDEO'?'narrationVideoAssetCode':'narrationAssetCode']=code;
- }
- step.audio.narrationText=clip.text;step.audio.digitalHuman={enabled:true,mode:'video',videoSource:'upload',generationStatus:'generated'};
- }
- training=await api('content/projects/'+training.project.id,{type:'TRAINING',name:training.project.name,description:training.project.description,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content:normalized,assets,dependencies,version:training.project.version,changeSummary:'导入 005 五步机构动作与新版数字人视频、语音'},'PUT');
- fixture.mediaUploaded=true;save();
- }
- training=await publish(training);fixture.trainingVersionId=training.project.publishedVersionId;save();
- if(!fixture.taskId){
- const task=await api('teaching/assignments',{code:prefix,name:'005 液压泵应急切换(编排验证)',description:'独立编排验证,不替换现有005',channel:'VIRTUAL',assignmentKind:'TRAINING',executionMode:'PRACTICE',audienceType:'COMMON',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:false,contentProjectId:training.project.id,contentVersionId:training.project.publishedVersionId,members:[]});
- fixture.taskId=task.id;save();
- }
- // Leave the isolated assignment in DRAFT: teachers can preview without exposing
- // another common task to learners or touching the four published assignments.
- const task=await api('teaching/assignments/'+fixture.taskId);
- fixture.taskStatus=task.status;fixture.previewUrl='/teaching/virtual-training/preview/'+task.id;save();
- const before=JSON.parse(fs.readFileSync(dir+'/original-assignments.json','utf8'));
- for(const id of [86,87,88,89]){const after=await api('teaching/assignments/'+id);delete after.serverNow;delete before[id].serverNow;assert.deepEqual(after,before[id]);}
- console.log(JSON.stringify(fixture));
|