|
- 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 {scriptTrainingDefinition} from '../../unreal_tran_web/src/features/virtual-training-scripts/registry.js';
- import {collectActions,applyActionBindings} from '../../unreal_tran_web/src/features/content-production/document.js';
-
- const dir='reports/bridge-content-authoring-20260924';fs.mkdirSync(dir,{recursive:true});
- const session=await login('root');
- 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;};
- const write=(name,value)=>fs.writeFileSync(`${dir}/${name}.json`,JSON.stringify(value,null,2));
- const read=name=>JSON.parse(fs.readFileSync(`${dir}/${name}.json`,'utf8'));
- const backup=(name,value)=>{if(!fs.existsSync(`${dir}/${name}.json`))write(name,value);};
- const fixture=fs.existsSync(`${dir}/fixture.json`)?read('fixture'):{items:[]};
- const stripVolatile=t=>{const d=structuredClone(t);delete d.serverNow;return d;};
- 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};};
- const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
- try{
- for(const taskId of [86,87,88,89]){
- const task=await api('teaching/assignments/'+taskId);backup('before-task-'+taskId,task);
- const original=await api('content/projects/'+task.contentProjectId);backup('before-project-'+task.contentProjectId,original);
- let item=fixture.items.find(i=>i.taskId===String(taskId));
- if(item?.complete){console.log('Already prepared',item.taskCode);continue;}
- const definition=scriptTrainingDefinition(task);
- 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;}
- definition.name=task.name;definition.task.title=task.name;definition.introduction=structuredClone(task.definitionSnapshot.introduction);
- for(const step of definition.steps)step.interaction||='rule-check';
- let actions=collectActions(definition);
- const page=await browser.newPage({viewport:{width:1500,height:1000}});
- await page.goto('http://127.0.0.1:6180/');
- const captured=await page.evaluate(async({definition,actions})=>{
- const host=document.createElement('div');host.style.cssText='position:fixed;inset:0;background:white;z-index:100000';document.body.append(host);
- const {createProductionPreview}=await import('/src/features/content-production/preview.js');
- const viewer=await createProductionPreview(host,definition);
- try{return {actions:viewer.configure(actions)};}finally{viewer.destroy();host.remove();}
- },{definition,actions});
- actions=captured.actions;await page.close();
- assert.equal(actions.length,definition.steps.reduce((n,s)=>n+s.action.mechanism.steps.length,0));
- assert.ok(actions.every(a=>a.binding?.sourceNode&&a.motion));
- write('actions-'+task.code,actions);
- const snapshot=structuredClone(definition.scenario.sceneResource.snapshot);
- const assets=snapshot.objects.map(bytesAsset);
- const resource=snapshot.objects.find(o=>o.assetRole!=='environment'&&!/地形|terrain/i.test(o.sourceName));assert.ok(resource);
- const primaryAsset=assets[snapshot.objects.indexOf(resource)];
- const production={schemaVersion:1,taskCode:task.code,taskId:String(taskId),trainingProjectId:task.contentProjectId,
- sourceContentVersionId:task.contentVersionId,scriptId:task.definitionSnapshot.runtime.scriptId,scriptVersion:task.definitionSnapshot.runtime.scriptVersion};
- const modelName=task.name+'机构模型',sceneName=task.name+'训练场景';
- if(!item){item={taskId:String(taskId),taskCode:task.code,trainingProjectId:task.contentProjectId};fixture.items.push(item);write('fixture',fixture);}
- const modelContent={schema:'digital-twin-editor-project',version:4,name:modelName,modelId:resource.modelId||resource.id,
- modelResource:{...structuredClone(resource),id:resource.modelId||resource.id,assetCode:primaryAsset.code,storageUri:primaryAsset.storageUri,kind:'builtin'},
- camera:{position:[4,3,5],target:[0,1,0]},model:null,environment:null,
- timeline:{version:1,duration:Math.max(1,actions.length),currentTime:0,loop:false,zoom:1,tracks:[],clips:[]},blueprint:{version:1,nodes:[],edges:[]},
- production:{...production},productionDefinition:definition,productionActions:actions,assemblyObjects:snapshot.objects};
- const getOrCreate=async(type,name,content,projectId)=>{
- if(projectId)return api('content/projects/'+projectId);
- const list=await api(`content/projects?type=${type}&keyword=${encodeURIComponent(name)}&size=100`);
- const found=list.records.find(p=>p.name===name);if(found)return api('content/projects/'+found.id);
- 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:[]});
- };
- let model=await getOrCreate('MODEL',modelName,modelContent,item.modelProjectId);item.modelProjectId=model.project.id;write('fixture',fixture);
- 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);
- Object.assign(production,{modelProjectId:model.project.id,sceneProjectId:scene.project.id});
- modelContent.production={...production};
- model=await api('content/projects/'+model.project.id,{...model.project,name:modelName,content:modelContent,assets,dependencies:[],version:model.project.version,changeSummary:'建立部件动作库、旋转轴心与场景制作关联'},'PUT');
- model=await api('content/projects/'+model.project.id+'/publish',{version:model.project.version});
- item.modelVersionId=model.project.publishedVersionId;production.modelVersionId=item.modelVersionId;
- const sceneObjects=snapshot.objects.map((o,i)=>({...o,assetCode:assets[i].code,targetProjectId:model.project.id,targetVersionId:item.modelVersionId}));
- const sceneContent={...snapshot,schema:'edit3dv4.scene',version:'1.5',name:sceneName,objects:sceneObjects,
- production:{...production},productionDefinition:structuredClone(definition),productionActions:actions};
- sceneContent.productionDefinition.scenario.sceneResource.snapshot.objects=structuredClone(sceneObjects);
- scene=await api('content/projects/'+scene.project.id,{...scene.project,name:sceneName,content:sceneContent,assets,
- dependencies:[{targetProjectId:model.project.id,targetVersionId:item.modelVersionId,relationType:'SCENE_MODEL',required:true}],version:scene.project.version,changeSummary:'通过模型固定版本组成训练场景'},'PUT');
- scene=await api('content/projects/'+scene.project.id+'/publish',{version:scene.project.version});
- item.sceneVersionId=scene.project.publishedVersionId;production.sceneVersionId=item.sceneVersionId;
- const content=structuredClone(original.currentVersion.content);
- Object.assign(content,{production:{...production},productionDefinition:applyActionBindings(definition,actions,model.project.id,item.modelVersionId),productionActions:actions});
- content.steps=content.steps.map((s,i)=>({...s,modelActions:content.productionDefinition.steps[i].modelActions}));
- content.scenario.sceneProjectId=scene.project.id;content.scenario.sceneVersionId=item.sceneVersionId;
- let training=await api('content/projects/'+original.project.id,{...original.project,description:'重型支援桥装备操作训练',content,assets:original.currentVersion.assets,
- dependencies:[{targetProjectId:scene.project.id,targetVersionId:item.sceneVersionId,relationType:'TRAINING_SCENE',required:true}],version:original.project.version,changeSummary:'关联模型动作和场景固定版本,保持现有教学任务不变'},'PUT');
- training=await api('content/projects/'+training.project.id+'/publish',{version:training.project.version});
- item.trainingVersionId=training.project.publishedVersionId;item.steps=definition.steps.length;item.actions=actions.length;item.complete=true;write('fixture',fixture);
- for(const [stage,data] of [['model',model],['scene',scene],['training',training]])write('after-'+stage+'-'+task.code,data);
- assert.deepEqual(stripVolatile(await api('teaching/assignments/'+taskId)),stripVolatile(read('before-task-'+taskId)));
- console.log(JSON.stringify(item));
- }
- for(const id of [86,87,88,89,116])assert.deepEqual(stripVolatile(await api('teaching/assignments/'+id)),stripVolatile(read('before-task-'+id)));
- write('registration',{pass:true,items:fixture.items,unchangedAssignments:[86,87,88,89,116],checkedAt:new Date().toISOString()});
- }finally{await browser.close();}
|