dev_handler.go 295 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242
  1. package handlers
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. neturl "net/url"
  7. "time"
  8. "cmr-backend/internal/httpx"
  9. "cmr-backend/internal/service"
  10. )
  11. type DevHandler struct {
  12. devService *service.DevService
  13. }
  14. func NewDevHandler(devService *service.DevService) *DevHandler {
  15. return &DevHandler{devService: devService}
  16. }
  17. func (h *DevHandler) BootstrapDemo(w http.ResponseWriter, r *http.Request) {
  18. result, err := h.devService.BootstrapDemo(r.Context())
  19. if err != nil {
  20. httpx.WriteError(w, err)
  21. return
  22. }
  23. httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
  24. }
  25. func (h *DevHandler) CreateClientLog(w http.ResponseWriter, r *http.Request) {
  26. if !h.devService.Enabled() {
  27. http.NotFound(w, r)
  28. return
  29. }
  30. var input service.CreateClientDebugLogInput
  31. if err := httpx.DecodeJSON(r, &input); err != nil {
  32. httpx.WriteError(w, fmt.Errorf("decode client log: %w", err))
  33. return
  34. }
  35. entry, err := h.devService.AddClientDebugLog(r.Context(), input)
  36. if err != nil {
  37. httpx.WriteError(w, err)
  38. return
  39. }
  40. httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": entry})
  41. }
  42. func (h *DevHandler) ListClientLogs(w http.ResponseWriter, r *http.Request) {
  43. if !h.devService.Enabled() {
  44. http.NotFound(w, r)
  45. return
  46. }
  47. limit := 50
  48. if raw := r.URL.Query().Get("limit"); raw != "" {
  49. var parsed int
  50. if _, err := fmt.Sscanf(raw, "%d", &parsed); err == nil {
  51. limit = parsed
  52. }
  53. }
  54. items, err := h.devService.ListClientDebugLogs(r.Context(), limit)
  55. if err != nil {
  56. httpx.WriteError(w, err)
  57. return
  58. }
  59. httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": items})
  60. }
  61. func (h *DevHandler) ClearClientLogs(w http.ResponseWriter, r *http.Request) {
  62. if !h.devService.Enabled() {
  63. http.NotFound(w, r)
  64. return
  65. }
  66. if err := h.devService.ClearClientDebugLogs(r.Context()); err != nil {
  67. httpx.WriteError(w, err)
  68. return
  69. }
  70. httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"cleared": true}})
  71. }
  72. func (h *DevHandler) Workbench(w http.ResponseWriter, r *http.Request) {
  73. if !h.devService.Enabled() {
  74. http.NotFound(w, r)
  75. return
  76. }
  77. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  78. _, _ = w.Write([]byte(devWorkbenchHTML))
  79. }
  80. func (h *DevHandler) ManifestSummary(w http.ResponseWriter, r *http.Request) {
  81. if !h.devService.Enabled() {
  82. http.NotFound(w, r)
  83. return
  84. }
  85. rawURL := r.URL.Query().Get("url")
  86. if rawURL == "" {
  87. httpx.WriteError(w, fmt.Errorf("manifest summary url is required"))
  88. return
  89. }
  90. parsed, err := neturl.Parse(rawURL)
  91. if err != nil || parsed.Scheme == "" || parsed.Host == "" {
  92. httpx.WriteError(w, fmt.Errorf("invalid manifest url"))
  93. return
  94. }
  95. client := &http.Client{Timeout: 15 * time.Second}
  96. resp, err := client.Get(parsed.String())
  97. if err != nil {
  98. httpx.WriteError(w, fmt.Errorf("fetch manifest: %w", err))
  99. return
  100. }
  101. defer resp.Body.Close()
  102. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  103. httpx.WriteError(w, fmt.Errorf("fetch manifest: http %d", resp.StatusCode))
  104. return
  105. }
  106. var manifest map[string]any
  107. if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
  108. httpx.WriteError(w, fmt.Errorf("decode manifest: %w", err))
  109. return
  110. }
  111. summary := map[string]any{
  112. "url": parsed.String(),
  113. "schemaVersion": pickString(manifest["schemaVersion"]),
  114. "playfieldKind": pickNestedString(manifest, "playfield", "kind"),
  115. "gameMode": pickNestedString(manifest, "game", "mode"),
  116. }
  117. httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": summary})
  118. }
  119. func (h *DevHandler) DemoPresentationSchema(w http.ResponseWriter, r *http.Request) {
  120. if !h.devService.Enabled() {
  121. http.NotFound(w, r)
  122. return
  123. }
  124. key := r.PathValue("demoKey")
  125. payload, ok := demoPresentationAssets[key]
  126. if !ok {
  127. http.NotFound(w, r)
  128. return
  129. }
  130. httpx.WriteJSON(w, http.StatusOK, payload)
  131. }
  132. func (h *DevHandler) DemoContentManifest(w http.ResponseWriter, r *http.Request) {
  133. if !h.devService.Enabled() {
  134. http.NotFound(w, r)
  135. return
  136. }
  137. key := r.PathValue("demoKey")
  138. payload, ok := demoContentAssets[key]
  139. if !ok {
  140. http.NotFound(w, r)
  141. return
  142. }
  143. httpx.WriteJSON(w, http.StatusOK, payload)
  144. }
  145. func (h *DevHandler) DemoGameManifest(w http.ResponseWriter, r *http.Request) {
  146. if !h.devService.Enabled() {
  147. http.NotFound(w, r)
  148. return
  149. }
  150. key := r.PathValue("demoKey")
  151. payload, ok := demoGameManifestAssets[key]
  152. if !ok {
  153. http.NotFound(w, r)
  154. return
  155. }
  156. httpx.WriteJSON(w, http.StatusOK, payload)
  157. }
  158. func pickString(v any) string {
  159. switch t := v.(type) {
  160. case string:
  161. return t
  162. case float64:
  163. return fmt.Sprintf("%.0f", t)
  164. default:
  165. return ""
  166. }
  167. }
  168. func pickNestedString(m map[string]any, parent, child string) string {
  169. value, ok := m[parent]
  170. if !ok {
  171. return ""
  172. }
  173. nested, ok := value.(map[string]any)
  174. if !ok {
  175. return ""
  176. }
  177. return pickString(nested[child])
  178. }
  179. var demoPresentationAssets = map[string]map[string]any{
  180. "classic": {
  181. "templateKey": "event.detail.city-run",
  182. "sourceType": "schema",
  183. "version": "v2026-04-03",
  184. "title": "雪熊领秀城区顺序赛展示定义",
  185. "event": map[string]any{
  186. "title": "雪熊领秀城区顺序赛",
  187. "subtitle": "沿河绿道 6 点经典路线",
  188. },
  189. "card": map[string]any{
  190. "heroTitle": "今日推荐路线",
  191. "heroSubtitle": "城区步道顺序挑战",
  192. "badge": "顺序赛",
  193. },
  194. "detail": map[string]any{
  195. "sections": []map[string]any{
  196. {"type": "hero", "title": "顺序打卡", "subtitle": "沿河绿道 6 点路线"},
  197. {"type": "summary", "items": []string{"预计时长 35 分钟", "适合首次联调与新手体验", "默认使用标准 6 点线路"}},
  198. {"type": "safety", "items": []string{"注意路口减速", "夜间建议结伴测试"}},
  199. },
  200. },
  201. },
  202. "score-o": {
  203. "templateKey": "event.detail.score-o",
  204. "sourceType": "schema",
  205. "version": "v2026-04-03",
  206. "title": "雪熊领秀城区积分赛展示定义",
  207. "event": map[string]any{
  208. "title": "雪熊领秀城区积分赛",
  209. "subtitle": "20 分钟自由取点积分挑战",
  210. },
  211. "card": map[string]any{
  212. "heroTitle": "自由取点",
  213. "heroSubtitle": "在限定时间内尽量拿高分",
  214. "badge": "积分赛",
  215. },
  216. "detail": map[string]any{
  217. "sections": []map[string]any{
  218. {"type": "hero", "title": "20 分钟自由取点", "subtitle": "控制点分值不同,自由规划路线"},
  219. {"type": "summary", "items": []string{"推荐热身后再开局", "适合熟悉地图后做效率测试", "默认接入 score-o 玩法"}},
  220. {"type": "result", "items": []string{"展示积分、完成点数、路线效率"}},
  221. },
  222. },
  223. },
  224. "manual-variant": {
  225. "templateKey": "event.detail.variant-selector",
  226. "sourceType": "schema",
  227. "version": "v2026-04-03",
  228. "title": "雪熊领秀城区多赛道挑战展示定义",
  229. "event": map[string]any{
  230. "title": "雪熊领秀城区多赛道挑战",
  231. "subtitle": "4 条路线手动选择联调活动",
  232. },
  233. "card": map[string]any{
  234. "heroTitle": "多赛道选择",
  235. "heroSubtitle": "同一地点,不同路线长度与难度",
  236. "badge": "多赛道",
  237. },
  238. "detail": map[string]any{
  239. "sections": []map[string]any{
  240. {"type": "hero", "title": "先选赛道再开始", "subtitle": "路线 01 ~ 04 四选一"},
  241. {"type": "variants", "items": []string{"路线 01", "路线 02", "路线 03", "路线 04"}},
  242. {"type": "summary", "items": []string{"适合验证 variant 选择与回流链", "默认推荐 路线 04 做联调"}},
  243. },
  244. },
  245. },
  246. }
  247. var demoContentAssets = map[string]map[string]any{
  248. "classic": {
  249. "manifestVersion": "1",
  250. "bundleType": "route_content",
  251. "version": "v2026-04-03",
  252. "title": "雪熊领秀城区顺序赛内容包",
  253. "locale": "zh-CN",
  254. "event": map[string]any{
  255. "title": "雪熊领秀城区顺序赛",
  256. "subtitle": "沿河绿道 6 点经典路线",
  257. },
  258. "hero": map[string]any{
  259. "title": "绿道顺序挑战",
  260. "subtitle": "按照既定顺序依次完成 6 个控制点",
  261. },
  262. "sections": []map[string]any{
  263. {"type": "intro", "title": "活动说明", "body": "适合首次联调与基础顺序赛流程验证。"},
  264. {"type": "tips", "title": "路线提示", "body": "默认路线沿河绿道展开,注意桥下拐点。"},
  265. {"type": "result", "title": "结果页文案", "body": "完成后展示用时、配速与打卡完成率。"},
  266. },
  267. "assets": map[string]any{
  268. "cover": "https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg",
  269. "entryHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  270. },
  271. },
  272. "score-o": {
  273. "manifestVersion": "1",
  274. "bundleType": "result_media",
  275. "version": "v2026-04-03",
  276. "title": "雪熊领秀城区积分赛内容包",
  277. "locale": "zh-CN",
  278. "event": map[string]any{
  279. "title": "雪熊领秀城区积分赛",
  280. "subtitle": "20 分钟自由取点积分挑战",
  281. },
  282. "hero": map[string]any{
  283. "title": "自由规划路线",
  284. "subtitle": "在限定时间内尽量争取更高积分",
  285. },
  286. "sections": []map[string]any{
  287. {"type": "intro", "title": "玩法说明", "body": "每个控制点分值不同,优先测试路径规划与效率。"},
  288. {"type": "tips", "title": "策略建议", "body": "建议先拿近点,再视剩余时间冲刺高分点。"},
  289. {"type": "result", "title": "结果页文案", "body": "结果页重点展示总积分、完成点位与平均速度。"},
  290. },
  291. "assets": map[string]any{
  292. "cover": "https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg",
  293. "entryHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  294. },
  295. },
  296. "manual-variant": {
  297. "manifestVersion": "1",
  298. "bundleType": "route_content",
  299. "version": "v2026-04-03",
  300. "title": "雪熊领秀城区多赛道挑战内容包",
  301. "locale": "zh-CN",
  302. "event": map[string]any{
  303. "title": "雪熊领秀城区多赛道挑战",
  304. "subtitle": "4 条路线手动选择联调活动",
  305. },
  306. "hero": map[string]any{
  307. "title": "同图多赛道",
  308. "subtitle": "先选路线,再验证 launch / result / history 回流",
  309. },
  310. "sections": []map[string]any{
  311. {"type": "intro", "title": "玩法说明", "body": "路线 01 ~ 04 使用四条不同 KML,用于验证多赛道选择与回流。"},
  312. {"type": "variants", "title": "赛道差异", "body": "四条赛道共享同一张底图,但各自使用独立 KML。"},
  313. {"type": "result", "title": "结果页文案", "body": "结果页需展示所选赛道名、routeCode 与成绩摘要。"},
  314. },
  315. "assets": map[string]any{
  316. "cover": "https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg",
  317. "entryHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  318. },
  319. },
  320. }
  321. var demoGameManifestAssets = map[string]map[string]any{
  322. "classic": {
  323. "schemaVersion": "1",
  324. "releaseId": "rel_demo_001",
  325. "version": "2026.04.07",
  326. "app": map[string]any{
  327. "id": "sample-classic-001",
  328. "title": "领秀城公园顺序赛",
  329. },
  330. "map": map[string]any{
  331. "tiles": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  332. "mapmeta": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  333. },
  334. "preview": demoGamePreview("classic"),
  335. "playfield": map[string]any{
  336. "kind": "course",
  337. "source": map[string]any{
  338. "type": "kml",
  339. "url": "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  340. },
  341. },
  342. "game": map[string]any{
  343. "mode": "classic-sequential",
  344. },
  345. "assets": map[string]any{
  346. "contentHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  347. },
  348. },
  349. "score-o": {
  350. "schemaVersion": "1",
  351. "releaseId": "rel_demo_score_o_001",
  352. "version": "2026.04.07",
  353. "app": map[string]any{
  354. "id": "sample-score-o-001",
  355. "title": "领秀城公园积分赛",
  356. },
  357. "map": map[string]any{
  358. "tiles": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  359. "mapmeta": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  360. },
  361. "preview": demoGamePreview("score-o"),
  362. "playfield": map[string]any{
  363. "kind": "control-set",
  364. "source": map[string]any{
  365. "type": "kml",
  366. "url": "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  367. },
  368. },
  369. "game": map[string]any{
  370. "mode": "score-o",
  371. },
  372. "assets": map[string]any{
  373. "contentHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  374. },
  375. },
  376. "manual-variant": {
  377. "schemaVersion": "1",
  378. "releaseId": "rel_demo_variant_manual_001",
  379. "version": "2026.04.07",
  380. "app": map[string]any{
  381. "id": "sample-variant-manual-001",
  382. "title": "领秀城公园多赛道挑战",
  383. },
  384. "map": map[string]any{
  385. "tiles": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  386. "mapmeta": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  387. },
  388. "preview": demoGamePreview("manual-variant"),
  389. "playfield": map[string]any{
  390. "kind": "course",
  391. "source": map[string]any{
  392. "type": "kml",
  393. "url": "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route04.kml",
  394. },
  395. },
  396. "game": map[string]any{
  397. "mode": "classic-sequential",
  398. },
  399. "play": map[string]any{
  400. "assignmentMode": "manual",
  401. "courseVariants": []map[string]any{
  402. {
  403. "id": "variant_a",
  404. "name": "路线 01",
  405. "description": "route01.kml",
  406. "routeCode": "route-variant-a",
  407. "selectable": true,
  408. },
  409. {
  410. "id": "variant_b",
  411. "name": "路线 02",
  412. "description": "route02.kml",
  413. "routeCode": "route-variant-b",
  414. "selectable": true,
  415. },
  416. {
  417. "id": "variant_c",
  418. "name": "路线 03",
  419. "description": "route03.kml",
  420. "routeCode": "route-variant-c",
  421. "selectable": true,
  422. },
  423. {
  424. "id": "variant_d",
  425. "name": "路线 04",
  426. "description": "route04.kml",
  427. "routeCode": "route-variant-d",
  428. "selectable": true,
  429. },
  430. },
  431. },
  432. "assets": map[string]any{
  433. "contentHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  434. },
  435. },
  436. }
  437. func demoGamePreview(kind string) map[string]any {
  438. baseTiles := map[string]any{
  439. "tileBaseUrl": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  440. "zoom": 15,
  441. "tileSize": 256,
  442. }
  443. viewport := map[string]any{
  444. "width": 800,
  445. "height": 450,
  446. "minLon": 117.0000,
  447. "minLat": 36.6000,
  448. "maxLon": 117.0800,
  449. "maxLat": 36.6600,
  450. }
  451. switch kind {
  452. case "score-o":
  453. return map[string]any{
  454. "mode": "readonly",
  455. "baseTiles": baseTiles,
  456. "viewport": viewport,
  457. "selectedVariantId": "variant_score_main",
  458. "variants": []map[string]any{
  459. {
  460. "variantId": "variant_score_main",
  461. "name": "积分赛主赛道",
  462. "routeCode": "route-score-o-001",
  463. "controls": []map[string]any{
  464. {"id": "start", "kind": "start", "lon": 117.012, "lat": 36.612, "label": "起点"},
  465. {"id": "s1", "kind": "control", "lon": 117.021, "lat": 36.618, "label": "10分点"},
  466. {"id": "s2", "kind": "control", "lon": 117.034, "lat": 36.624, "label": "20分点"},
  467. {"id": "s3", "kind": "control", "lon": 117.046, "lat": 36.616, "label": "15分点"},
  468. {"id": "finish", "kind": "finish", "lon": 117.025, "lat": 36.606, "label": "终点"},
  469. },
  470. "legs": []map[string]any{
  471. {"from": "start", "to": "s1"},
  472. {"from": "s1", "to": "s2"},
  473. {"from": "s2", "to": "s3"},
  474. {"from": "s3", "to": "finish"},
  475. },
  476. },
  477. },
  478. }
  479. case "manual-variant":
  480. return map[string]any{
  481. "mode": "readonly",
  482. "baseTiles": baseTiles,
  483. "viewport": viewport,
  484. "selectedVariantId": "variant_d",
  485. "variants": []map[string]any{
  486. {
  487. "variantId": "variant_a",
  488. "name": "路线 01",
  489. "routeCode": "route-variant-a",
  490. "controls": []map[string]any{
  491. {"id": "start", "kind": "start", "lon": 117.011, "lat": 36.611, "label": "A起点"},
  492. {"id": "a1", "kind": "control", "lon": 117.020, "lat": 36.616, "label": "A1"},
  493. {"id": "a2", "kind": "control", "lon": 117.028, "lat": 36.621, "label": "A2"},
  494. {"id": "finish", "kind": "finish", "lon": 117.034, "lat": 36.626, "label": "A终点"},
  495. },
  496. "legs": []map[string]any{
  497. {"from": "start", "to": "a1"},
  498. {"from": "a1", "to": "a2"},
  499. {"from": "a2", "to": "finish"},
  500. },
  501. },
  502. {
  503. "variantId": "variant_b",
  504. "name": "路线 02",
  505. "routeCode": "route-variant-b",
  506. "controls": []map[string]any{
  507. {"id": "start", "kind": "start", "lon": 117.014, "lat": 36.609, "label": "B起点"},
  508. {"id": "b1", "kind": "control", "lon": 117.025, "lat": 36.615, "label": "B1"},
  509. {"id": "b2", "kind": "control", "lon": 117.038, "lat": 36.622, "label": "B2"},
  510. {"id": "b3", "kind": "control", "lon": 117.051, "lat": 36.629, "label": "B3"},
  511. {"id": "finish", "kind": "finish", "lon": 117.060, "lat": 36.634, "label": "B终点"},
  512. },
  513. "legs": []map[string]any{
  514. {"from": "start", "to": "b1"},
  515. {"from": "b1", "to": "b2"},
  516. {"from": "b2", "to": "b3"},
  517. {"from": "b3", "to": "finish"},
  518. },
  519. },
  520. {
  521. "variantId": "variant_c",
  522. "name": "路线 03",
  523. "routeCode": "route-variant-c",
  524. "controls": []map[string]any{
  525. {"id": "1", "kind": "control", "lon": 117.000649296107, "lat": 36.5921631022497, "label": "1"},
  526. {"id": "2", "kind": "control", "lon": 117.000665559813, "lat": 36.5919574366878, "label": "2"},
  527. {"id": "3", "kind": "control", "lon": 117.001397426578, "lat": 36.5915983367736, "label": "3"},
  528. {"id": "4", "kind": "control", "lon": 117.000441933857, "lat": 36.5915004001434, "label": "4"},
  529. {"id": "5", "kind": "control", "lon": 117.000340285695, "lat": 36.5909356298175, "label": "5"},
  530. {"id": "6", "kind": "control", "lon": 116.999860506371, "lat": 36.5912131186443, "label": "6"},
  531. {"id": "7", "kind": "control", "lon": 116.999823913032, "lat": 36.591572220351, "label": "7"},
  532. {"id": "8", "kind": "control", "lon": 116.999108309973, "lat": 36.5919019395375, "label": "8"},
  533. {"id": "9", "kind": "control", "lon": 116.999689737459, "lat": 36.5922740961347, "label": "9"},
  534. {"id": "10", "kind": "control", "lon": 117.000649296107, "lat": 36.5921631022497, "label": "10"},
  535. },
  536. "legs": []map[string]any{
  537. {"from": "1", "to": "2"},
  538. {"from": "2", "to": "3"},
  539. {"from": "3", "to": "4"},
  540. {"from": "4", "to": "5"},
  541. {"from": "5", "to": "6"},
  542. {"from": "6", "to": "7"},
  543. {"from": "7", "to": "8"},
  544. {"from": "8", "to": "9"},
  545. {"from": "9", "to": "10"},
  546. },
  547. },
  548. {
  549. "variantId": "variant_d",
  550. "name": "路线 04",
  551. "routeCode": "route-variant-d",
  552. "controls": []map[string]any{
  553. {"id": "1", "kind": "control", "lon": 117.000649296107, "lat": 36.5921631022497, "label": "1"},
  554. {"id": "2", "kind": "control", "lon": 117.000803801313, "lat": 36.5919411140006, "label": "2"},
  555. {"id": "3", "kind": "control", "lon": 117.000937976887, "lat": 36.5916113949816, "label": "3"},
  556. {"id": "4", "kind": "control", "lon": 117.000238637533, "lat": 36.5914742836876, "label": "4"},
  557. {"id": "5", "kind": "control", "lon": 117.00058830721, "lat": 36.5905340853955, "label": "5"},
  558. {"id": "6", "kind": "control", "lon": 116.998941606987, "lat": 36.5908278985923, "label": "6"},
  559. {"id": "7", "kind": "control", "lon": 116.998774904002, "lat": 36.5913306430232, "label": "7"},
  560. {"id": "8", "kind": "control", "lon": 116.999710067091, "lat": 36.5917615642144, "label": "8"},
  561. {"id": "9", "kind": "control", "lon": 116.999766990062, "lat": 36.5921141343085, "label": "9"},
  562. {"id": "10", "kind": "control", "lon": 117.000649296107, "lat": 36.5921631022497, "label": "10"},
  563. },
  564. "legs": []map[string]any{
  565. {"from": "1", "to": "2"},
  566. {"from": "2", "to": "3"},
  567. {"from": "3", "to": "4"},
  568. {"from": "4", "to": "5"},
  569. {"from": "5", "to": "6"},
  570. {"from": "6", "to": "7"},
  571. {"from": "7", "to": "8"},
  572. {"from": "8", "to": "9"},
  573. {"from": "9", "to": "10"},
  574. },
  575. },
  576. },
  577. }
  578. default:
  579. return map[string]any{
  580. "mode": "readonly",
  581. "baseTiles": baseTiles,
  582. "viewport": viewport,
  583. "selectedVariantId": "variant_classic_main",
  584. "variants": []map[string]any{
  585. {
  586. "variantId": "variant_classic_main",
  587. "name": "顺序赛主赛道",
  588. "routeCode": "route-demo-a",
  589. "controls": []map[string]any{
  590. {"id": "start", "kind": "start", "lon": 117.010, "lat": 36.610, "label": "起点"},
  591. {"id": "c1", "kind": "control", "lon": 117.018, "lat": 36.615, "label": "1"},
  592. {"id": "c2", "kind": "control", "lon": 117.027, "lat": 36.621, "label": "2"},
  593. {"id": "c3", "kind": "control", "lon": 117.036, "lat": 36.627, "label": "3"},
  594. {"id": "finish", "kind": "finish", "lon": 117.044, "lat": 36.632, "label": "终点"},
  595. },
  596. "legs": []map[string]any{
  597. {"from": "start", "to": "c1"},
  598. {"from": "c1", "to": "c2"},
  599. {"from": "c2", "to": "c3"},
  600. {"from": "c3", "to": "finish"},
  601. },
  602. },
  603. },
  604. }
  605. }
  606. }
  607. const devWorkbenchHTML = `<!doctype html>
  608. <html lang="zh-CN">
  609. <head>
  610. <meta charset="utf-8">
  611. <meta name="viewport" content="width=device-width, initial-scale=1">
  612. <title>CMR Backend Workbench</title>
  613. <style>
  614. :root {
  615. --bg: #0d1418;
  616. --panel: #132129;
  617. --panel-alt: #182b34;
  618. --text: #e9f1f5;
  619. --muted: #8ea3ad;
  620. --line: #29424d;
  621. --accent: #4fd1a5;
  622. --accent-2: #ffd166;
  623. --danger: #ff6b6b;
  624. --mono: "Consolas", "SFMono-Regular", monospace;
  625. --sans: "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
  626. }
  627. * { box-sizing: border-box; }
  628. body {
  629. margin: 0;
  630. background:
  631. radial-gradient(circle at top right, rgba(79, 209, 165, 0.12), transparent 24%),
  632. radial-gradient(circle at bottom left, rgba(255, 209, 102, 0.10), transparent 28%),
  633. var(--bg);
  634. color: var(--text);
  635. font-family: var(--sans);
  636. }
  637. .shell {
  638. max-width: 1400px;
  639. margin: 0 auto;
  640. padding: 28px 24px 40px;
  641. }
  642. .hero {
  643. display: grid;
  644. gap: 8px;
  645. margin-bottom: 22px;
  646. }
  647. .eyebrow {
  648. color: var(--accent);
  649. text-transform: uppercase;
  650. letter-spacing: 0.14em;
  651. font-size: 12px;
  652. font-weight: 700;
  653. }
  654. h1 {
  655. margin: 0;
  656. font-size: 34px;
  657. line-height: 1.1;
  658. }
  659. .hero p {
  660. margin: 0;
  661. max-width: 920px;
  662. color: var(--muted);
  663. line-height: 1.6;
  664. }
  665. .layout {
  666. display: grid;
  667. grid-template-columns: 260px minmax(0, 1fr);
  668. gap: 20px;
  669. align-items: start;
  670. }
  671. .sidebar {
  672. position: sticky;
  673. top: 18px;
  674. display: grid;
  675. gap: 16px;
  676. }
  677. .workspace {
  678. display: grid;
  679. gap: 0;
  680. min-width: 0;
  681. }
  682. .side-card {
  683. background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)), var(--panel);
  684. border: 1px solid var(--line);
  685. border-radius: 18px;
  686. padding: 16px;
  687. display: grid;
  688. gap: 12px;
  689. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
  690. }
  691. .side-card h2 {
  692. margin: 0;
  693. font-size: 16px;
  694. }
  695. .side-card p {
  696. margin: 0;
  697. color: var(--muted);
  698. font-size: 12px;
  699. line-height: 1.6;
  700. }
  701. .mode-list,
  702. .side-links {
  703. display: grid;
  704. gap: 8px;
  705. }
  706. .mode-btn,
  707. .side-link {
  708. display: inline-flex;
  709. align-items: center;
  710. justify-content: flex-start;
  711. min-height: 40px;
  712. padding: 0 14px;
  713. border-radius: 12px;
  714. border: 1px solid var(--line);
  715. background: rgba(255,255,255,0.04);
  716. color: var(--text);
  717. font-size: 13px;
  718. font-weight: 600;
  719. text-decoration: none;
  720. cursor: pointer;
  721. }
  722. .mode-btn.active {
  723. background: rgba(79, 209, 165, 0.16);
  724. border-color: rgba(79, 209, 165, 0.55);
  725. color: var(--accent);
  726. }
  727. .guide-list {
  728. display: grid;
  729. gap: 8px;
  730. margin: 0;
  731. padding-left: 18px;
  732. color: var(--muted);
  733. font-size: 13px;
  734. line-height: 1.6;
  735. }
  736. .category-head {
  737. display: grid;
  738. gap: 6px;
  739. margin: 28px 0 14px;
  740. }
  741. .category-kicker {
  742. color: var(--accent);
  743. font-size: 12px;
  744. font-weight: 700;
  745. text-transform: uppercase;
  746. letter-spacing: 0.08em;
  747. }
  748. .category-head h2 {
  749. margin: 0;
  750. font-size: 24px;
  751. line-height: 1.2;
  752. }
  753. .category-head p {
  754. margin: 0;
  755. color: var(--muted);
  756. line-height: 1.6;
  757. font-size: 13px;
  758. max-width: 960px;
  759. }
  760. .grid {
  761. display: grid;
  762. gap: 16px;
  763. grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  764. align-items: start;
  765. }
  766. .masonry {
  767. position: relative;
  768. margin-top: 16px;
  769. min-height: 0;
  770. }
  771. .masonry > .panel {
  772. margin: 0;
  773. }
  774. .stack {
  775. display: grid;
  776. gap: 16px;
  777. }
  778. .panel {
  779. background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)), var(--panel);
  780. border: 1px solid var(--line);
  781. border-radius: 18px;
  782. padding: 16px;
  783. display: grid;
  784. gap: 12px;
  785. align-content: start;
  786. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
  787. }
  788. .panel h2 {
  789. margin: 0;
  790. font-size: 18px;
  791. }
  792. .panel p {
  793. margin: 0;
  794. color: var(--muted);
  795. font-size: 13px;
  796. line-height: 1.5;
  797. }
  798. .row {
  799. display: grid;
  800. gap: 8px;
  801. }
  802. .row.two {
  803. grid-template-columns: repeat(2, minmax(0, 1fr));
  804. }
  805. label {
  806. display: grid;
  807. gap: 6px;
  808. font-size: 12px;
  809. color: var(--muted);
  810. }
  811. input, textarea, select {
  812. width: 100%;
  813. border: 1px solid var(--line);
  814. border-radius: 12px;
  815. background: var(--panel-alt);
  816. color: var(--text);
  817. padding: 10px 12px;
  818. font: inherit;
  819. }
  820. textarea {
  821. min-height: 90px;
  822. resize: vertical;
  823. font-family: var(--mono);
  824. font-size: 12px;
  825. }
  826. button {
  827. border: 0;
  828. border-radius: 12px;
  829. padding: 10px 14px;
  830. background: var(--accent);
  831. color: #062419;
  832. font-weight: 700;
  833. cursor: pointer;
  834. transition: transform .14s ease, opacity .14s ease, filter .14s ease, box-shadow .14s ease;
  835. }
  836. button:hover:not(:disabled) {
  837. transform: translateY(-1px);
  838. filter: brightness(1.02);
  839. }
  840. button:disabled {
  841. cursor: not-allowed;
  842. opacity: 0.72;
  843. }
  844. button.is-running {
  845. position: relative;
  846. box-shadow: 0 0 0 1px rgba(79, 209, 165, 0.35), 0 0 0 4px rgba(79, 209, 165, 0.08);
  847. }
  848. button.is-running::after {
  849. content: "";
  850. width: 12px;
  851. height: 12px;
  852. margin-left: 10px;
  853. border-radius: 999px;
  854. border: 2px solid rgba(6, 36, 25, 0.32);
  855. border-top-color: currentColor;
  856. display: inline-block;
  857. vertical-align: -2px;
  858. animation: spin .8s linear infinite;
  859. }
  860. button.secondary {
  861. background: var(--accent-2);
  862. color: #312200;
  863. }
  864. button.ghost {
  865. background: transparent;
  866. color: var(--text);
  867. border: 1px solid var(--line);
  868. }
  869. .btn-stack {
  870. display: inline-flex;
  871. align-items: center;
  872. gap: 8px;
  873. flex-wrap: wrap;
  874. }
  875. .btn-badge {
  876. display: inline-flex;
  877. align-items: center;
  878. justify-content: center;
  879. min-width: 38px;
  880. min-height: 20px;
  881. padding: 0 8px;
  882. border-radius: 999px;
  883. font-size: 11px;
  884. font-weight: 800;
  885. letter-spacing: 0.02em;
  886. background: rgba(255,255,255,0.16);
  887. color: #08231a;
  888. }
  889. .btn-badge.home {
  890. background: rgba(79, 209, 165, 0.22);
  891. color: #083226;
  892. }
  893. .btn-badge.game {
  894. background: rgba(255, 209, 102, 0.3);
  895. color: #3c2a00;
  896. }
  897. .btn-badge.publish {
  898. background: rgba(125, 211, 252, 0.28);
  899. color: #082a43;
  900. }
  901. .btn-badge.verify {
  902. background: rgba(251, 146, 60, 0.3);
  903. color: #482100;
  904. }
  905. .btn-badge.recommend {
  906. background: rgba(248, 113, 113, 0.28);
  907. color: #4a1111;
  908. }
  909. .actions {
  910. display: flex;
  911. flex-wrap: wrap;
  912. gap: 8px;
  913. }
  914. .kv {
  915. display: grid;
  916. gap: 6px;
  917. font-size: 12px;
  918. color: var(--muted);
  919. }
  920. .kv code {
  921. display: block;
  922. padding: 8px 10px;
  923. border-radius: 10px;
  924. background: rgba(255,255,255,0.04);
  925. color: var(--text);
  926. font-family: var(--mono);
  927. word-break: break-all;
  928. }
  929. .log {
  930. min-height: 220px;
  931. max-height: 520px;
  932. overflow: auto;
  933. white-space: pre-wrap;
  934. word-break: break-word;
  935. font-family: var(--mono);
  936. font-size: 12px;
  937. line-height: 1.55;
  938. background: #0a1013;
  939. border: 1px solid var(--line);
  940. border-radius: 16px;
  941. padding: 14px;
  942. }
  943. .subpanel {
  944. display: grid;
  945. gap: 8px;
  946. padding: 12px;
  947. border-radius: 14px;
  948. background: rgba(255,255,255,0.03);
  949. border: 1px solid rgba(255,255,255,0.05);
  950. }
  951. .history {
  952. display: grid;
  953. gap: 8px;
  954. max-height: 280px;
  955. overflow: auto;
  956. }
  957. .history-item {
  958. padding: 10px 12px;
  959. border-radius: 12px;
  960. background: rgba(255,255,255,0.04);
  961. border: 1px solid rgba(255,255,255,0.05);
  962. font-family: var(--mono);
  963. font-size: 12px;
  964. line-height: 1.5;
  965. }
  966. .history-item strong {
  967. color: var(--accent);
  968. }
  969. .muted-note {
  970. color: var(--muted);
  971. font-size: 12px;
  972. line-height: 1.5;
  973. }
  974. .api-toolbar {
  975. display: flex;
  976. flex-wrap: wrap;
  977. gap: 10px;
  978. align-items: center;
  979. }
  980. .api-toolbar input {
  981. max-width: 360px;
  982. }
  983. .api-catalog {
  984. display: grid;
  985. gap: 12px;
  986. }
  987. .api-summary {
  988. display: flex;
  989. flex-wrap: wrap;
  990. gap: 8px;
  991. margin: 4px 0 2px;
  992. }
  993. .api-chip {
  994. display: inline-flex;
  995. align-items: center;
  996. min-height: 30px;
  997. padding: 0 10px;
  998. border-radius: 999px;
  999. border: 1px solid var(--line);
  1000. background: rgba(255,255,255,0.04);
  1001. color: var(--muted);
  1002. font-size: 12px;
  1003. font-weight: 600;
  1004. }
  1005. .api-item {
  1006. display: grid;
  1007. gap: 8px;
  1008. padding: 14px;
  1009. border-radius: 14px;
  1010. background: rgba(255,255,255,0.03);
  1011. border: 1px solid rgba(255,255,255,0.06);
  1012. }
  1013. .api-item.hidden {
  1014. display: none;
  1015. }
  1016. .mode-hidden {
  1017. display: none !important;
  1018. }
  1019. .api-head {
  1020. display: flex;
  1021. flex-wrap: wrap;
  1022. gap: 8px 12px;
  1023. align-items: center;
  1024. }
  1025. .api-method {
  1026. padding: 4px 8px;
  1027. border-radius: 999px;
  1028. background: rgba(79, 209, 165, 0.14);
  1029. color: var(--accent);
  1030. font-family: var(--mono);
  1031. font-size: 12px;
  1032. font-weight: 700;
  1033. }
  1034. .api-path {
  1035. font-family: var(--mono);
  1036. font-size: 13px;
  1037. color: var(--text);
  1038. word-break: break-all;
  1039. }
  1040. .api-desc {
  1041. color: var(--muted);
  1042. font-size: 13px;
  1043. line-height: 1.6;
  1044. }
  1045. .api-meta {
  1046. display: grid;
  1047. gap: 6px;
  1048. font-size: 12px;
  1049. color: var(--muted);
  1050. }
  1051. .api-meta strong {
  1052. color: var(--text);
  1053. font-weight: 600;
  1054. }
  1055. .status {
  1056. display: inline-flex;
  1057. align-items: center;
  1058. gap: 8px;
  1059. color: var(--accent);
  1060. font-weight: 700;
  1061. min-height: 24px;
  1062. }
  1063. .status::before {
  1064. content: "";
  1065. width: 10px;
  1066. height: 10px;
  1067. border-radius: 999px;
  1068. background: currentColor;
  1069. box-shadow: 0 0 0 0 rgba(79, 209, 165, 0.35);
  1070. }
  1071. .status.running::before {
  1072. animation: pulse 1.1s ease infinite;
  1073. }
  1074. .status.error {
  1075. color: var(--danger);
  1076. }
  1077. .progress-card {
  1078. display: grid;
  1079. gap: 8px;
  1080. padding: 12px;
  1081. border-radius: 14px;
  1082. background: rgba(255,255,255,0.03);
  1083. border: 1px solid rgba(255,255,255,0.05);
  1084. }
  1085. .progress-meta {
  1086. display: flex;
  1087. justify-content: space-between;
  1088. gap: 12px;
  1089. align-items: center;
  1090. color: var(--muted);
  1091. font-size: 12px;
  1092. }
  1093. .progress-label {
  1094. color: var(--text);
  1095. font-weight: 700;
  1096. }
  1097. .progress-track {
  1098. width: 100%;
  1099. height: 10px;
  1100. border-radius: 999px;
  1101. background: rgba(255,255,255,0.08);
  1102. overflow: hidden;
  1103. }
  1104. .progress-fill {
  1105. height: 100%;
  1106. width: 0%;
  1107. border-radius: 999px;
  1108. background: linear-gradient(90deg, rgba(79, 209, 165, 0.95), rgba(125, 211, 252, 0.95));
  1109. transition: width .18s ease;
  1110. }
  1111. .progress-note {
  1112. color: var(--muted);
  1113. font-size: 12px;
  1114. line-height: 1.5;
  1115. }
  1116. @keyframes spin {
  1117. from { transform: rotate(0deg); }
  1118. to { transform: rotate(360deg); }
  1119. }
  1120. @keyframes pulse {
  1121. 0% { box-shadow: 0 0 0 0 rgba(79, 209, 165, 0.38); }
  1122. 70% { box-shadow: 0 0 0 8px rgba(79, 209, 165, 0); }
  1123. 100% { box-shadow: 0 0 0 0 rgba(79, 209, 165, 0); }
  1124. }
  1125. @media (max-width: 900px) {
  1126. .layout { grid-template-columns: 1fr; }
  1127. .sidebar { position: static; }
  1128. .row.two { grid-template-columns: 1fr; }
  1129. .shell { padding: 20px 16px 32px; }
  1130. .masonry {
  1131. display: grid;
  1132. grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  1133. gap: 16px;
  1134. height: auto !important;
  1135. }
  1136. .masonry > .panel {
  1137. position: static !important;
  1138. width: auto !important;
  1139. left: auto !important;
  1140. top: auto !important;
  1141. }
  1142. }
  1143. @media (max-width: 640px) {
  1144. .masonry {
  1145. grid-template-columns: 1fr;
  1146. }
  1147. }
  1148. </style>
  1149. </head>
  1150. <body>
  1151. <div class="shell">
  1152. <div class="hero">
  1153. <div class="eyebrow">Developer Workbench</div>
  1154. <h1>CMR Backend API Flow Panel</h1>
  1155. <p>把入口、登录、首页、活动详情、launch、session、profile 串成一条完整调试链。这个页面只在非 production 环境开放,适合后续继续扩展成你想要的 API 测试面板。</p>
  1156. </div>
  1157. <div class="layout">
  1158. <aside class="sidebar">
  1159. <section class="side-card">
  1160. <h2>工作模式</h2>
  1161. <p>先选你现在要做的事,主区只显示这一类内容。</p>
  1162. <div class="mode-list">
  1163. <button class="mode-btn" data-mode-btn="frontend" type="button">前台联调</button>
  1164. <button class="mode-btn" data-mode-btn="config" type="button">配置发布</button>
  1165. <button class="mode-btn" data-mode-btn="admin" type="button">后台运营</button>
  1166. <button class="mode-btn" data-mode-btn="reference" type="button">接口参考</button>
  1167. <button class="mode-btn" data-mode-btn="all" type="button">全部显示</button>
  1168. </div>
  1169. </section>
  1170. <section class="side-card">
  1171. <h2>区域跳转</h2>
  1172. <div class="side-links">
  1173. <a class="side-link" href="#nav-main" data-nav-target="nav-main" data-nav-mode="frontend">联调主区</a>
  1174. <a class="side-link" href="#nav-fast" data-nav-target="nav-fast" data-nav-mode="frontend">快捷操作</a>
  1175. <a class="side-link" href="#nav-admin" data-nav-target="nav-admin" data-nav-mode="admin">后台运营</a>
  1176. <a class="side-link" href="#nav-tools" data-nav-target="nav-tools">辅助工具</a>
  1177. <a class="side-link" href="#nav-api" data-nav-target="nav-api" data-nav-mode="reference">API 目录 <span id="nav-api-count">(0)</span></a>
  1178. </div>
  1179. </section>
  1180. <section class="side-card">
  1181. <h2>建议顺序</h2>
  1182. <ol class="guide-list">
  1183. <li>前台联调:先跑 demo、登录、入口、launch、session、result。</li>
  1184. <li>配置发布:只看 source、build、publish、rollback。</li>
  1185. <li>后台运营:只在要管理地图、KML、资源包时进入。</li>
  1186. </ol>
  1187. </section>
  1188. </aside>
  1189. <main class="workspace">
  1190. <div class="category-head" id="nav-main" data-modes="frontend config">
  1191. <div class="category-kicker">Main Flow</div>
  1192. <h2>第一步:选玩法与准备数据</h2>
  1193. <p>先在这里选当前要测的玩法,workbench 后面的发布链、launch、result、history 都会复用这里的 event。</p>
  1194. </div>
  1195. <div class="grid">
  1196. <section class="panel" data-modes="frontend config admin">
  1197. <h2>第一步:选玩法</h2>
  1198. <p>先在这里准备 demo 数据并选择玩法入口。顺序赛、积分赛、多赛道各有一套独立 demo 数据,后面一键流程都会复用这里选中的 event。</p>
  1199. <div class="actions">
  1200. <button id="btn-bootstrap"><span class="btn-stack"><span class="btn-badge recommend">先点</span><span>Bootstrap Demo(只准备数据)</span></span></button>
  1201. <button class="secondary" id="btn-bootstrap-publish"><span class="btn-stack"><span class="btn-badge publish">发布</span><span>Bootstrap + 发布当前玩法</span></span></button>
  1202. <button class="secondary" id="btn-use-classic-demo"><span class="btn-stack"><span class="btn-badge home">顺序赛</span><span>Use Classic Demo</span></span></button>
  1203. <button class="secondary" id="btn-use-score-o-demo"><span class="btn-stack"><span class="btn-badge home">积分赛</span><span>Use Score-O Demo</span></span></button>
  1204. <button class="secondary" id="btn-use-variant-manual-demo"><span class="btn-stack"><span class="btn-badge home">多赛道</span><span>Use Manual Variant Demo</span></span></button>
  1205. </div>
  1206. <div class="kv">
  1207. <div>默认入口 <code id="bootstrap-entry">tenant_demo / mini-demo / evt_demo_001</code></div>
  1208. <div>积分赛入口 <code id="bootstrap-score-o-entry">tenant_demo / mini-demo / evt_demo_score_o_001</code></div>
  1209. <div>多赛道入口 <code id="bootstrap-variant-entry">tenant_demo / mini-demo / evt_demo_variant_manual_001</code></div>
  1210. </div>
  1211. <div class="muted-note">说明:<code>Bootstrap Demo(只准备数据)</code> 只负责把三种玩法的 demo 对象和默认样例准备好;<code>Bootstrap + 发布当前玩法</code> 会先准备 demo,再对当前选中的玩法执行一遍“发布活动配置(自动补 Runtime)”。</div>
  1212. </section>
  1213. <section class="panel" data-modes="frontend config">
  1214. <h2>本地配置导入与发布</h2>
  1215. <p>从本地 event 目录导入 source config,生成 preview build,并可在发布时直接挂接 runtime binding。</p>
  1216. <div class="row two">
  1217. <label>Local Config File
  1218. <input id="local-config-file" value="classic-sequential.json">
  1219. </label>
  1220. <label>Event ID
  1221. <input id="config-event-id" value="evt_demo_001">
  1222. </label>
  1223. </div>
  1224. <div class="row two">
  1225. <label>Source ID
  1226. <input id="config-source-id" placeholder="import 后自动填充">
  1227. </label>
  1228. <label>Build ID
  1229. <input id="config-build-id" placeholder="preview 后自动填充">
  1230. </label>
  1231. </div>
  1232. <div class="row two">
  1233. <label>Runtime Binding ID
  1234. <input id="config-runtime-binding-id" placeholder="可选,发布时直接挂接">
  1235. </label>
  1236. <div class="muted-note">第四刀发布闭环:publish 时可直接带 runtimeBindingId,旧发布路径继续可用。</div>
  1237. </div>
  1238. <div class="row two">
  1239. <label>Presentation ID
  1240. <input id="config-presentation-id" placeholder="可选,发布时挂接 presentation">
  1241. </label>
  1242. <label>Content Bundle ID
  1243. <input id="config-content-bundle-id" placeholder="可选,发布时挂接内容包">
  1244. </label>
  1245. </div>
  1246. <div class="actions">
  1247. <button id="btn-config-files">List Local Files</button>
  1248. <button id="btn-config-import">Import Local</button>
  1249. <button class="secondary" id="btn-config-preview">Build Preview</button>
  1250. <button class="secondary" id="btn-config-publish">Publish Build</button>
  1251. <button class="ghost" id="btn-config-source">Get Source</button>
  1252. <button class="ghost" id="btn-config-build">Get Build</button>
  1253. </div>
  1254. </section>
  1255. <section class="panel" data-modes="common">
  1256. <h2>当前上下文</h2>
  1257. <p>当前调试上下文,所有按钮共享这一组状态。</p>
  1258. <div class="kv">
  1259. <div>Access Token <code id="state-access">-</code></div>
  1260. <div>Refresh Token <code id="state-refresh">-</code></div>
  1261. <div>Source ID <code id="state-source">-</code></div>
  1262. <div>Build ID <code id="state-build">-</code></div>
  1263. <div>Release ID <code id="state-release">-</code></div>
  1264. <div>Session ID <code id="state-session">-</code></div>
  1265. <div>Session Token <code id="state-session-token">-</code></div>
  1266. </div>
  1267. <div class="actions">
  1268. <button class="ghost" id="btn-clear-state">Clear State</button>
  1269. </div>
  1270. </section>
  1271. </div>
  1272. <div class="masonry" data-modes="frontend">
  1273. <section class="panel" data-modes="frontend">
  1274. <h2>短信登录 / 绑定</h2>
  1275. <div class="row two">
  1276. <label>Client Type
  1277. <select id="sms-client-type">
  1278. <option value="app">app</option>
  1279. <option value="wechat">wechat</option>
  1280. </select>
  1281. </label>
  1282. <label>Scene
  1283. <select id="sms-scene">
  1284. <option value="login">login</option>
  1285. <option value="bind_mobile">bind_mobile</option>
  1286. </select>
  1287. </label>
  1288. </div>
  1289. <div class="row two">
  1290. <label>Mobile
  1291. <input id="sms-mobile" value="13800138000">
  1292. </label>
  1293. <label>Device Key
  1294. <input id="sms-device" value="workbench-device-001">
  1295. </label>
  1296. </div>
  1297. <div class="row two">
  1298. <label>Country Code
  1299. <input id="sms-country" value="86">
  1300. </label>
  1301. <label>Code
  1302. <input id="sms-code" placeholder="send 后自动填充 devCode">
  1303. </label>
  1304. </div>
  1305. <div class="actions">
  1306. <button id="btn-send-sms">Send SMS</button>
  1307. <button class="secondary" id="btn-login-sms">Login SMS</button>
  1308. <button class="ghost" id="btn-bind-mobile">Bind Mobile</button>
  1309. </div>
  1310. </section>
  1311. <section class="panel" data-modes="frontend">
  1312. <h2>微信小程序登录</h2>
  1313. <p>开发环境可直接使用 dev-xxx code。</p>
  1314. <div class="row two">
  1315. <label>Code
  1316. <input id="wechat-code" value="dev-workbench-user">
  1317. </label>
  1318. <label>Device Key
  1319. <input id="wechat-device" value="wechat-device-001">
  1320. </label>
  1321. </div>
  1322. <div class="actions">
  1323. <button id="btn-login-wechat">Login WeChat Mini</button>
  1324. </div>
  1325. </section>
  1326. <section class="panel" data-modes="frontend">
  1327. <h2>入口与首页</h2>
  1328. <div class="row two">
  1329. <label>Channel Code
  1330. <input id="entry-channel-code" value="mini-demo">
  1331. </label>
  1332. <label>Channel Type
  1333. <input id="entry-channel-type" value="wechat_mini">
  1334. </label>
  1335. </div>
  1336. <div class="actions">
  1337. <button id="btn-resolve-entry">Resolve Entry</button>
  1338. <button id="btn-home">Home</button>
  1339. <button class="secondary" id="btn-entry-home">My Entry Home</button>
  1340. </div>
  1341. </section>
  1342. <section class="panel" data-modes="frontend">
  1343. <h2>活动与启动</h2>
  1344. <div class="row two">
  1345. <label>Event ID
  1346. <input id="event-id" value="evt_demo_001">
  1347. </label>
  1348. <label>Release ID
  1349. <input id="event-release-id" value="rel_demo_001">
  1350. </label>
  1351. </div>
  1352. <div class="row two">
  1353. <label>Launch Device
  1354. <input id="event-device" value="workbench-device-001">
  1355. </label>
  1356. <label>Variant ID
  1357. <input id="event-variant-id" placeholder="可选,manual 时可传">
  1358. </label>
  1359. </div>
  1360. <div class="actions">
  1361. <button id="btn-event-detail">Event Detail</button>
  1362. <button id="btn-event-play">Event Play</button>
  1363. <button class="secondary" id="btn-launch">Launch</button>
  1364. </div>
  1365. </section>
  1366. <section class="panel" data-modes="frontend">
  1367. <h2>局内状态</h2>
  1368. <div class="row two">
  1369. <label>Session ID
  1370. <input id="session-id" placeholder="launch 后自动填充">
  1371. </label>
  1372. <label>Session Token
  1373. <input id="session-token" placeholder="launch 后自动填充">
  1374. </label>
  1375. </div>
  1376. <div class="row two">
  1377. <label>Finish Status
  1378. <select id="finish-status">
  1379. <option value="finished">finished</option>
  1380. <option value="failed">failed</option>
  1381. <option value="cancelled">cancelled</option>
  1382. </select>
  1383. </label>
  1384. <div></div>
  1385. </div>
  1386. <div class="row two">
  1387. <label>Duration Sec
  1388. <input id="finish-duration" type="number" value="960">
  1389. </label>
  1390. <label>Score
  1391. <input id="finish-score" type="number" value="88">
  1392. </label>
  1393. </div>
  1394. <div class="row two">
  1395. <label>Completed Controls
  1396. <input id="finish-controls-done" type="number" value="7">
  1397. </label>
  1398. <label>Total Controls
  1399. <input id="finish-controls-total" type="number" value="8">
  1400. </label>
  1401. </div>
  1402. <div class="row two">
  1403. <label>Distance Meters
  1404. <input id="finish-distance" type="number" step="0.01" value="5230">
  1405. </label>
  1406. <label>Average Speed KM/H
  1407. <input id="finish-speed" type="number" step="0.001" value="6.45">
  1408. </label>
  1409. </div>
  1410. <div class="row two">
  1411. <label>Max Heart Rate BPM
  1412. <input id="finish-heart-rate" type="number" value="168">
  1413. </label>
  1414. <div></div>
  1415. </div>
  1416. <div class="actions">
  1417. <button id="btn-session-detail">Session Detail</button>
  1418. <button id="btn-session-start">Start Session</button>
  1419. <button class="secondary" id="btn-session-finish">Finish Session</button>
  1420. <button class="ghost" id="btn-my-sessions">My Sessions</button>
  1421. </div>
  1422. </section>
  1423. <section class="panel" data-modes="frontend">
  1424. <h2>结果查询</h2>
  1425. <div class="actions">
  1426. <button id="btn-session-result">Session Result</button>
  1427. <button id="btn-my-results">My Results</button>
  1428. </div>
  1429. </section>
  1430. <section class="panel" data-modes="frontend">
  1431. <h2>当前用户</h2>
  1432. <div class="actions">
  1433. <button id="btn-me">/me</button>
  1434. <button id="btn-profile">/me/profile</button>
  1435. </div>
  1436. </section>
  1437. </div>
  1438. <div class="category-head" id="nav-fast" data-modes="frontend config admin">
  1439. <div class="category-kicker">Fast Path</div>
  1440. <h2>第二步:选测试目标</h2>
  1441. <p>玩法选好以后,再决定你现在要测首页、发布链、局内流程,还是整条链一次验收。</p>
  1442. </div>
  1443. <div class="grid" style="margin-top:16px; grid-template-columns:minmax(380px,1.2fr) minmax(320px,0.8fr);" data-modes="frontend config admin">
  1444. <section class="panel" data-modes="frontend config admin">
  1445. <h2>第二步:点测试目标</h2>
  1446. <p>先选玩法入口,再按“你现在想测什么”点对应按钮。大多数情况下,你只需要点最后一个“一键标准回归”。</p>
  1447. <div class="actions">
  1448. <button id="btn-flow-home"><span class="btn-stack"><span class="btn-badge home">首页</span><span>看首页是否正常</span></span></button>
  1449. <button class="secondary" id="btn-flow-launch"><span class="btn-stack"><span class="btn-badge game">局内</span><span>快速进一局</span></span></button>
  1450. <button class="ghost" id="btn-flow-finish"><span class="btn-stack"><span class="btn-badge game">局内</span><span>结束当前这一局</span></span></button>
  1451. <button class="ghost" id="btn-flow-result"><span class="btn-stack"><span class="btn-badge game">结果</span><span>结束并看结果</span></span></button>
  1452. <button class="secondary" id="btn-flow-admin-default-publish"><span class="btn-stack"><span class="btn-badge publish">发布</span><span>发布活动配置(默认绑定)</span></span></button>
  1453. <button class="secondary" id="btn-flow-admin-runtime-publish"><span class="btn-stack"><span class="btn-badge publish">发布</span><span>发布活动配置(自动补 Runtime)</span></span></button>
  1454. <button class="secondary" id="btn-flow-standard-regression"><span class="btn-stack"><span class="btn-badge verify">推荐</span><span>整条链一键验收</span></span></button>
  1455. </div>
  1456. <div class="muted-note">
  1457. 推荐顺序:
  1458. <br>1. 先点上面的玩法入口:Use Classic Demo / Use Score-O Demo / Use Manual Variant Demo
  1459. <br>2. 想直接验收,就点 整条链一键验收
  1460. <br>3. 想只测发布链,就点 发布活动配置(自动补 Runtime)
  1461. <br>4. 想只测局内流程,就点 快速进一局、结束并看结果
  1462. </div>
  1463. <div class="muted-note">这些流程会复用当前表单里的手机号、设备、event、channel 等输入。发布活动配置(默认绑定)会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。发布活动配置(自动补 Runtime)会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。整条链一键验收会先准备并发布当前玩法,再继续执行:play -> launch -> start -> finish -> result -> history。</div>
  1464. <div class="subpanel">
  1465. <div class="muted-note">预期结果</div>
  1466. <div class="kv">
  1467. <div>Release ID <code id="flow-admin-release-result">-</code></div>
  1468. <div>Presentation <code id="flow-admin-presentation-result">-</code></div>
  1469. <div>Content Bundle <code id="flow-admin-content-bundle-result">-</code></div>
  1470. <div>Runtime Binding <code id="flow-admin-runtime-result">-</code></div>
  1471. <div>判定 <code id="flow-admin-verdict">待执行</code></div>
  1472. </div>
  1473. </div>
  1474. <div class="subpanel">
  1475. <div class="muted-note">回归结果汇总</div>
  1476. <div class="kv">
  1477. <div>发布链 <code id="flow-regression-publish-result">待执行</code></div>
  1478. <div>Play <code id="flow-regression-play-result">待执行</code></div>
  1479. <div>Launch <code id="flow-regression-launch-result">待执行</code></div>
  1480. <div>Result <code id="flow-regression-result-result">待执行</code></div>
  1481. <div>History <code id="flow-regression-history-result">待执行</code></div>
  1482. <div>Session ID <code id="flow-regression-session-id">-</code></div>
  1483. <div>总判定 <code id="flow-regression-overall">待执行</code></div>
  1484. </div>
  1485. </div>
  1486. <div class="subpanel">
  1487. <div class="muted-note">当前 Launch 实际配置摘要</div>
  1488. <div class="kv">
  1489. <div>Config URL <code id="launch-config-url">-</code></div>
  1490. <div>Release ID <code id="launch-config-release-id">-</code></div>
  1491. <div>Manifest URL <code id="launch-config-manifest-url">-</code></div>
  1492. <div>Schema Version <code id="launch-config-schema-version">-</code></div>
  1493. <div>Playfield Kind <code id="launch-config-playfield-kind">-</code></div>
  1494. <div>Game Mode <code id="launch-config-game-mode">-</code></div>
  1495. <div>判定 <code id="launch-config-verdict">待执行</code></div>
  1496. </div>
  1497. </div>
  1498. <div class="subpanel">
  1499. <div class="muted-note">当前玩法关键状态</div>
  1500. <div class="kv">
  1501. <div>Event ID <code id="current-flow-event-id">-</code></div>
  1502. <div>Release ID <code id="current-flow-release-id">-</code></div>
  1503. <div>Can Launch <code id="current-flow-can-launch">-</code></div>
  1504. <div>Assignment Mode <code id="current-flow-assignment-mode">-</code></div>
  1505. <div>Variant Count <code id="current-flow-variant-count">-</code></div>
  1506. <div>Game Mode <code id="current-flow-game-mode">-</code></div>
  1507. <div>Playfield Kind <code id="current-flow-playfield-kind">-</code></div>
  1508. </div>
  1509. </div>
  1510. <div class="subpanel">
  1511. <div class="muted-note">准备页地图预览状态</div>
  1512. <div class="kv">
  1513. <div>Preview Mode <code id="current-preview-mode">-</code></div>
  1514. <div>Tile Base URL <code id="current-preview-tile-url">-</code></div>
  1515. <div>Zoom <code id="current-preview-zoom">-</code></div>
  1516. <div>Viewport <code id="current-preview-viewport">-</code></div>
  1517. <div>Selected Variant <code id="current-preview-selected-variant">-</code></div>
  1518. <div>Preview Variant Count <code id="current-preview-variant-count">-</code></div>
  1519. <div>First Variant Controls <code id="current-preview-control-count">-</code></div>
  1520. <div>First Variant Legs <code id="current-preview-leg-count">-</code></div>
  1521. </div>
  1522. </div>
  1523. </section>
  1524. <div class="stack" data-modes="common frontend">
  1525. <section class="panel" data-modes="common">
  1526. <h2>请求导出</h2>
  1527. <p>最后一次请求会生成一条可复制的 curl,后面做问题复现会方便很多。</p>
  1528. <div class="actions">
  1529. <button id="btn-copy-curl">Copy Last Curl</button>
  1530. <button class="ghost" id="btn-clear-history">Clear History</button>
  1531. </div>
  1532. <div class="subpanel">
  1533. <div class="muted-note">Last Curl</div>
  1534. <div id="curl" class="log" style="min-height:120px; max-height:200px;"></div>
  1535. </div>
  1536. </section>
  1537. <section class="panel" data-modes="common frontend">
  1538. <h2>前端调试日志</h2>
  1539. <p>前端可把 launch、manifest、地图页、结果页等调试信息直接打到 backend。这里显示最近日志,便于和 workbench 当前配置对口排查。</p>
  1540. <div class="actions">
  1541. <button id="btn-client-logs-refresh">拉取前端日志</button>
  1542. <button class="ghost" id="btn-client-logs-clear">清空前端日志</button>
  1543. </div>
  1544. <div class="muted-note">建议前端至少上报:eventId / releaseId / manifestUrl / game.mode / playfield.kind / 页面阶段。</div>
  1545. <div id="client-logs" class="log" style="min-height:180px; max-height:420px;"></div>
  1546. </section>
  1547. </div>
  1548. </div>
  1549. <div class="category-head" id="nav-admin" data-modes="config admin">
  1550. <div class="category-kicker">Advanced</div>
  1551. <h2>后台运营与发布</h2>
  1552. <p>这一组给资源对象、Event 组装、Build / Publish / Rollback 使用。默认隐藏,只有需要管理配置和资源时再打开。</p>
  1553. </div>
  1554. <div class="grid" style="margin-top:16px;" data-modes="admin">
  1555. <section class="panel" data-modes="admin">
  1556. <h2>第一阶段生产骨架联调台</h2>
  1557. <p>这里只做总控确认的最小范围:地点、地图资产、瓦片版本、赛道输入源、赛道集合、赛道方案、运行绑定。</p>
  1558. <div class="subpanel">
  1559. <div class="muted-note">A. 地点与地图</div>
  1560. <div class="row two">
  1561. <label>Place Code
  1562. <input id="prod-place-code" value="place-demo-001">
  1563. </label>
  1564. <label>Place Name
  1565. <input id="prod-place-name" value="Demo Park">
  1566. </label>
  1567. </div>
  1568. <div class="row two">
  1569. <label>Place ID
  1570. <input id="prod-place-id" placeholder="list/create/detail 后填入">
  1571. </label>
  1572. <label>Place Status
  1573. <select id="prod-place-status">
  1574. <option value="active">active</option>
  1575. <option value="draft">draft</option>
  1576. <option value="disabled">disabled</option>
  1577. </select>
  1578. </label>
  1579. </div>
  1580. <div class="row two">
  1581. <label>Place Region
  1582. <input id="prod-place-region" value="Shanghai">
  1583. </label>
  1584. <label>Place Cover URL
  1585. <input id="prod-place-cover-url" value="">
  1586. </label>
  1587. </div>
  1588. <div class="actions">
  1589. <button id="btn-prod-places-list">List Places</button>
  1590. <button id="btn-prod-place-create">Create Place</button>
  1591. <button class="ghost" id="btn-prod-place-detail">Get Place</button>
  1592. </div>
  1593. <div class="row two">
  1594. <label>Map Asset Code
  1595. <input id="prod-map-asset-code" value="mapasset-demo-001">
  1596. </label>
  1597. <label>Map Asset Name
  1598. <input id="prod-map-asset-name" value="Demo Asset Map">
  1599. </label>
  1600. </div>
  1601. <div class="row two">
  1602. <label>Map Asset ID
  1603. <input id="prod-map-asset-id" placeholder="create/detail 后填入">
  1604. </label>
  1605. <label>Legacy Map ID
  1606. <input id="prod-map-asset-legacy-map-id" placeholder="可选,复用 /admin/maps 的 id">
  1607. </label>
  1608. </div>
  1609. <div class="row two">
  1610. <label>Map Asset Type
  1611. <input id="prod-map-asset-type" value="standard">
  1612. </label>
  1613. <label>Map Asset Status
  1614. <select id="prod-map-asset-status">
  1615. <option value="active">active</option>
  1616. <option value="draft">draft</option>
  1617. <option value="disabled">disabled</option>
  1618. </select>
  1619. </label>
  1620. </div>
  1621. <div class="actions">
  1622. <button id="btn-prod-map-asset-create">Create Map Asset</button>
  1623. <button class="ghost" id="btn-prod-map-asset-detail">Get Map Asset</button>
  1624. </div>
  1625. <div class="row two">
  1626. <label>Tile Release ID
  1627. <input id="prod-tile-release-id" placeholder="create/detail 后填入">
  1628. </label>
  1629. <label>Legacy Tile Version ID
  1630. <input id="prod-tile-legacy-version-id" placeholder="可选,复用 /admin/maps 版本 id">
  1631. </label>
  1632. </div>
  1633. <div class="row two">
  1634. <label>Tile Version Code
  1635. <input id="prod-tile-version-code" value="v2026-04-03">
  1636. </label>
  1637. <label>Tile Status
  1638. <select id="prod-tile-status">
  1639. <option value="published">published</option>
  1640. <option value="active">active</option>
  1641. <option value="draft">draft</option>
  1642. </select>
  1643. </label>
  1644. </div>
  1645. <div class="row two">
  1646. <label>Tile Base URL
  1647. <input id="prod-tile-base-url" value="https://example.com/tiles/demo/">
  1648. </label>
  1649. <label>Tile Meta URL
  1650. <input id="prod-tile-meta-url" value="https://example.com/tiles/demo/meta.json">
  1651. </label>
  1652. </div>
  1653. <div class="actions">
  1654. <button id="btn-prod-tile-create">Create Tile Release</button>
  1655. </div>
  1656. <div class="muted-note">正式瓦片导入已迁到运维后台:<a href="/admin/ops-workbench" target="_blank" rel="noopener noreferrer">/admin/ops-workbench</a></div>
  1657. </div>
  1658. <div class="subpanel">
  1659. <div class="muted-note">B. 赛道与 KML</div>
  1660. <div class="row two">
  1661. <label>Course Source ID
  1662. <input id="prod-course-source-id" placeholder="list/create 后填入">
  1663. </label>
  1664. <label>Legacy Playfield ID
  1665. <input id="prod-course-source-legacy-playfield-id" placeholder="可选,复用 /admin/playfields 的 id">
  1666. </label>
  1667. </div>
  1668. <div class="row two">
  1669. <label>Legacy Playfield Version ID
  1670. <input id="prod-course-source-legacy-version-id" placeholder="可选,复用 /admin/playfields 版本 id">
  1671. </label>
  1672. <label>Source Type
  1673. <input id="prod-course-source-type" value="kml">
  1674. </label>
  1675. </div>
  1676. <div class="row two">
  1677. <label>Source File URL
  1678. <input id="prod-course-source-file-url" value="https://example.com/course/demo.kml">
  1679. </label>
  1680. <label>Import Status
  1681. <select id="prod-course-source-status">
  1682. <option value="imported">imported</option>
  1683. <option value="parsed">parsed</option>
  1684. <option value="draft">draft</option>
  1685. </select>
  1686. </label>
  1687. </div>
  1688. <div class="actions">
  1689. <button id="btn-prod-course-sources-list">List Sources</button>
  1690. <button id="btn-prod-course-source-create">Create Source</button>
  1691. <button class="ghost" id="btn-prod-course-source-detail">Get Source</button>
  1692. </div>
  1693. <div class="row two">
  1694. <label>Course Set Code
  1695. <input id="prod-course-set-code" value="cset-demo-001">
  1696. </label>
  1697. <label>Course Set Name
  1698. <input id="prod-course-set-name" value="Demo Course Set">
  1699. </label>
  1700. </div>
  1701. <div class="row two">
  1702. <label>Course Set ID
  1703. <input id="prod-course-set-id" placeholder="create/detail 后填入">
  1704. </label>
  1705. <label>Course Mode
  1706. <input id="prod-course-mode" value="classic-sequential">
  1707. </label>
  1708. </div>
  1709. <div class="row two">
  1710. <label>Course Set Status
  1711. <select id="prod-course-set-status">
  1712. <option value="active">active</option>
  1713. <option value="draft">draft</option>
  1714. <option value="disabled">disabled</option>
  1715. </select>
  1716. </label>
  1717. <label>Course Variant ID
  1718. <input id="prod-course-variant-id" placeholder="create/detail 后填入">
  1719. </label>
  1720. </div>
  1721. <div class="actions">
  1722. <button id="btn-prod-course-set-create">Create Course Set</button>
  1723. <button class="ghost" id="btn-prod-course-set-detail">Get Course Set</button>
  1724. </div>
  1725. <div class="row two">
  1726. <label>Variant Name
  1727. <input id="prod-course-variant-name" value="Demo Variant A">
  1728. </label>
  1729. <label>Variant Route Code
  1730. <input id="prod-course-variant-route-code" value="route-demo-a">
  1731. </label>
  1732. </div>
  1733. <div class="row two">
  1734. <label>Variant Status
  1735. <select id="prod-course-variant-status">
  1736. <option value="active">active</option>
  1737. <option value="draft">draft</option>
  1738. <option value="disabled">disabled</option>
  1739. </select>
  1740. </label>
  1741. <label>Variant Control Count
  1742. <input id="prod-course-variant-control-count" type="number" value="8">
  1743. </label>
  1744. </div>
  1745. <div class="actions">
  1746. <button id="btn-prod-course-variant-create">Create Variant</button>
  1747. </div>
  1748. <div class="row one" style="margin-top:12px;">
  1749. <label>Default Route Code
  1750. <input id="prod-course-default-route-code" value="route-variant-d" placeholder="例如 route-variant-d">
  1751. </label>
  1752. </div>
  1753. <div class="row one">
  1754. <label>KML Batch JSON
  1755. <textarea id="prod-course-routes-json" placeholder='例如:[{"name":"路线 01","routeCode":"route-variant-a","fileUrl":"https://.../route01.kml","sourceType":"kml","controlCount":10,"status":"active"}]'>[{"name":"路线 01","routeCode":"route-variant-a","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route01.kml","sourceType":"kml","controlCount":10,"status":"active"},{"name":"路线 02","routeCode":"route-variant-b","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route02.kml","sourceType":"kml","controlCount":10,"status":"active"},{"name":"路线 03","routeCode":"route-variant-c","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route03.kml","sourceType":"kml","controlCount":10,"status":"active"},{"name":"路线 04","routeCode":"route-variant-d","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route04.kml","sourceType":"kml","controlCount":10,"status":"active"}]</textarea>
  1756. </label>
  1757. </div>
  1758. <div class="actions">
  1759. </div>
  1760. <div class="muted-note">正式 KML 批量导入已迁到运维后台:<a href="/admin/ops-workbench" target="_blank" rel="noopener noreferrer">/admin/ops-workbench</a></div>
  1761. </div>
  1762. <div class="subpanel">
  1763. <div class="muted-note">C. 运行绑定</div>
  1764. <div class="row two">
  1765. <label>Runtime Binding ID
  1766. <input id="prod-runtime-binding-id" placeholder="list/create 后填入">
  1767. </label>
  1768. <label>Runtime Event ID
  1769. <input id="prod-runtime-event-id" value="evt_demo_001">
  1770. </label>
  1771. </div>
  1772. <div class="row two">
  1773. <label>Runtime Binding Status
  1774. <select id="prod-runtime-binding-status">
  1775. <option value="active">active</option>
  1776. <option value="draft">draft</option>
  1777. <option value="disabled">disabled</option>
  1778. </select>
  1779. </label>
  1780. <label>Runtime Notes
  1781. <input id="prod-runtime-notes" value="workbench runtime binding">
  1782. </label>
  1783. </div>
  1784. <div class="actions">
  1785. <button id="btn-prod-runtime-bindings-list">List Runtime Bindings</button>
  1786. <button id="btn-prod-runtime-binding-create">Create Runtime Binding</button>
  1787. <button class="ghost" id="btn-prod-runtime-binding-detail">Get Runtime Binding</button>
  1788. </div>
  1789. </div>
  1790. </section>
  1791. <section class="panel" data-modes="admin">
  1792. <h2>资源对象管理</h2>
  1793. <p>管理地图、赛场和资源包对象,先建对象,再建版本,后面 Event source 直接引用这些对象。</p>
  1794. <div class="row two">
  1795. <label>Map Code
  1796. <input id="admin-map-code" value="map-demo-001">
  1797. </label>
  1798. <label>Map Name
  1799. <input id="admin-map-name" value="Demo Park Map">
  1800. </label>
  1801. </div>
  1802. <div class="row two">
  1803. <label>Map ID
  1804. <input id="admin-map-id" placeholder="create/list 后填入">
  1805. </label>
  1806. <label>Map Version ID
  1807. <input id="admin-map-version-id" placeholder="create version 后填入">
  1808. </label>
  1809. </div>
  1810. <div class="row two">
  1811. <label>Map Version Code
  1812. <input id="admin-map-version-code" value="v2026-04-02">
  1813. </label>
  1814. <label>Map Status
  1815. <select id="admin-map-status">
  1816. <option value="active">active</option>
  1817. <option value="draft">draft</option>
  1818. <option value="inactive">inactive</option>
  1819. </select>
  1820. </label>
  1821. </div>
  1822. <div class="row two">
  1823. <label>Mapmeta URL
  1824. <input id="admin-mapmeta-url" value="https://example.com/maps/demo/mapmeta.json">
  1825. </label>
  1826. <label>Tiles Root URL
  1827. <input id="admin-tiles-root-url" value="https://example.com/maps/demo/tiles/">
  1828. </label>
  1829. </div>
  1830. <div class="actions">
  1831. <button id="btn-admin-maps-list">List Maps</button>
  1832. <button id="btn-admin-map-create">Create Map</button>
  1833. <button class="secondary" id="btn-admin-map-version">Create Map Version</button>
  1834. <button class="ghost" id="btn-admin-map-detail">Get Map</button>
  1835. </div>
  1836. <div class="row two">
  1837. <label>Playfield Code
  1838. <input id="admin-playfield-code" value="pf-demo-001">
  1839. </label>
  1840. <label>Playfield Name
  1841. <input id="admin-playfield-name" value="Demo Course">
  1842. </label>
  1843. </div>
  1844. <div class="row two">
  1845. <label>Playfield ID
  1846. <input id="admin-playfield-id" placeholder="create/list 后填入">
  1847. </label>
  1848. <label>Playfield Version ID
  1849. <input id="admin-playfield-version-id" placeholder="create version 后填入">
  1850. </label>
  1851. </div>
  1852. <div class="row two">
  1853. <label>Playfield Kind
  1854. <select id="admin-playfield-kind">
  1855. <option value="course">course</option>
  1856. <option value="score">score</option>
  1857. <option value="custom">custom</option>
  1858. </select>
  1859. </label>
  1860. <label>Playfield Status
  1861. <select id="admin-playfield-status">
  1862. <option value="active">active</option>
  1863. <option value="draft">draft</option>
  1864. <option value="inactive">inactive</option>
  1865. </select>
  1866. </label>
  1867. </div>
  1868. <div class="row two">
  1869. <label>Playfield Version Code
  1870. <input id="admin-playfield-version-code" value="v2026-04-02">
  1871. </label>
  1872. <label>Playfield Source Type
  1873. <input id="admin-playfield-source-type" value="kml">
  1874. </label>
  1875. </div>
  1876. <div class="row two">
  1877. <label>Playfield Source URL
  1878. <input id="admin-playfield-source-url" value="https://example.com/playfields/demo/course.kml">
  1879. </label>
  1880. <label>Control Count
  1881. <input id="admin-playfield-control-count" type="number" value="8">
  1882. </label>
  1883. </div>
  1884. <div class="actions">
  1885. <button id="btn-admin-playfields-list">List Playfields</button>
  1886. <button id="btn-admin-playfield-create">Create Playfield</button>
  1887. <button class="secondary" id="btn-admin-playfield-version">Create Playfield Version</button>
  1888. <button class="ghost" id="btn-admin-playfield-detail">Get Playfield</button>
  1889. </div>
  1890. <div class="row two">
  1891. <label>Pack Code
  1892. <input id="admin-pack-code" value="pack-demo-001">
  1893. </label>
  1894. <label>Pack Name
  1895. <input id="admin-pack-name" value="Demo Resource Pack">
  1896. </label>
  1897. </div>
  1898. <div class="row two">
  1899. <label>Pack ID
  1900. <input id="admin-pack-id" placeholder="create/list 后填入">
  1901. </label>
  1902. <label>Pack Version ID
  1903. <input id="admin-pack-version-id" placeholder="create version 后填入">
  1904. </label>
  1905. </div>
  1906. <div class="row two">
  1907. <label>Pack Version Code
  1908. <input id="admin-pack-version-code" value="v2026-04-02">
  1909. </label>
  1910. <label>Pack Status
  1911. <select id="admin-pack-status">
  1912. <option value="active">active</option>
  1913. <option value="draft">draft</option>
  1914. <option value="inactive">inactive</option>
  1915. </select>
  1916. </label>
  1917. </div>
  1918. <div class="row two">
  1919. <label>Content Entry URL
  1920. <input id="admin-pack-content-url" value="https://example.com/packs/demo/content.html">
  1921. </label>
  1922. <label>Audio Root URL
  1923. <input id="admin-pack-audio-url" value="https://example.com/packs/demo/audio/">
  1924. </label>
  1925. </div>
  1926. <div class="row two">
  1927. <label>Theme Profile Code
  1928. <input id="admin-pack-theme-code" value="theme-demo">
  1929. </label>
  1930. <label>Published Asset Root
  1931. <input id="admin-published-asset-root" value="">
  1932. </label>
  1933. </div>
  1934. <div class="actions">
  1935. <button id="btn-admin-packs-list">List Packs</button>
  1936. <button id="btn-admin-pack-create">Create Pack</button>
  1937. <button class="secondary" id="btn-admin-pack-version">Create Pack Version</button>
  1938. <button class="ghost" id="btn-admin-pack-detail">Get Pack</button>
  1939. </div>
  1940. </section>
  1941. <section class="panel" data-modes="config admin">
  1942. <h2>Event Source 组装</h2>
  1943. <p>创建 Event 并把 map version、playfield version、resource pack version 组装成 source config。</p>
  1944. <div class="row two">
  1945. <label>Tenant Code
  1946. <input id="admin-tenant-code" value="tenant_demo">
  1947. </label>
  1948. <label>Event Status
  1949. <select id="admin-event-status">
  1950. <option value="active">active</option>
  1951. <option value="draft">draft</option>
  1952. <option value="inactive">inactive</option>
  1953. </select>
  1954. </label>
  1955. </div>
  1956. <div class="row two">
  1957. <label>Event ID
  1958. <input id="admin-event-ref-id" value="evt_demo_001">
  1959. </label>
  1960. <label>Event Slug
  1961. <input id="admin-event-slug" value="demo-city-run">
  1962. </label>
  1963. </div>
  1964. <div class="row">
  1965. <label>Event Display Name
  1966. <input id="admin-event-name" value="Demo City Run">
  1967. </label>
  1968. </div>
  1969. <div class="row">
  1970. <label>Event Summary
  1971. <textarea id="admin-event-summary" placeholder="后台第一版 Event 摘要">后台第一版 Event,用于资源对象组装和发布链路验证。</textarea>
  1972. </label>
  1973. </div>
  1974. <div class="row two">
  1975. <label>Game Mode Code
  1976. <input id="admin-game-mode-code" value="classic-sequential">
  1977. </label>
  1978. <label>Route Code
  1979. <input id="admin-route-code" value="route-demo-001">
  1980. </label>
  1981. </div>
  1982. <div class="row">
  1983. <label>Source Notes
  1984. <input id="admin-source-notes" value="workbench assembled source">
  1985. </label>
  1986. </div>
  1987. <div class="row">
  1988. <label>Overrides JSON
  1989. <textarea id="admin-overrides-json" placeholder='例如:{"game":{"presentation":{"showCompass":true}}}'>{}</textarea>
  1990. </label>
  1991. </div>
  1992. <div class="actions">
  1993. <button id="btn-admin-events-list">List Events</button>
  1994. <button id="btn-admin-event-create">Create Event</button>
  1995. <button class="secondary" id="btn-admin-event-update">Update Event</button>
  1996. <button class="ghost" id="btn-admin-event-detail">Get Event</button>
  1997. <button class="ghost" id="btn-admin-event-source">Assemble Source</button>
  1998. </div>
  1999. <div class="subpanel">
  2000. <div class="muted-note">Event Presentation</div>
  2001. <div class="row two">
  2002. <label>Presentation ID
  2003. <input id="admin-presentation-id" placeholder="list/create/detail 后填入">
  2004. </label>
  2005. <label>Presentation Code
  2006. <input id="admin-presentation-code" value="presentation-demo-001">
  2007. </label>
  2008. </div>
  2009. <div class="row two">
  2010. <label>Presentation Name
  2011. <input id="admin-presentation-name" value="Demo Event Presentation">
  2012. </label>
  2013. <label>Presentation Type
  2014. <select id="admin-presentation-type">
  2015. <option value="card">card</option>
  2016. <option value="detail">detail</option>
  2017. <option value="h5">h5</option>
  2018. <option value="result">result</option>
  2019. <option value="generic">generic</option>
  2020. </select>
  2021. </label>
  2022. </div>
  2023. <div class="row">
  2024. <label>Presentation Schema JSON
  2025. <textarea id="admin-presentation-schema-json" placeholder='例如:{"card":{"title":"Demo City Run"}}'>{"card":{"title":"Demo City Run"},"detail":{"template":"event-detail-default"}}</textarea>
  2026. </label>
  2027. </div>
  2028. <div class="actions">
  2029. <button id="btn-admin-presentations-list">List Presentations</button>
  2030. <button id="btn-admin-presentation-create">Create Presentation</button>
  2031. <button class="secondary" id="btn-admin-presentation-import">Import Presentation</button>
  2032. <button class="ghost" id="btn-admin-presentation-detail">Get Presentation</button>
  2033. </div>
  2034. <div class="row two">
  2035. <label>Import Title
  2036. <input id="admin-presentation-import-title" value="领秀城公园顺序赛展示定义">
  2037. </label>
  2038. <label>Template Key
  2039. <input id="admin-presentation-import-template-key" value="event.detail.standard">
  2040. </label>
  2041. </div>
  2042. <div class="row two">
  2043. <label>Source Type
  2044. <input id="admin-presentation-import-source-type" value="schema">
  2045. </label>
  2046. <label>Version
  2047. <input id="admin-presentation-import-version" value="v2026-04-03">
  2048. </label>
  2049. </div>
  2050. <div class="row">
  2051. <label>Schema URL
  2052. <input id="admin-presentation-import-schema-url" value="">
  2053. </label>
  2054. </div>
  2055. </div>
  2056. <div class="subpanel">
  2057. <div class="muted-note">Content Bundle</div>
  2058. <div class="row two">
  2059. <label>Content Bundle ID
  2060. <input id="admin-content-bundle-id" placeholder="list/create/detail 后填入">
  2061. </label>
  2062. <label>Content Bundle Code
  2063. <input id="admin-content-bundle-code" value="bundle-demo-001">
  2064. </label>
  2065. </div>
  2066. <div class="row two">
  2067. <label>Content Bundle Name
  2068. <input id="admin-content-bundle-name" value="领秀城公园内容包">
  2069. </label>
  2070. <label>Content Entry URL
  2071. <input id="admin-content-entry-url" value="https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html">
  2072. </label>
  2073. </div>
  2074. <div class="row two">
  2075. <label>Content Asset Root URL
  2076. <input id="admin-content-asset-root-url" value="https://oss-mbh5.colormaprun.com/gotomars/assets/">
  2077. </label>
  2078. <label>Content Metadata JSON
  2079. <textarea id="admin-content-metadata-json" placeholder='例如:{"resultTemplate":"default"}'>{"resultTemplate":"default","locale":"zh-CN","theme":"city-park"}</textarea>
  2080. </label>
  2081. </div>
  2082. <div class="actions">
  2083. <button id="btn-admin-content-bundles-list">List Bundles</button>
  2084. <button id="btn-admin-content-bundle-create">Create Bundle</button>
  2085. <button class="secondary" id="btn-admin-content-bundle-import">Import Bundle</button>
  2086. <button class="ghost" id="btn-admin-content-bundle-detail">Get Bundle</button>
  2087. </div>
  2088. <div class="row two">
  2089. <label>Import Title
  2090. <input id="admin-content-import-title" value="领秀城公园顺序赛内容包">
  2091. </label>
  2092. <label>Bundle Type
  2093. <input id="admin-content-import-bundle-type" value="result_media">
  2094. </label>
  2095. </div>
  2096. <div class="row two">
  2097. <label>Source Type
  2098. <input id="admin-content-import-source-type" value="manifest">
  2099. </label>
  2100. <label>Version
  2101. <input id="admin-content-import-version" value="v2026-04-03">
  2102. </label>
  2103. </div>
  2104. <div class="row two">
  2105. <label>Manifest URL
  2106. <input id="admin-content-import-manifest-url" value="">
  2107. </label>
  2108. <label>Asset Manifest JSON
  2109. <textarea id="admin-content-import-asset-manifest-json" placeholder='例如:{"manifestUrl":"https://example.com/content/demo/manifest.json"}'></textarea>
  2110. </label>
  2111. </div>
  2112. </div>
  2113. </section>
  2114. </div>
  2115. <div class="category-head" id="nav-tools" data-modes="common">
  2116. <div class="category-kicker">Tools</div>
  2117. <h2>辅助工具</h2>
  2118. <p>保存场景、查看日志、复制 curl、回看请求历史,都放在这里。</p>
  2119. </div>
  2120. <div class="grid" style="margin-top:16px;">
  2121. <section class="panel" data-modes="config admin">
  2122. <h2>Build / Publish / Rollback</h2>
  2123. <p>围绕当前 Event 查询 source/build/release 流水线,并执行 build、publish、rollback。</p>
  2124. <div class="row two">
  2125. <label>Source ID
  2126. <input id="admin-pipeline-source-id" placeholder="source 组装或导入后自动填充">
  2127. </label>
  2128. <label>Build ID
  2129. <input id="admin-pipeline-build-id" placeholder="build 后自动填充">
  2130. </label>
  2131. </div>
  2132. <div class="row two">
  2133. <label>Release ID
  2134. <input id="admin-pipeline-release-id" value="rel_demo_001">
  2135. </label>
  2136. <label>Rollback Release ID
  2137. <input id="admin-rollback-release-id" placeholder="输入要切回的 releaseId">
  2138. </label>
  2139. </div>
  2140. <div class="row two">
  2141. <label>Runtime Binding ID
  2142. <input id="admin-release-runtime-binding-id" placeholder="输入或复用已创建的 runtimeBindingId">
  2143. </label>
  2144. <div class="muted-note">第四刀:发布时可直接带 runtime binding;旧的“先发布再绑定”路径继续保留。</div>
  2145. </div>
  2146. <div class="row two">
  2147. <label>Presentation ID
  2148. <input id="admin-release-presentation-id" placeholder="可选,发布时绑定 presentation">
  2149. </label>
  2150. <label>Content Bundle ID
  2151. <input id="admin-release-content-bundle-id" placeholder="可选,发布时绑定内容包">
  2152. </label>
  2153. </div>
  2154. <div class="actions">
  2155. <button class="ghost" id="btn-admin-event-defaults">Save Event Defaults</button>
  2156. </div>
  2157. <div class="muted-note">不填发布参数时,后端会先继承当前 Event 的默认 active:presentation / content bundle / runtime。</div>
  2158. <div class="actions">
  2159. <button id="btn-admin-pipeline">Get Pipeline</button>
  2160. <button id="btn-admin-build-source">Build Source</button>
  2161. <button class="secondary" id="btn-admin-build-detail">Get Build</button>
  2162. <button class="secondary" id="btn-admin-build-publish">Publish Build</button>
  2163. <button class="secondary" id="btn-admin-release-detail">Get Release</button>
  2164. <button class="secondary" id="btn-admin-bind-runtime">Bind Runtime</button>
  2165. <button class="ghost" id="btn-admin-rollback">Rollback Release</button>
  2166. </div>
  2167. </section>
  2168. <section class="panel" data-modes="common">
  2169. <h2>场景模板</h2>
  2170. <p>保存当前表单状态为可复用场景,也支持导入导出 JSON,适合后续切换不同俱乐部、入口和 event。</p>
  2171. <div class="row two">
  2172. <label>Scenario Name
  2173. <input id="scenario-name" placeholder="例如:俱乐部A-小程序-Launch流">
  2174. </label>
  2175. <label>Saved / Preset
  2176. <select id="scenario-select"></select>
  2177. </label>
  2178. </div>
  2179. <div class="actions">
  2180. <button id="btn-scenario-save">Save Current</button>
  2181. <button class="secondary" id="btn-scenario-load">Load Selected</button>
  2182. <button class="ghost" id="btn-scenario-delete">Delete Selected</button>
  2183. </div>
  2184. <div class="subpanel">
  2185. <div class="muted-note">Scenario JSON</div>
  2186. <textarea id="scenario-json" placeholder="导出后可复制,导入时贴回这里"></textarea>
  2187. <div class="actions">
  2188. <button id="btn-scenario-export">Export Selected</button>
  2189. <button class="secondary" id="btn-scenario-import">Import JSON</button>
  2190. </div>
  2191. </div>
  2192. </section>
  2193. <section class="panel" data-modes="common">
  2194. <h2>响应日志</h2>
  2195. <p>最后一次请求的结果会记录在这里,便于后续做请求回放和用例保存。</p>
  2196. <div id="status" class="status">ready</div>
  2197. <div class="progress-card">
  2198. <div class="progress-meta">
  2199. <span id="progress-label" class="progress-label">当前进度:待执行</span>
  2200. <span id="progress-step">0 / 0</span>
  2201. </div>
  2202. <div class="progress-track"><div id="progress-fill" class="progress-fill"></div></div>
  2203. <div id="progress-note" class="progress-note">长流程会在这里显示当前步骤。</div>
  2204. </div>
  2205. <div id="log" class="log"></div>
  2206. </section>
  2207. </div>
  2208. <div class="grid" style="margin-top:16px;">
  2209. <section class="panel" data-modes="common">
  2210. <h2>请求历史</h2>
  2211. <p>最近 12 次请求会保留在浏览器本地,刷新页面不会丢。</p>
  2212. <div id="history" class="history"></div>
  2213. </section>
  2214. </div>
  2215. <div class="category-head" id="nav-api" data-modes="reference">
  2216. <div class="category-kicker">Reference</div>
  2217. <h2>API 目录 <span id="api-total-count">(0)</span></h2>
  2218. <p>需要查路径、参数、鉴权方式时再展开这一块,不影响主链调试。</p>
  2219. </div>
  2220. <div class="grid" style="margin-top:16px;" data-modes="reference">
  2221. <section class="panel" data-modes="reference">
  2222. <h2>API 目录</h2>
  2223. <p>把当前已实现接口按分组放进 workbench,直接看中文说明、鉴权要求和关键参数,不用来回翻文档。</p>
  2224. <div class="api-toolbar">
  2225. <input id="api-filter" placeholder="搜索路径、用途、参数,例如 launch / wechat / result">
  2226. <div class="muted-note" id="api-filter-meta">共 0 个接口,支持按关键词筛选。</div>
  2227. </div>
  2228. <div id="api-summary" class="api-summary"></div>
  2229. <div id="api-catalog" class="api-catalog">
  2230. <div class="api-item" data-api="healthz 健康检查 服务状态">
  2231. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/healthz</span></div>
  2232. <div class="api-desc">健康检查接口,用来确认服务是否存活。</div>
  2233. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2234. </div>
  2235. <div class="api-item" data-api="auth sms send 验证码 登录 绑定 手机">
  2236. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/auth/sms/send</span></div>
  2237. <div class="api-desc">发送短信验证码,支持登录和绑定手机号两种场景。</div>
  2238. <div class="api-meta">
  2239. <div><strong>鉴权:</strong>无需鉴权</div>
  2240. <div><strong>关键参数:</strong><code>countryCode</code>、<code>mobile</code>、<code>clientType</code>、<code>deviceKey</code>、<code>scene</code></div>
  2241. </div>
  2242. </div>
  2243. <div class="api-item" data-api="auth login sms app 手机号 验证码 登录">
  2244. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/auth/login/sms</span></div>
  2245. <div class="api-desc">APP 主登录入口,使用手机号验证码登录并返回 access/refresh token。</div>
  2246. <div class="api-meta">
  2247. <div><strong>鉴权:</strong>无需鉴权</div>
  2248. <div><strong>关键参数:</strong><code>mobile</code>、<code>code</code>、<code>clientType</code>、<code>deviceKey</code></div>
  2249. </div>
  2250. </div>
  2251. <div class="api-item" data-api="auth login wechat mini 微信 小程序 code openid 登录">
  2252. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/auth/login/wechat-mini</span></div>
  2253. <div class="api-desc">微信小程序登录入口。开发环境支持 <code>dev-</code> 前缀 code 直接模拟登录。</div>
  2254. <div class="api-meta">
  2255. <div><strong>鉴权:</strong>无需鉴权</div>
  2256. <div><strong>关键参数:</strong><code>code</code>、<code>clientType=wechat</code>、<code>deviceKey</code></div>
  2257. </div>
  2258. </div>
  2259. <div class="api-item" data-api="auth bind mobile 绑定 手机号 合并 账号">
  2260. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/auth/bind/mobile</span></div>
  2261. <div class="api-desc">已登录用户绑定手机号,必要时把微信轻账号合并到手机号主账号。</div>
  2262. <div class="api-meta">
  2263. <div><strong>鉴权:</strong>Bearer token</div>
  2264. <div><strong>关键参数:</strong><code>mobile</code>、<code>code</code>、<code>clientType</code>、<code>deviceKey</code></div>
  2265. </div>
  2266. </div>
  2267. <div class="api-item" data-api="auth refresh token 刷新 登录态">
  2268. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/auth/refresh</span></div>
  2269. <div class="api-desc">使用 refresh token 刷新 access token。</div>
  2270. <div class="api-meta">
  2271. <div><strong>鉴权:</strong>无需 Bearer token</div>
  2272. <div><strong>关键参数:</strong><code>refreshToken</code>、<code>clientType</code>、<code>deviceKey</code></div>
  2273. </div>
  2274. </div>
  2275. <div class="api-item" data-api="auth logout 登出 撤销 refresh token">
  2276. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/auth/logout</span></div>
  2277. <div class="api-desc">登出并撤销 refresh token。</div>
  2278. <div class="api-meta"><div><strong>鉴权:</strong>可带 Bearer token</div></div>
  2279. </div>
  2280. <div class="api-item" data-api="entry resolve tenant channel 入口 解析">
  2281. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/entry/resolve</span></div>
  2282. <div class="api-desc">解析当前入口属于哪个 tenant / channel,是多俱乐部、多公众号接入的入口层基础接口。</div>
  2283. <div class="api-meta">
  2284. <div><strong>鉴权:</strong>无需鉴权</div>
  2285. <div><strong>查询参数:</strong><code>channelCode</code>、<code>channelType</code>、<code>platformAppId</code>、<code>tenantCode</code></div>
  2286. </div>
  2287. </div>
  2288. <div class="api-item" data-api="admin assets list 运维 资源 纳管 列表">
  2289. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/assets</span></div>
  2290. <div class="api-desc">列出当前 backend 已纳管的正式资源对象,统一查看上传文件和外链登记结果。</div>
  2291. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2292. </div>
  2293. <div class="api-item" data-api="admin assets register link 运维 外链 登记">
  2294. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/assets/register-link</span></div>
  2295. <div class="api-desc">登记一个已有正式外链为受管资源,不要求先上传文件。</div>
  2296. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2297. </div>
  2298. <div class="api-item" data-api="admin assets upload 运维 文件 上传 oss">
  2299. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/assets/upload</span></div>
  2300. <div class="api-desc">通过 backend 上传文件到 OSS,并自动登记为受管资源对象。</div>
  2301. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2302. </div>
  2303. <div class="api-item" data-api="admin asset detail 运维 资源 明细">
  2304. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/assets/{assetPublicID}</span></div>
  2305. <div class="api-desc">查看单个已纳管资源的明细,包括 publicUrl、objectKey、版本、checksum 等。</div>
  2306. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2307. </div>
  2308. <div class="api-item" data-api="home 首页 卡片 列表">
  2309. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/home</span></div>
  2310. <div class="api-desc">返回入口首页卡片数据。</div>
  2311. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2312. </div>
  2313. <div class="api-item" data-api="cards 卡片 列表">
  2314. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/cards</span></div>
  2315. <div class="api-desc">只返回卡片列表,适合调试卡片数据本身。</div>
  2316. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2317. </div>
  2318. <div class="api-item" data-api="experience maps 地图 列表 默认体验 活动">
  2319. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/experience-maps</span></div>
  2320. <div class="api-desc">地图资源列表接口,返回 place/map 摘要、默认体验活动数量和默认活动 ID 列表。</div>
  2321. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2322. </div>
  2323. <div class="api-item" data-api="experience map detail 地图 详情 默认体验 活动 摘要">
  2324. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/experience-maps/{mapAssetPublicID}</span></div>
  2325. <div class="api-desc">地图详情接口,返回地图基础信息和当前挂在该地图下的默认体验活动摘要。</div>
  2326. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2327. </div>
  2328. <div class="api-item" data-api="public experience maps 游客 地图 列表 默认体验">
  2329. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/public/experience-maps</span></div>
  2330. <div class="api-desc">游客模式地图列表,返回可公开体验的地图与默认活动摘要。</div>
  2331. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2332. </div>
  2333. <div class="api-item" data-api="public experience map detail 游客 地图 详情 默认体验">
  2334. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/public/experience-maps/{mapAssetPublicID}</span></div>
  2335. <div class="api-desc">游客模式地图详情,返回该地图下的默认体验活动列表。</div>
  2336. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2337. </div>
  2338. <div class="api-item" data-api="me entry home 首页 聚合 ongoing recent">
  2339. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/me/entry-home</span></div>
  2340. <div class="api-desc">首页聚合接口,返回用户、tenant、channel、cards、进行中 session 和最近一局。</div>
  2341. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2342. </div>
  2343. <div class="api-item" data-api="event detail 活动 详情 release resolvedRelease">
  2344. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/events/{eventPublicID}</span></div>
  2345. <div class="api-desc">活动详情接口,会带当前发布的 release 和 resolvedRelease。</div>
  2346. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2347. </div>
  2348. <div class="api-item" data-api="public event detail 游客 活动 详情 默认体验">
  2349. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/public/events/{eventPublicID}</span></div>
  2350. <div class="api-desc">游客模式活动详情,只允许默认体验活动进入。</div>
  2351. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2352. </div>
  2353. <div class="api-item" data-api="event play 活动 准备页 聚合 canLaunch continue review variant assignmentMode courseVariants">
  2354. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/events/{eventPublicID}/play</span></div>
  2355. <div class="api-desc">活动详情页 / 开始前准备页聚合接口,判断是否可启动、继续还是查看上次结果;第一阶段也会返回多赛道 assignmentMode 和 courseVariants。</div>
  2356. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2357. </div>
  2358. <div class="api-item" data-api="public event play 游客 活动 准备页 canLaunch variant assignmentMode courseVariants">
  2359. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/public/events/{eventPublicID}/play</span></div>
  2360. <div class="api-desc">游客模式准备页聚合接口,返回 canLaunch、多赛道信息和 preview。</div>
  2361. <div class="api-meta"><div><strong>鉴权:</strong>无需鉴权</div></div>
  2362. </div>
  2363. <div class="api-item" data-api="event launch 启动 一局 release manifest sessionToken variantId assignmentMode">
  2364. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/events/{eventPublicID}/launch</span></div>
  2365. <div class="api-desc">基于当前 event 的已发布 release 创建一局 session,并返回 config URL、releaseId、sessionToken;多赛道第一阶段支持可选 variantId,并返回最终绑定的 launch.variant。</div>
  2366. <div class="api-meta">
  2367. <div><strong>鉴权:</strong>Bearer token</div>
  2368. <div><strong>关键参数:</strong><code>releaseId</code>、<code>variantId</code>、<code>clientType</code>、<code>deviceKey</code></div>
  2369. </div>
  2370. </div>
  2371. <div class="api-item" data-api="public event launch 游客 启动 一局 release sessionToken isGuest variantId">
  2372. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/public/events/{eventPublicID}/launch</span></div>
  2373. <div class="api-desc">游客模式启动一局,返回与正式 launch 基本同构的结构,并带 business.isGuest = true。</div>
  2374. <div class="api-meta">
  2375. <div><strong>鉴权:</strong>无需鉴权</div>
  2376. <div><strong>关键参数:</strong><code>releaseId</code>、<code>variantId</code>、<code>clientType</code>、<code>deviceKey</code></div>
  2377. </div>
  2378. </div>
  2379. <div class="api-item" data-api="config sources event source 配置 列表">
  2380. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/events/{eventPublicID}/config-sources</span></div>
  2381. <div class="api-desc">查看某个 event 下已经导入过的 source config 列表。</div>
  2382. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2383. </div>
  2384. <div class="api-item" data-api="config source detail 源配置 明细">
  2385. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/config-sources/{sourceID}</span></div>
  2386. <div class="api-desc">查看单条 source config 明细。</div>
  2387. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2388. </div>
  2389. <div class="api-item" data-api="config build detail 预览 build 明细 manifest assets">
  2390. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/config-builds/{buildID}</span></div>
  2391. <div class="api-desc">查看单次 build 的 manifest 和 asset index。</div>
  2392. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2393. </div>
  2394. <div class="api-item" data-api="session detail 一局 详情 resolvedRelease">
  2395. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/sessions/{sessionPublicID}</span></div>
  2396. <div class="api-desc">查询一局详情,带 session 状态、event 和 resolvedRelease。</div>
  2397. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2398. </div>
  2399. <div class="api-item" data-api="session start running 开始 一局 sessionToken">
  2400. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/sessions/{sessionPublicID}/start</span></div>
  2401. <div class="api-desc">把 session 从 <code>launched</code> 推进到 <code>running</code>。</div>
  2402. <div class="api-meta">
  2403. <div><strong>鉴权:</strong><code>sessionToken</code></div>
  2404. <div><strong>关键参数:</strong><code>sessionToken</code></div>
  2405. </div>
  2406. </div>
  2407. <div class="api-item" data-api="session finish 结束 成绩 摘要 result summary sessionToken">
  2408. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/sessions/{sessionPublicID}/finish</span></div>
  2409. <div class="api-desc">结束一局并沉淀结果摘要,是结果页数据的来源。</div>
  2410. <div class="api-meta">
  2411. <div><strong>鉴权:</strong><code>sessionToken</code></div>
  2412. <div><strong>关键参数:</strong><code>sessionToken</code>、<code>status</code>、<code>summary.*</code></div>
  2413. </div>
  2414. </div>
  2415. <div class="api-item" data-api="me sessions 我的 最近 局 列表">
  2416. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/me/sessions</span></div>
  2417. <div class="api-desc">查询用户最近 session 列表。</div>
  2418. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2419. </div>
  2420. <div class="api-item" data-api="session result 单局 结果 页 成绩">
  2421. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/sessions/{sessionPublicID}/result</span></div>
  2422. <div class="api-desc">单局结果页接口,返回 session 和 result。</div>
  2423. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2424. </div>
  2425. <div class="api-item" data-api="me results 我的 成绩 结果 列表">
  2426. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/me/results</span></div>
  2427. <div class="api-desc">查询用户最近结果列表。</div>
  2428. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2429. </div>
  2430. <div class="api-item" data-api="me 当前用户 信息">
  2431. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/me</span></div>
  2432. <div class="api-desc">返回当前用户基础信息。</div>
  2433. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2434. </div>
  2435. <div class="api-item" data-api="me profile 我的页 聚合 绑定 最近记录">
  2436. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/me/profile</span></div>
  2437. <div class="api-desc">“我的页”聚合接口,返回绑定概览、绑定项列表和最近记录摘要。</div>
  2438. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2439. </div>
  2440. <div class="api-item" data-api="dev bootstrap demo 初始化 示例 数据">
  2441. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/dev/bootstrap-demo</span></div>
  2442. <div class="api-desc">开发态自举 demo 数据,会准备 tenant、channel、event、release、card、source、build。</div>
  2443. <div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
  2444. </div>
  2445. <div class="api-item" data-api="dev workbench 工作台 面板 调试">
  2446. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/workbench</span></div>
  2447. <div class="api-desc">开发态工作台页面,集中提供一键流、日志、配置摘要、API 目录和后台运营联调入口。</div>
  2448. <div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
  2449. </div>
  2450. <div class="api-item" data-api="admin ops workbench 运维 工作台 资源 录入 纳管">
  2451. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/ops-workbench</span></div>
  2452. <div class="api-desc">运维后台第一期页面,专门处理资源录入、OSS 纳管、地图瓦片导入和 KML 批量导入。</div>
  2453. <div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权;页面内 API 仍需先登录获取 Bearer token</div></div>
  2454. </div>
  2455. <div class="api-item" data-api="dev client logs 前端 调试 日志 上报">
  2456. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/dev/client-logs</span></div>
  2457. <div class="api-desc">接收 frontend 主动上报的调试日志,供 backend 在 workbench 中统一查看。</div>
  2458. <div class="api-meta">
  2459. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2460. <div><strong>关键参数:</strong><code>source</code>、<code>level</code>、<code>category</code>、<code>message</code>、<code>eventId</code>、<code>releaseId</code>、<code>sessionId</code>、<code>manifestUrl</code>、<code>route</code>、<code>details</code></div>
  2461. </div>
  2462. </div>
  2463. <div class="api-item" data-api="dev client logs 前端 调试 日志 列表">
  2464. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/client-logs</span></div>
  2465. <div class="api-desc">获取 frontend 最近上报的调试日志,便于 backend 直接对照排查。</div>
  2466. <div class="api-meta">
  2467. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2468. <div><strong>查询参数:</strong><code>limit</code></div>
  2469. </div>
  2470. </div>
  2471. <div class="api-item" data-api="dev client logs 前端 调试 日志 清空">
  2472. <div class="api-head"><span class="api-method">DELETE</span><span class="api-path">/dev/client-logs</span></div>
  2473. <div class="api-desc">清空当前内存中的 frontend 调试日志,方便开始新一轮联调。</div>
  2474. <div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
  2475. </div>
  2476. <div class="api-item" data-api="dev manifest summary manifest 摘要 代读 调试">
  2477. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/manifest-summary</span></div>
  2478. <div class="api-desc">由 backend 代读指定 manifest,并返回 <code>schemaVersion</code>、<code>playfield.kind</code>、<code>game.mode</code> 调试摘要。</div>
  2479. <div class="api-meta">
  2480. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2481. <div><strong>查询参数:</strong><code>url</code></div>
  2482. </div>
  2483. </div>
  2484. <div class="api-item" data-api="dev demo assets game manifest 游戏 manifest 调试样例">
  2485. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/demo-assets/manifests/{demoKey}</span></div>
  2486. <div class="api-desc">读取联调用的示例游戏 manifest,供顺序赛、积分赛、多赛道统一切换调试入口。</div>
  2487. <div class="api-meta">
  2488. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2489. <div><strong>路径参数:</strong><code>demoKey</code></div>
  2490. </div>
  2491. </div>
  2492. <div class="api-item" data-api="dev demo assets presentation 展示定义 schema 调试样例">
  2493. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/demo-assets/presentations/{demoKey}</span></div>
  2494. <div class="api-desc">读取联调用的示例展示定义 schema,给 workbench 快速导入。</div>
  2495. <div class="api-meta">
  2496. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2497. <div><strong>路径参数:</strong><code>demoKey</code></div>
  2498. </div>
  2499. </div>
  2500. <div class="api-item" data-api="dev demo assets content manifest 内容包 调试样例">
  2501. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/demo-assets/content-manifests/{demoKey}</span></div>
  2502. <div class="api-desc">读取联调用的示例内容 manifest,给 workbench 快速导入。</div>
  2503. <div class="api-meta">
  2504. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2505. <div><strong>路径参数:</strong><code>demoKey</code></div>
  2506. </div>
  2507. </div>
  2508. <div class="api-item" data-api="dev config local files 本地 配置 文件 列表">
  2509. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/config/local-files</span></div>
  2510. <div class="api-desc">列出本地配置目录中的 JSON 文件,作为 source config 导入入口。</div>
  2511. <div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
  2512. </div>
  2513. <div class="api-item" data-api="dev import local source config 导入 本地 event json">
  2514. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/dev/events/{eventPublicID}/config-sources/import-local</span></div>
  2515. <div class="api-desc">从本地 event 目录导入 source config。</div>
  2516. <div class="api-meta">
  2517. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2518. <div><strong>关键参数:</strong><code>fileName</code>、<code>notes</code></div>
  2519. </div>
  2520. </div>
  2521. <div class="api-item" data-api="dev config preview build 预览 manifest asset index">
  2522. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/dev/config-builds/preview</span></div>
  2523. <div class="api-desc">基于 source config 生成 preview build,并产出 preview manifest。</div>
  2524. <div class="api-meta">
  2525. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2526. <div><strong>关键参数:</strong><code>sourceId</code></div>
  2527. </div>
  2528. </div>
  2529. <div class="api-item" data-api="dev config publish build 发布 release 当前版本">
  2530. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/dev/config-builds/publish</span></div>
  2531. <div class="api-desc">把成功的 build 发布成正式 release,并可选直接挂接 runtime binding。</div>
  2532. <div class="api-meta">
  2533. <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
  2534. <div><strong>关键参数:</strong><code>buildId</code>、<code>runtimeBindingId</code></div>
  2535. </div>
  2536. </div>
  2537. <div class="api-item" data-api="ops auth sms send 运维 验证码">
  2538. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/ops/auth/sms/send</span></div>
  2539. <div class="api-desc">发送运维账号登录或注册验证码。</div>
  2540. <div class="api-meta"><div><strong>鉴权:</strong>生产走手机号验证码;开发环境可直连运维后台</div></div>
  2541. </div>
  2542. <div class="api-item" data-api="ops auth register 运维 注册">
  2543. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/ops/auth/register</span></div>
  2544. <div class="api-desc">注册独立运维账号,首个账号默认授予 owner。</div>
  2545. <div class="api-meta"><div><strong>鉴权:</strong>无需 Bearer token</div></div>
  2546. </div>
  2547. <div class="api-item" data-api="ops auth login sms 运维 登录">
  2548. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/ops/auth/login/sms</span></div>
  2549. <div class="api-desc">运维账号手机号验证码登录。</div>
  2550. <div class="api-meta"><div><strong>鉴权:</strong>无需 Bearer token</div></div>
  2551. </div>
  2552. <div class="api-item" data-api="ops auth refresh 运维 刷新 token">
  2553. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/ops/auth/refresh</span></div>
  2554. <div class="api-desc">刷新运维 access token。</div>
  2555. <div class="api-meta"><div><strong>鉴权:</strong>refresh token</div></div>
  2556. </div>
  2557. <div class="api-item" data-api="ops auth logout 运维 退出">
  2558. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/ops/auth/logout</span></div>
  2559. <div class="api-desc">撤销运维 refresh token。</div>
  2560. <div class="api-meta"><div><strong>鉴权:</strong>refresh token</div></div>
  2561. </div>
  2562. <div class="api-item" data-api="ops me 当前运维 用户 信息">
  2563. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/ops/me</span></div>
  2564. <div class="api-desc">获取当前运维账号信息和主角色。</div>
  2565. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token</div></div>
  2566. </div>
  2567. <div class="api-item" data-api="ops admin summary 运维 总览 统计">
  2568. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/ops/admin/summary</span></div>
  2569. <div class="api-desc">运维后台总览聚合接口,返回资源、活动、运行绑定和发布版本统计。</div>
  2570. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2571. </div>
  2572. <div class="api-item" data-api="ops admin region options 省市 列表 地点 管理">
  2573. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/ops/admin/region-options</span></div>
  2574. <div class="api-desc">返回运维后台地点管理使用的全国省市两级列表,页面可直接用于省市选择。</div>
  2575. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2576. </div>
  2577. <div class="api-item" data-api="ops admin assets list 运维 受管资源 列表">
  2578. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/ops/admin/assets</span></div>
  2579. <div class="api-desc">读取受管资源列表,给运维后台资源总览与录入校验使用。</div>
  2580. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2581. </div>
  2582. <div class="api-item" data-api="ops admin assets register link 运维 登记 外链">
  2583. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/ops/admin/assets/register-link</span></div>
  2584. <div class="api-desc">登记已有正式外链资源,统一纳管到 backend 资源对象。</div>
  2585. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2586. </div>
  2587. <div class="api-item" data-api="ops admin assets upload 运维 上传 资源">
  2588. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/ops/admin/assets/upload</span></div>
  2589. <div class="api-desc">上传文件给 backend,再统一存入 OSS,运维不用处理底层存储细节。</div>
  2590. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2591. </div>
  2592. <div class="api-item" data-api="ops admin asset detail 运维 资源 明细">
  2593. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/ops/admin/assets/{assetPublicID}</span></div>
  2594. <div class="api-desc">查看单个受管资源详情,包括类型、版本、正式 URL 和元数据。</div>
  2595. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2596. </div>
  2597. <div class="api-item" data-api="ops admin places list 运维 地点 列表">
  2598. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/ops/admin/places</span></div>
  2599. <div class="api-desc">运维后台读取地点列表,作为地图管理流程入口。</div>
  2600. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2601. </div>
  2602. <div class="api-item" data-api="ops admin places create 运维 新建 地点">
  2603. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/ops/admin/places</span></div>
  2604. <div class="api-desc">运维后台新建地点。</div>
  2605. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2606. </div>
  2607. <div class="api-item" data-api="ops admin place detail 运维 地点 明细">
  2608. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/ops/admin/places/{placePublicID}</span></div>
  2609. <div class="api-desc">运维后台读取地点详情,并带出当前地点下地图资源。</div>
  2610. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2611. </div>
  2612. <div class="api-item" data-api="ops admin map assets list 运维 地图 列表">
  2613. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/ops/admin/map-assets</span></div>
  2614. <div class="api-desc">运维后台地图列表接口,返回地点、当前瓦片版本和关联活动摘要。</div>
  2615. <div class="api-meta"><div><strong>鉴权:</strong>Ops bearer token;开发环境默认可直连</div></div>
  2616. </div>
  2617. <div class="api-item" data-api="admin maps 列表 地图 后台 资源">
  2618. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/maps</span></div>
  2619. <div class="api-desc">后台地图对象列表接口。</div>
  2620. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2621. </div>
  2622. <div class="api-item" data-api="admin maps create 创建 地图 对象 后台 资源">
  2623. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/maps</span></div>
  2624. <div class="api-desc">创建地图对象,后续再为它追加版本。</div>
  2625. <div class="api-meta">
  2626. <div><strong>鉴权:</strong>Bearer token</div>
  2627. <div><strong>关键参数:</strong><code>code</code>、<code>name</code>、<code>status</code></div>
  2628. </div>
  2629. </div>
  2630. <div class="api-item" data-api="admin map detail 地图 明细 版本 列表">
  2631. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/maps/{mapPublicID}</span></div>
  2632. <div class="api-desc">查看单个地图对象和它的版本列表。</div>
  2633. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2634. </div>
  2635. <div class="api-item" data-api="admin map version 创建 地图 版本 mapmeta tiles">
  2636. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/maps/{mapPublicID}/versions</span></div>
  2637. <div class="api-desc">为地图对象创建一个版本,挂接 mapmeta 和 tiles 根路径。</div>
  2638. <div class="api-meta">
  2639. <div><strong>鉴权:</strong>Bearer token</div>
  2640. <div><strong>关键参数:</strong><code>versionCode</code>、<code>mapmetaUrl</code>、<code>tilesRootUrl</code>、<code>setAsCurrent</code></div>
  2641. </div>
  2642. </div>
  2643. <div class="api-item" data-api="admin places 列表 地点 生产骨架">
  2644. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/places</span></div>
  2645. <div class="api-desc">第一阶段生产骨架的地点对象列表接口。</div>
  2646. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2647. </div>
  2648. <div class="api-item" data-api="admin places create 创建 地点 place 生产骨架">
  2649. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/places</span></div>
  2650. <div class="api-desc">创建地点对象,作为地图资产的上层归属。</div>
  2651. <div class="api-meta">
  2652. <div><strong>鉴权:</strong>Bearer token</div>
  2653. <div><strong>关键参数:</strong><code>code</code>、<code>name</code>、<code>region</code>、<code>status</code></div>
  2654. </div>
  2655. </div>
  2656. <div class="api-item" data-api="admin place detail 地点 明细 map assets">
  2657. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/places/{placePublicID}</span></div>
  2658. <div class="api-desc">查看地点详情,并带出该地点下的地图资产列表。</div>
  2659. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2660. </div>
  2661. <div class="api-item" data-api="admin place map asset 创建 地点 地图资产">
  2662. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/places/{placePublicID}/map-assets</span></div>
  2663. <div class="api-desc">在指定地点下创建地图资产,可选挂接已有 legacy map。</div>
  2664. <div class="api-meta">
  2665. <div><strong>鉴权:</strong>Bearer token</div>
  2666. <div><strong>关键参数:</strong><code>code</code>、<code>name</code>、<code>mapType</code>、<code>legacyMapId</code></div>
  2667. </div>
  2668. </div>
  2669. <div class="api-item" data-api="admin map asset detail 地图资产 明细 tile releases course sets">
  2670. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/map-assets/{mapAssetPublicID}</span></div>
  2671. <div class="api-desc">查看地图资产详情,带出瓦片版本和赛道集合摘要。</div>
  2672. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2673. </div>
  2674. <div class="api-item" data-api="admin tile release 创建 瓦片版本 生产骨架">
  2675. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/map-assets/{mapAssetPublicID}/tile-releases</span></div>
  2676. <div class="api-desc">为地图资产创建瓦片版本,可选关联已有 legacy map version。</div>
  2677. <div class="api-meta">
  2678. <div><strong>鉴权:</strong>Bearer token</div>
  2679. <div><strong>关键参数:</strong><code>versionCode</code>、<code>tileBaseUrl</code>、<code>metaUrl</code>、<code>setAsCurrent</code></div>
  2680. </div>
  2681. </div>
  2682. <div class="api-item" data-api="admin course source 列表 kml 输入源">
  2683. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/course-sources</span></div>
  2684. <div class="api-desc">查看赛道原始输入源列表,承接 KML / GeoJSON 等输入。</div>
  2685. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2686. </div>
  2687. <div class="api-item" data-api="admin course source 创建 kml 输入源">
  2688. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/course-sources</span></div>
  2689. <div class="api-desc">创建赛道输入源,为后续解析成 CourseVariant 做准备。</div>
  2690. <div class="api-meta">
  2691. <div><strong>鉴权:</strong>Bearer token</div>
  2692. <div><strong>关键参数:</strong><code>sourceType</code>、<code>fileUrl</code>、<code>legacyPlayfieldId</code>、<code>legacyVersionId</code></div>
  2693. </div>
  2694. </div>
  2695. <div class="api-item" data-api="admin course source detail 输入源 明细">
  2696. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/course-sources/{sourcePublicID}</span></div>
  2697. <div class="api-desc">查看单个赛道输入源详情。</div>
  2698. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2699. </div>
  2700. <div class="api-item" data-api="admin course set 创建 赛道集合 map asset">
  2701. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/map-assets/{mapAssetPublicID}/course-sets</span></div>
  2702. <div class="api-desc">在指定地图资产下创建赛道集合。</div>
  2703. <div class="api-meta">
  2704. <div><strong>鉴权:</strong>Bearer token</div>
  2705. <div><strong>关键参数:</strong><code>code</code>、<code>mode</code>、<code>name</code>、<code>status</code></div>
  2706. </div>
  2707. </div>
  2708. <div class="api-item" data-api="admin course set detail 赛道集合 variant 列表">
  2709. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/course-sets/{courseSetPublicID}</span></div>
  2710. <div class="api-desc">查看单个赛道集合详情和 variant 列表。</div>
  2711. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2712. </div>
  2713. <div class="api-item" data-api="admin course variant 创建 variant 赛道方案">
  2714. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/course-sets/{courseSetPublicID}/variants</span></div>
  2715. <div class="api-desc">为赛道集合创建具体可运行赛道方案。</div>
  2716. <div class="api-meta">
  2717. <div><strong>鉴权:</strong>Bearer token</div>
  2718. <div><strong>关键参数:</strong><code>sourceId</code>、<code>name</code>、<code>routeCode</code>、<code>mode</code>、<code>isDefault</code></div>
  2719. </div>
  2720. </div>
  2721. <div class="api-item" data-api="admin runtime bindings 列表 运行绑定">
  2722. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/runtime-bindings</span></div>
  2723. <div class="api-desc">查看活动运行绑定列表。</div>
  2724. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2725. </div>
  2726. <div class="api-item" data-api="admin runtime binding 创建 活动 运行绑定">
  2727. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/runtime-bindings</span></div>
  2728. <div class="api-desc">把活动和地点、地图资产、瓦片、赛道集合、variant 绑定起来。</div>
  2729. <div class="api-meta">
  2730. <div><strong>鉴权:</strong>Bearer token</div>
  2731. <div><strong>关键参数:</strong><code>eventId</code>、<code>placeId</code>、<code>mapAssetId</code>、<code>tileReleaseId</code>、<code>courseSetId</code>、<code>courseVariantId</code></div>
  2732. </div>
  2733. </div>
  2734. <div class="api-item" data-api="admin runtime binding detail 明细">
  2735. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/runtime-bindings/{runtimeBindingPublicID}</span></div>
  2736. <div class="api-desc">查看单个运行绑定详情。</div>
  2737. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2738. </div>
  2739. <div class="api-item" data-api="admin ops tile releases import 运维录入 瓦片版本 导入">
  2740. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/ops/tile-releases/import</span></div>
  2741. <div class="api-desc">运维入口第一期:按 placeCode / mapAssetCode / versionCode 导入或复用地点、地图资产和瓦片版本,并可直接设为当前版本。</div>
  2742. <div class="api-meta">
  2743. <div><strong>鉴权:</strong>Bearer token</div>
  2744. <div><strong>关键参数:</strong><code>placeCode</code>、<code>placeName</code>、<code>mapAssetCode</code>、<code>mapAssetName</code>、<code>versionCode</code>、<code>tileBaseUrl</code>、<code>metaUrl</code></div>
  2745. </div>
  2746. </div>
  2747. <div class="api-item" data-api="admin ops course sets import kml batch 运维录入 赛道 KML 批量导入">
  2748. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/ops/course-sets/import-kml-batch</span></div>
  2749. <div class="api-desc">运维入口第一期:批量登记多条 KML,自动补 course source / course set / variants,并支持指定默认赛道。</div>
  2750. <div class="api-meta">
  2751. <div><strong>鉴权:</strong>Bearer token</div>
  2752. <div><strong>关键参数:</strong><code>placeCode</code>、<code>mapAssetCode</code>、<code>courseSetCode</code>、<code>mode</code>、<code>defaultRouteCode</code>、<code>routes[]</code></div>
  2753. </div>
  2754. </div>
  2755. <div class="api-item" data-api="admin playfields 列表 赛场 kml 后台 资源">
  2756. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/playfields</span></div>
  2757. <div class="api-desc">后台赛场对象列表接口。</div>
  2758. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2759. </div>
  2760. <div class="api-item" data-api="admin playfields create 创建 赛场 对象 kml">
  2761. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/playfields</span></div>
  2762. <div class="api-desc">创建赛场对象,适合管理 KML / GeoJSON 这类可复用场地资源。</div>
  2763. <div class="api-meta">
  2764. <div><strong>鉴权:</strong>Bearer token</div>
  2765. <div><strong>关键参数:</strong><code>code</code>、<code>name</code>、<code>kind</code>、<code>status</code></div>
  2766. </div>
  2767. </div>
  2768. <div class="api-item" data-api="admin playfield detail 赛场 明细 版本 列表">
  2769. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/playfields/{playfieldPublicID}</span></div>
  2770. <div class="api-desc">查看单个赛场对象和它的版本列表。</div>
  2771. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2772. </div>
  2773. <div class="api-item" data-api="admin playfield version 创建 赛场 版本 kml sourceUrl">
  2774. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/playfields/{playfieldPublicID}/versions</span></div>
  2775. <div class="api-desc">为赛场对象创建一个版本,挂接 KML 等源文件地址和控制点摘要。</div>
  2776. <div class="api-meta">
  2777. <div><strong>鉴权:</strong>Bearer token</div>
  2778. <div><strong>关键参数:</strong><code>versionCode</code>、<code>sourceType</code>、<code>sourceUrl</code>、<code>controlCount</code>、<code>setAsCurrent</code></div>
  2779. </div>
  2780. </div>
  2781. <div class="api-item" data-api="admin resource packs 列表 资源包 后台">
  2782. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/resource-packs</span></div>
  2783. <div class="api-desc">后台资源包对象列表接口。</div>
  2784. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2785. </div>
  2786. <div class="api-item" data-api="admin resource packs create 创建 资源包">
  2787. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/resource-packs</span></div>
  2788. <div class="api-desc">创建资源包对象,用来管理内容页、音频和主题资源。</div>
  2789. <div class="api-meta">
  2790. <div><strong>鉴权:</strong>Bearer token</div>
  2791. <div><strong>关键参数:</strong><code>code</code>、<code>name</code>、<code>status</code></div>
  2792. </div>
  2793. </div>
  2794. <div class="api-item" data-api="admin resource pack detail 资源包 明细 版本">
  2795. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/resource-packs/{resourcePackPublicID}</span></div>
  2796. <div class="api-desc">查看单个资源包对象和它的版本列表。</div>
  2797. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2798. </div>
  2799. <div class="api-item" data-api="admin resource pack version 创建 资源包 版本 content audio theme">
  2800. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/resource-packs/{resourcePackPublicID}/versions</span></div>
  2801. <div class="api-desc">为资源包对象创建版本,配置内容入口、音频根路径和主题代码。</div>
  2802. <div class="api-meta">
  2803. <div><strong>鉴权:</strong>Bearer token</div>
  2804. <div><strong>关键参数:</strong><code>versionCode</code>、<code>contentEntryUrl</code>、<code>audioRootUrl</code>、<code>themeProfileCode</code>、<code>setAsCurrent</code></div>
  2805. </div>
  2806. </div>
  2807. <div class="api-item" data-api="admin events 列表 后台 event">
  2808. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/events</span></div>
  2809. <div class="api-desc">后台 event 列表接口。</div>
  2810. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2811. </div>
  2812. <div class="api-item" data-api="admin events create 创建 event">
  2813. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/events</span></div>
  2814. <div class="api-desc">创建 event 基础信息。</div>
  2815. <div class="api-meta">
  2816. <div><strong>鉴权:</strong>Bearer token</div>
  2817. <div><strong>关键参数:</strong><code>tenantCode</code>、<code>slug</code>、<code>displayName</code>、<code>status</code></div>
  2818. </div>
  2819. </div>
  2820. <div class="api-item" data-api="admin event detail 后台 event 明细 latest source">
  2821. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/events/{eventPublicID}</span></div>
  2822. <div class="api-desc">查看 event 明细、最新 source 和当前 source 摘要。</div>
  2823. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2824. </div>
  2825. <div class="api-item" data-api="admin event update 更新 event 基础信息">
  2826. <div class="api-head"><span class="api-method">PUT</span><span class="api-path">/admin/events/{eventPublicID}</span></div>
  2827. <div class="api-desc">更新 event 基础信息。</div>
  2828. <div class="api-meta">
  2829. <div><strong>鉴权:</strong>Bearer token</div>
  2830. <div><strong>关键参数:</strong><code>tenantCode</code>、<code>slug</code>、<code>displayName</code>、<code>status</code></div>
  2831. </div>
  2832. </div>
  2833. <div class="api-item" data-api="admin event source 组装 map playfield resource pack game mode">
  2834. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/events/{eventPublicID}/source</span></div>
  2835. <div class="api-desc">把 map/playfield/resource pack 版本和 gameModeCode 组装成 source config。</div>
  2836. <div class="api-meta">
  2837. <div><strong>鉴权:</strong>Bearer token</div>
  2838. <div><strong>关键参数:</strong><code>map.mapId</code>、<code>map.versionId</code>、<code>playfield.playfieldId</code>、<code>playfield.versionId</code>、<code>gameModeCode</code>、<code>overrides</code></div>
  2839. </div>
  2840. </div>
  2841. <div class="api-item" data-api="admin event presentations 列表 展示定义 presentation">
  2842. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/events/{eventPublicID}/presentations</span></div>
  2843. <div class="api-desc">查看某个 event 下的展示定义列表。</div>
  2844. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2845. </div>
  2846. <div class="api-item" data-api="admin event presentations create 创建 展示定义 presentation schema">
  2847. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/events/{eventPublicID}/presentations</span></div>
  2848. <div class="api-desc">为 event 创建一条最小 presentation 定义,供 release 绑定使用。</div>
  2849. <div class="api-meta">
  2850. <div><strong>鉴权:</strong>Bearer token</div>
  2851. <div><strong>关键参数:</strong><code>code</code>、<code>name</code>、<code>presentationType</code>、<code>schema</code></div>
  2852. </div>
  2853. </div>
  2854. <div class="api-item" data-api="admin event presentations import 导入 展示定义 templateKey schemaUrl version">
  2855. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/events/{eventPublicID}/presentations/import</span></div>
  2856. <div class="api-desc">通过统一导入入口为 event 创建展示定义,先记录 templateKey、sourceType、schemaUrl、version 和 title。</div>
  2857. <div class="api-meta">
  2858. <div><strong>鉴权:</strong>Bearer token</div>
  2859. <div><strong>关键参数:</strong><code>title</code>、<code>templateKey</code>、<code>sourceType</code>、<code>schemaUrl</code>、<code>version</code></div>
  2860. </div>
  2861. </div>
  2862. <div class="api-item" data-api="admin presentation detail 展示定义 明细">
  2863. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/presentations/{presentationPublicID}</span></div>
  2864. <div class="api-desc">查看单条 presentation 明细。</div>
  2865. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2866. </div>
  2867. <div class="api-item" data-api="admin event content bundles 列表 内容包">
  2868. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/events/{eventPublicID}/content-bundles</span></div>
  2869. <div class="api-desc">查看某个 event 下的内容包列表。</div>
  2870. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2871. </div>
  2872. <div class="api-item" data-api="admin event content bundles create 创建 内容包 entry asset metadata">
  2873. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/events/{eventPublicID}/content-bundles</span></div>
  2874. <div class="api-desc">为 event 创建一条最小 content bundle,供 release 绑定使用。</div>
  2875. <div class="api-meta">
  2876. <div><strong>鉴权:</strong>Bearer token</div>
  2877. <div><strong>关键参数:</strong><code>code</code>、<code>name</code>、<code>entryUrl</code>、<code>assetRootUrl</code>、<code>metadata</code></div>
  2878. </div>
  2879. </div>
  2880. <div class="api-item" data-api="admin event content bundles import 导入 内容包 manifest bundleType version">
  2881. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/events/{eventPublicID}/content-bundles/import</span></div>
  2882. <div class="api-desc">通过统一导入入口为 event 创建内容包,先记录 bundleType、sourceType、manifestUrl、version 和 assetManifest。</div>
  2883. <div class="api-meta">
  2884. <div><strong>鉴权:</strong>Bearer token</div>
  2885. <div><strong>关键参数:</strong><code>title</code>、<code>bundleType</code>、<code>sourceType</code>、<code>manifestUrl</code>、<code>version</code></div>
  2886. </div>
  2887. </div>
  2888. <div class="api-item" data-api="admin content bundle detail 内容包 明细">
  2889. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/content-bundles/{contentBundlePublicID}</span></div>
  2890. <div class="api-desc">查看单条内容包明细。</div>
  2891. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2892. </div>
  2893. <div class="api-item" data-api="admin event defaults 默认绑定 presentation content bundle runtime">
  2894. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/events/{eventPublicID}/defaults</span></div>
  2895. <div class="api-desc">固化 event 当前默认 active 绑定,供后续 publish 在未显式传参时继承。</div>
  2896. <div class="api-meta">
  2897. <div><strong>鉴权:</strong>Bearer token</div>
  2898. <div><strong>关键参数:</strong><code>presentationId</code>、<code>contentBundleId</code>、<code>runtimeBindingId</code></div>
  2899. </div>
  2900. </div>
  2901. <div class="api-item" data-api="admin event pipeline 流水线 source build release">
  2902. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/events/{eventPublicID}/pipeline</span></div>
  2903. <div class="api-desc">查看 event 下的 source、build、release 流水线概览。</div>
  2904. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2905. </div>
  2906. <div class="api-item" data-api="admin source build 后台 build source">
  2907. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/sources/{sourceID}/build</span></div>
  2908. <div class="api-desc">基于 source 生成一条 build 记录和 preview manifest。</div>
  2909. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2910. </div>
  2911. <div class="api-item" data-api="admin build detail 后台 build 明细">
  2912. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/builds/{buildID}</span></div>
  2913. <div class="api-desc">查看后台 build 明细。</div>
  2914. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2915. </div>
  2916. <div class="api-item" data-api="admin build publish 后台 发布 release runtime binding">
  2917. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/builds/{buildID}/publish</span></div>
  2918. <div class="api-desc">把后台 build 发布为正式 release,可选直接挂接 runtime binding、presentation 和内容包,并切换为 event 当前发布版本。</div>
  2919. <div class="api-meta">
  2920. <div><strong>鉴权:</strong>Bearer token</div>
  2921. <div><strong>关键参数:</strong><code>runtimeBindingId</code>、<code>presentationId</code>、<code>contentBundleId</code></div>
  2922. </div>
  2923. </div>
  2924. <div class="api-item" data-api="admin release detail runtime binding 运行 摘要">
  2925. <div class="api-head"><span class="api-method">GET</span><span class="api-path">/admin/releases/{releasePublicID}</span></div>
  2926. <div class="api-desc">查看单个 release 明细,并带出当前已挂接的 runtime 摘要。</div>
  2927. <div class="api-meta"><div><strong>鉴权:</strong>Bearer token</div></div>
  2928. </div>
  2929. <div class="api-item" data-api="admin release runtime binding 挂接 运行对象">
  2930. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/releases/{releasePublicID}/runtime-binding</span></div>
  2931. <div class="api-desc">把某个 runtime binding 挂接到指定 release,上游 launch 会透出新的 runtime 摘要。</div>
  2932. <div class="api-meta">
  2933. <div><strong>鉴权:</strong>Bearer token</div>
  2934. <div><strong>关键参数:</strong><code>runtimeBindingId</code></div>
  2935. </div>
  2936. </div>
  2937. <div class="api-item" data-api="admin event rollback 回滚 发布版本">
  2938. <div class="api-head"><span class="api-method">POST</span><span class="api-path">/admin/events/{eventPublicID}/rollback</span></div>
  2939. <div class="api-desc">将 event 当前发布版本回滚到指定 releaseId。</div>
  2940. <div class="api-meta">
  2941. <div><strong>鉴权:</strong>Bearer token</div>
  2942. <div><strong>关键参数:</strong><code>releaseId</code></div>
  2943. </div>
  2944. </div>
  2945. </div>
  2946. </section>
  2947. </div>
  2948. </main>
  2949. </div>
  2950. </div>
  2951. <script>
  2952. const STORAGE_KEY = 'cmr-backend-workbench-state-v1';
  2953. const HISTORY_KEY = 'cmr-backend-workbench-history-v1';
  2954. const SCENARIO_KEY = 'cmr-backend-workbench-scenarios-v1';
  2955. const MODE_KEY = 'cmr-backend-workbench-mode-v1';
  2956. const FLOW_STEP_PLANS = {
  2957. 'flow-admin-default-publish': ['get-event', 'import-presentation', 'import-bundle', 'save-defaults', 'build-source', 'publish-build', 'get-release'],
  2958. 'flow-admin-runtime-publish': ['bootstrap-demo', 'get-event', 'import-presentation', 'import-bundle', 'create-runtime-binding', 'save-defaults', 'build-source', 'publish-build', 'get-release'],
  2959. 'flow-standard-regression': ['prepare-release', 'login-wechat', 'event-play', 'event-launch', 'session-start', 'session-finish', 'session-result', 'history-check']
  2960. };
  2961. const state = {
  2962. accessToken: '',
  2963. refreshToken: '',
  2964. sourceId: '',
  2965. buildId: '',
  2966. releaseId: '',
  2967. sessionId: '',
  2968. sessionToken: '',
  2969. lastCurl: ''
  2970. };
  2971. const currentFlowStatus = {
  2972. eventId: '-',
  2973. releaseId: '-',
  2974. canLaunch: '-',
  2975. assignmentMode: '-',
  2976. variantCount: '-',
  2977. gameMode: '-',
  2978. playfieldKind: '-'
  2979. };
  2980. const currentPreviewStatus = {
  2981. mode: '-',
  2982. tileBaseUrl: '-',
  2983. zoom: '-',
  2984. viewport: '-',
  2985. selectedVariantId: '-',
  2986. variantCount: '-',
  2987. controlCount: '-',
  2988. legCount: '-'
  2989. };
  2990. const $ = (id) => document.getElementById(id);
  2991. const logEl = $('log');
  2992. const curlEl = $('curl');
  2993. const historyEl = $('history');
  2994. const statusEl = $('status');
  2995. const progressLabelEl = $('progress-label');
  2996. const progressStepEl = $('progress-step');
  2997. const progressFillEl = $('progress-fill');
  2998. const progressNoteEl = $('progress-note');
  2999. const modeNodes = Array.from(document.querySelectorAll('[data-modes]'));
  3000. const modeButtons = Array.from(document.querySelectorAll('[data-mode-btn]'));
  3001. const navLinks = Array.from(document.querySelectorAll('[data-nav-target]'));
  3002. const builtInScenarios = [
  3003. {
  3004. id: 'preset-demo-wechat',
  3005. builtin: true,
  3006. name: 'Preset: Demo WeChat Flow',
  3007. fields: {
  3008. smsClientType: 'wechat',
  3009. smsScene: 'login',
  3010. smsMobile: '13800138000',
  3011. smsDevice: 'workbench-device-001',
  3012. smsCountry: '86',
  3013. smsCode: '',
  3014. wechatCode: 'dev-workbench-user',
  3015. wechatDevice: 'wechat-device-001',
  3016. entryChannelCode: 'mini-demo',
  3017. entryChannelType: 'wechat_mini',
  3018. eventId: 'evt_demo_001',
  3019. eventReleaseId: 'rel_demo_001',
  3020. eventVariantId: '',
  3021. eventDevice: 'wechat-device-001',
  3022. finishStatus: 'finished',
  3023. finishDuration: '960',
  3024. finishScore: '88',
  3025. finishControlsDone: '7',
  3026. finishControlsTotal: '8',
  3027. finishDistance: '5230',
  3028. finishSpeed: '6.45',
  3029. finishHeartRate: '168',
  3030. adminMapCode: 'map-demo-001',
  3031. adminMapName: 'Demo Park Map',
  3032. adminMapVersionCode: 'v2026-04-02',
  3033. adminMapStatus: 'active',
  3034. adminMapmetaUrl: 'https://example.com/maps/demo/mapmeta.json',
  3035. adminTilesRootUrl: 'https://example.com/maps/demo/tiles/',
  3036. adminPlayfieldCode: 'pf-demo-001',
  3037. adminPlayfieldName: 'Demo Course',
  3038. adminPlayfieldKind: 'course',
  3039. adminPlayfieldStatus: 'active',
  3040. adminPlayfieldVersionCode: 'v2026-04-02',
  3041. adminPlayfieldSourceType: 'kml',
  3042. adminPlayfieldSourceUrl: 'https://example.com/playfields/demo/course.kml',
  3043. adminPlayfieldControlCount: '8',
  3044. adminPackCode: 'pack-demo-001',
  3045. adminPackName: 'Demo Resource Pack',
  3046. adminPackVersionCode: 'v2026-04-02',
  3047. adminPackStatus: 'active',
  3048. adminPackContentUrl: 'https://example.com/packs/demo/content.html',
  3049. adminPackAudioUrl: 'https://example.com/packs/demo/audio/',
  3050. adminPackThemeCode: 'theme-demo',
  3051. adminPublishedAssetRoot: '',
  3052. adminTenantCode: 'tenant_demo',
  3053. adminEventStatus: 'active',
  3054. adminEventRefId: 'evt_demo_001',
  3055. adminEventSlug: 'demo-city-run',
  3056. adminEventName: 'Demo City Run',
  3057. adminEventSummary: '后台第一版 Event,用于资源对象组装和发布链路验证。',
  3058. adminGameModeCode: 'classic-sequential',
  3059. adminRouteCode: 'route-demo-001',
  3060. adminSourceNotes: 'workbench assembled source',
  3061. adminOverridesJSON: '{}',
  3062. adminPipelineSourceId: '',
  3063. adminPipelineBuildId: '',
  3064. adminPipelineReleaseId: '',
  3065. adminRollbackReleaseId: ''
  3066. }
  3067. },
  3068. {
  3069. id: 'preset-demo-app-launch',
  3070. builtin: true,
  3071. name: 'Preset: Demo App Launch Flow',
  3072. fields: {
  3073. smsClientType: 'app',
  3074. smsScene: 'login',
  3075. smsMobile: '13800138000',
  3076. smsDevice: 'workbench-device-001',
  3077. smsCountry: '86',
  3078. smsCode: '',
  3079. wechatCode: 'dev-workbench-user',
  3080. wechatDevice: 'wechat-device-001',
  3081. entryChannelCode: 'mini-demo',
  3082. entryChannelType: 'wechat_mini',
  3083. eventId: 'evt_demo_001',
  3084. eventReleaseId: 'rel_demo_001',
  3085. eventVariantId: '',
  3086. eventDevice: 'workbench-device-001',
  3087. finishStatus: 'finished',
  3088. finishDuration: '960',
  3089. finishScore: '88',
  3090. finishControlsDone: '7',
  3091. finishControlsTotal: '8',
  3092. finishDistance: '5230',
  3093. finishSpeed: '6.45',
  3094. finishHeartRate: '168',
  3095. adminMapCode: 'map-demo-001',
  3096. adminMapName: 'Demo Park Map',
  3097. adminMapVersionCode: 'v2026-04-02',
  3098. adminMapStatus: 'active',
  3099. adminMapmetaUrl: 'https://example.com/maps/demo/mapmeta.json',
  3100. adminTilesRootUrl: 'https://example.com/maps/demo/tiles/',
  3101. adminPlayfieldCode: 'pf-demo-001',
  3102. adminPlayfieldName: 'Demo Course',
  3103. adminPlayfieldKind: 'course',
  3104. adminPlayfieldStatus: 'active',
  3105. adminPlayfieldVersionCode: 'v2026-04-02',
  3106. adminPlayfieldSourceType: 'kml',
  3107. adminPlayfieldSourceUrl: 'https://example.com/playfields/demo/course.kml',
  3108. adminPlayfieldControlCount: '8',
  3109. adminPackCode: 'pack-demo-001',
  3110. adminPackName: 'Demo Resource Pack',
  3111. adminPackVersionCode: 'v2026-04-02',
  3112. adminPackStatus: 'active',
  3113. adminPackContentUrl: 'https://example.com/packs/demo/content.html',
  3114. adminPackAudioUrl: 'https://example.com/packs/demo/audio/',
  3115. adminPackThemeCode: 'theme-demo',
  3116. adminPublishedAssetRoot: '',
  3117. adminTenantCode: 'tenant_demo',
  3118. adminEventStatus: 'active',
  3119. adminEventRefId: 'evt_demo_001',
  3120. adminEventSlug: 'demo-city-run',
  3121. adminEventName: 'Demo City Run',
  3122. adminEventSummary: '后台第一版 Event,用于资源对象组装和发布链路验证。',
  3123. adminGameModeCode: 'classic-sequential',
  3124. adminRouteCode: 'route-demo-001',
  3125. adminSourceNotes: 'workbench assembled source',
  3126. adminOverridesJSON: '{}',
  3127. adminPipelineSourceId: '',
  3128. adminPipelineBuildId: '',
  3129. adminPipelineReleaseId: '',
  3130. adminRollbackReleaseId: ''
  3131. }
  3132. }
  3133. ];
  3134. function syncState() {
  3135. $('state-access').textContent = state.accessToken || '-';
  3136. $('state-refresh').textContent = state.refreshToken || '-';
  3137. $('state-source').textContent = state.sourceId || '-';
  3138. $('state-build').textContent = state.buildId || '-';
  3139. $('state-release').textContent = state.releaseId || '-';
  3140. $('state-session').textContent = state.sessionId || '-';
  3141. $('state-session-token').textContent = state.sessionToken || '-';
  3142. $('config-source-id').value = state.sourceId || '';
  3143. $('config-build-id').value = state.buildId || '';
  3144. $('admin-pipeline-source-id').value = state.sourceId || '';
  3145. $('admin-pipeline-build-id').value = state.buildId || '';
  3146. $('event-release-id').value = state.releaseId || $('event-release-id').value;
  3147. $('admin-pipeline-release-id').value = state.releaseId || $('admin-pipeline-release-id').value;
  3148. $('session-id').value = state.sessionId || '';
  3149. $('session-token').value = state.sessionToken || '';
  3150. curlEl.textContent = state.lastCurl || '-';
  3151. syncCurrentFlowStatusFromForms();
  3152. persistState();
  3153. }
  3154. function renderCurrentFlowStatus() {
  3155. $('current-flow-event-id').textContent = currentFlowStatus.eventId || '-';
  3156. $('current-flow-release-id').textContent = currentFlowStatus.releaseId || '-';
  3157. $('current-flow-can-launch').textContent = currentFlowStatus.canLaunch || '-';
  3158. $('current-flow-assignment-mode').textContent = currentFlowStatus.assignmentMode || '-';
  3159. $('current-flow-variant-count').textContent = currentFlowStatus.variantCount || '-';
  3160. $('current-flow-game-mode').textContent = currentFlowStatus.gameMode || '-';
  3161. $('current-flow-playfield-kind').textContent = currentFlowStatus.playfieldKind || '-';
  3162. }
  3163. function renderCurrentPreviewStatus() {
  3164. $('current-preview-mode').textContent = currentPreviewStatus.mode || '-';
  3165. $('current-preview-tile-url').textContent = currentPreviewStatus.tileBaseUrl || '-';
  3166. $('current-preview-zoom').textContent = currentPreviewStatus.zoom || '-';
  3167. $('current-preview-viewport').textContent = currentPreviewStatus.viewport || '-';
  3168. $('current-preview-selected-variant').textContent = currentPreviewStatus.selectedVariantId || '-';
  3169. $('current-preview-variant-count').textContent = currentPreviewStatus.variantCount || '-';
  3170. $('current-preview-control-count').textContent = currentPreviewStatus.controlCount || '-';
  3171. $('current-preview-leg-count').textContent = currentPreviewStatus.legCount || '-';
  3172. }
  3173. function syncCurrentFlowStatusFromForms() {
  3174. currentFlowStatus.eventId = trimmedOrUndefined($('event-id').value) || '-';
  3175. currentFlowStatus.releaseId = trimmedOrUndefined($('event-release-id').value) || state.releaseId || '-';
  3176. renderCurrentFlowStatus();
  3177. }
  3178. function resetCurrentFlowStatus() {
  3179. currentFlowStatus.eventId = trimmedOrUndefined($('event-id').value) || '-';
  3180. currentFlowStatus.releaseId = trimmedOrUndefined($('event-release-id').value) || state.releaseId || '-';
  3181. currentFlowStatus.canLaunch = '-';
  3182. currentFlowStatus.assignmentMode = '-';
  3183. currentFlowStatus.variantCount = '-';
  3184. currentFlowStatus.gameMode = '-';
  3185. currentFlowStatus.playfieldKind = '-';
  3186. renderCurrentFlowStatus();
  3187. resetCurrentPreviewStatus();
  3188. }
  3189. function resetCurrentPreviewStatus() {
  3190. currentPreviewStatus.mode = '-';
  3191. currentPreviewStatus.tileBaseUrl = '-';
  3192. currentPreviewStatus.zoom = '-';
  3193. currentPreviewStatus.viewport = '-';
  3194. currentPreviewStatus.selectedVariantId = '-';
  3195. currentPreviewStatus.variantCount = '-';
  3196. currentPreviewStatus.controlCount = '-';
  3197. currentPreviewStatus.legCount = '-';
  3198. renderCurrentPreviewStatus();
  3199. }
  3200. function setCurrentFlowStatusFromPlayResponse(result) {
  3201. const payload = result && result.data ? result.data : (result || {});
  3202. const play = payload.play || {};
  3203. const resolvedRelease = payload.resolvedRelease || {};
  3204. const variants = Array.isArray(play.courseVariants) ? play.courseVariants : [];
  3205. currentFlowStatus.eventId = trimmedOrUndefined($('event-id').value) || currentFlowStatus.eventId || '-';
  3206. currentFlowStatus.releaseId = resolvedRelease.releaseId || trimmedOrUndefined($('event-release-id').value) || state.releaseId || '-';
  3207. currentFlowStatus.canLaunch = typeof play.canLaunch === 'boolean' ? String(play.canLaunch) : '-';
  3208. currentFlowStatus.assignmentMode = play.assignmentMode || '-';
  3209. currentFlowStatus.variantCount = variants.length ? String(variants.length) : '0';
  3210. renderCurrentFlowStatus();
  3211. setCurrentPreviewStatusFromPayload(payload);
  3212. }
  3213. function setCurrentFlowStatusFromLaunch(summary, launchData) {
  3214. const payload = launchData && launchData.launch ? launchData.launch : {};
  3215. const resolvedRelease = payload.resolvedRelease || {};
  3216. const variant = payload.variant || {};
  3217. currentFlowStatus.eventId = trimmedOrUndefined($('event-id').value) || currentFlowStatus.eventId || '-';
  3218. currentFlowStatus.releaseId = summary && summary.releaseId ? summary.releaseId : (resolvedRelease.releaseId || currentFlowStatus.releaseId || '-');
  3219. if (summary && summary.gameMode) {
  3220. currentFlowStatus.gameMode = summary.gameMode;
  3221. }
  3222. if (summary && summary.playfieldKind) {
  3223. currentFlowStatus.playfieldKind = summary.playfieldKind;
  3224. }
  3225. if (variant && variant.assignmentMode) {
  3226. currentFlowStatus.assignmentMode = variant.assignmentMode;
  3227. }
  3228. renderCurrentFlowStatus();
  3229. }
  3230. function setCurrentPreviewStatusFromPayload(payload) {
  3231. const preview = payload && payload.preview ? payload.preview : {};
  3232. const baseTiles = preview.baseTiles || {};
  3233. const viewport = preview.viewport || {};
  3234. const variants = Array.isArray(preview.variants) ? preview.variants : [];
  3235. const firstVariant = variants[0] || {};
  3236. const controls = Array.isArray(firstVariant.controls) ? firstVariant.controls : [];
  3237. const legs = Array.isArray(firstVariant.legs) ? firstVariant.legs : [];
  3238. currentPreviewStatus.mode = preview.mode || '-';
  3239. currentPreviewStatus.tileBaseUrl = baseTiles.tileBaseUrl || '-';
  3240. currentPreviewStatus.zoom = typeof baseTiles.zoom === 'number' ? String(baseTiles.zoom) : '-';
  3241. currentPreviewStatus.viewport = formatPreviewViewport(viewport);
  3242. currentPreviewStatus.selectedVariantId = preview.selectedVariantId || '-';
  3243. currentPreviewStatus.variantCount = variants.length ? String(variants.length) : '0';
  3244. currentPreviewStatus.controlCount = controls.length ? String(controls.length) : '0';
  3245. currentPreviewStatus.legCount = legs.length ? String(legs.length) : '0';
  3246. renderCurrentPreviewStatus();
  3247. }
  3248. function formatPreviewViewport(viewport) {
  3249. if (!viewport || Object.keys(viewport).length === 0) {
  3250. return '-';
  3251. }
  3252. const size = (typeof viewport.width === 'number' && typeof viewport.height === 'number')
  3253. ? (String(viewport.width) + 'x' + String(viewport.height))
  3254. : null;
  3255. const bboxReady = ['minLon', 'minLat', 'maxLon', 'maxLat'].every((key) => typeof viewport[key] === 'number');
  3256. const bbox = bboxReady
  3257. ? [viewport.minLon, viewport.minLat, viewport.maxLon, viewport.maxLat].map((value) => Number(value).toFixed(4)).join(', ')
  3258. : null;
  3259. if (size && bbox) {
  3260. return size + ' | ' + bbox;
  3261. }
  3262. return size || bbox || '-';
  3263. }
  3264. function setDefaultPublishExpectation(result) {
  3265. const release = result || {};
  3266. const releaseId = release.id || '-';
  3267. const presentationId = release.presentation && release.presentation.presentationId ? release.presentation.presentationId : '-';
  3268. const contentBundleId = release.contentBundle && release.contentBundle.contentBundleId ? release.contentBundle.contentBundleId : '-';
  3269. const runtimeBindingId = release.runtime && release.runtime.runtimeBindingId ? release.runtime.runtimeBindingId : '-';
  3270. const expectedRuntime = trimmedOrUndefined($('admin-release-runtime-binding-id').value || $('prod-runtime-binding-id').value);
  3271. const hasPresentation = presentationId !== '-';
  3272. const hasContentBundle = contentBundleId !== '-';
  3273. const runtimeSatisfied = expectedRuntime ? runtimeBindingId !== '-' : true;
  3274. let verdict = '未通过';
  3275. if (hasPresentation && hasContentBundle && runtimeSatisfied) {
  3276. verdict = expectedRuntime ? '通过:presentation / content bundle / runtime 已继承' : '通过:presentation / content bundle 已继承,runtime 未配置';
  3277. }
  3278. $('flow-admin-release-result').textContent = releaseId;
  3279. $('flow-admin-presentation-result').textContent = presentationId;
  3280. $('flow-admin-content-bundle-result').textContent = contentBundleId;
  3281. $('flow-admin-runtime-result').textContent = runtimeBindingId;
  3282. $('flow-admin-verdict').textContent = verdict;
  3283. }
  3284. function resetStandardRegressionExpectation() {
  3285. $('flow-regression-publish-result').textContent = '待执行';
  3286. $('flow-regression-play-result').textContent = '待执行';
  3287. $('flow-regression-launch-result').textContent = '待执行';
  3288. $('flow-regression-result-result').textContent = '待执行';
  3289. $('flow-regression-history-result').textContent = '待执行';
  3290. $('flow-regression-session-id').textContent = '-';
  3291. $('flow-regression-overall').textContent = '待执行';
  3292. }
  3293. function setStandardRegressionExpectation(summary) {
  3294. const publishText = summary && summary.publish ? summary.publish : '待执行';
  3295. const playText = summary && summary.play ? summary.play : '待执行';
  3296. const launchText = summary && summary.launch ? summary.launch : '待执行';
  3297. const resultText = summary && summary.result ? summary.result : '待执行';
  3298. const historyText = summary && summary.history ? summary.history : '待执行';
  3299. const sessionId = summary && summary.sessionId ? summary.sessionId : '-';
  3300. const overall = summary && summary.overall ? summary.overall : '待执行';
  3301. $('flow-regression-publish-result').textContent = publishText;
  3302. $('flow-regression-play-result').textContent = playText;
  3303. $('flow-regression-launch-result').textContent = launchText;
  3304. $('flow-regression-result-result').textContent = resultText;
  3305. $('flow-regression-history-result').textContent = historyText;
  3306. $('flow-regression-session-id').textContent = sessionId;
  3307. $('flow-regression-overall').textContent = overall;
  3308. }
  3309. function resetLaunchConfigSummary() {
  3310. $('launch-config-url').textContent = '-';
  3311. $('launch-config-release-id').textContent = '-';
  3312. $('launch-config-manifest-url').textContent = '-';
  3313. $('launch-config-schema-version').textContent = '-';
  3314. $('launch-config-playfield-kind').textContent = '-';
  3315. $('launch-config-game-mode').textContent = '-';
  3316. $('launch-config-verdict').textContent = '待执行';
  3317. resetCurrentFlowStatus();
  3318. }
  3319. function buildDemoPresentationSchemaURL(demoKind) {
  3320. return window.location.origin + '/dev/demo-assets/presentations/' + encodeURIComponent(demoKind);
  3321. }
  3322. function buildDemoContentManifestURL(demoKind) {
  3323. return window.location.origin + '/dev/demo-assets/content-manifests/' + encodeURIComponent(demoKind);
  3324. }
  3325. function buildDemoContentAssetManifest(demoKind) {
  3326. return JSON.stringify({
  3327. manifestUrl: buildDemoContentManifestURL(demoKind),
  3328. coverUrl: 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
  3329. entryHtml: 'https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html'
  3330. }, null, 2);
  3331. }
  3332. function applyDemoImportInputs(demoKind) {
  3333. const configs = {
  3334. classic: {
  3335. presentationTitle: '领秀城公园顺序赛展示定义',
  3336. templateKey: 'event.detail.standard',
  3337. presentationVersion: 'v2026-04-03-classic',
  3338. contentTitle: '领秀城公园顺序赛内容包',
  3339. contentVersion: 'v2026-04-03-classic',
  3340. bundleType: 'result_media'
  3341. },
  3342. 'score-o': {
  3343. presentationTitle: '领秀城公园积分赛展示定义',
  3344. templateKey: 'event.detail.score-o',
  3345. presentationVersion: 'v2026-04-03-score-o',
  3346. contentTitle: '领秀城公园积分赛内容包',
  3347. contentVersion: 'v2026-04-03-score-o',
  3348. bundleType: 'result_media'
  3349. },
  3350. 'manual-variant': {
  3351. presentationTitle: '领秀城公园多赛道挑战展示定义',
  3352. templateKey: 'event.detail.multi-variant',
  3353. presentationVersion: 'v2026-04-03-manual',
  3354. contentTitle: '领秀城公园多赛道挑战内容包',
  3355. contentVersion: 'v2026-04-03-manual',
  3356. bundleType: 'result_media'
  3357. }
  3358. };
  3359. const config = configs[demoKind] || configs.classic;
  3360. $('admin-presentation-import-title').value = config.presentationTitle;
  3361. $('admin-presentation-import-template-key').value = config.templateKey;
  3362. $('admin-presentation-import-source-type').value = 'schema';
  3363. $('admin-presentation-import-version').value = config.presentationVersion;
  3364. $('admin-presentation-import-schema-url').value = buildDemoPresentationSchemaURL(demoKind);
  3365. $('admin-content-import-title').value = config.contentTitle;
  3366. $('admin-content-import-bundle-type').value = config.bundleType;
  3367. $('admin-content-import-source-type').value = 'manifest';
  3368. $('admin-content-import-version').value = config.contentVersion;
  3369. $('admin-content-import-manifest-url').value = buildDemoContentManifestURL(demoKind);
  3370. $('admin-content-import-asset-manifest-json').value = buildDemoContentAssetManifest(demoKind);
  3371. }
  3372. async function resolveLaunchConfigSummary(launchPayload) {
  3373. const launchData = launchPayload && launchPayload.launch ? launchPayload.launch : {};
  3374. const config = launchData.config || {};
  3375. const resolvedRelease = launchData.resolvedRelease || {};
  3376. const configUrl = config.configUrl || '-';
  3377. const releaseId = config.releaseId || resolvedRelease.releaseId || '-';
  3378. const manifestUrl = resolvedRelease.manifestUrl || config.configUrl || '-';
  3379. const summary = {
  3380. configUrl: configUrl,
  3381. releaseId: releaseId,
  3382. manifestUrl: manifestUrl,
  3383. schemaVersion: '-',
  3384. playfieldKind: '-',
  3385. gameMode: '-',
  3386. verdict: '未读取 manifest'
  3387. };
  3388. const targetUrl = config.configUrl || resolvedRelease.manifestUrl;
  3389. if (!targetUrl) {
  3390. summary.verdict = '未通过:launch 未返回 configUrl / manifestUrl';
  3391. return summary;
  3392. }
  3393. try {
  3394. const proxy = await request('GET', '/dev/manifest-summary?url=' + encodeURIComponent(targetUrl));
  3395. const data = proxy && proxy.data ? proxy.data : {};
  3396. summary.schemaVersion = data.schemaVersion || '-';
  3397. summary.playfieldKind = data.playfieldKind || '-';
  3398. summary.gameMode = data.gameMode || '-';
  3399. if (summary.schemaVersion !== '-' && summary.playfieldKind !== '-' && summary.gameMode !== '-') {
  3400. summary.verdict = '通过:已读取 launch 实际 manifest 摘要';
  3401. } else {
  3402. summary.verdict = '未通过:manifest 已读取,但关键信息不完整';
  3403. }
  3404. return summary;
  3405. } catch (error) {
  3406. summary.verdict = '未通过:manifest 读取异常';
  3407. summary.error = error && error.message ? error.message : String(error);
  3408. return summary;
  3409. }
  3410. }
  3411. async function resolveEventManifestSummary(payload) {
  3412. const data = payload || {};
  3413. const release = data.release || {};
  3414. const resolvedRelease = data.resolvedRelease || {};
  3415. const manifestUrl = release.manifestUrl || resolvedRelease.manifestUrl || '-';
  3416. const releaseId = release.id || resolvedRelease.releaseId || '-';
  3417. const summary = {
  3418. configUrl: manifestUrl,
  3419. releaseId: releaseId,
  3420. manifestUrl: manifestUrl,
  3421. schemaVersion: '-',
  3422. playfieldKind: '-',
  3423. gameMode: '-',
  3424. verdict: '未读取 manifest'
  3425. };
  3426. const targetUrl = release.manifestUrl || resolvedRelease.manifestUrl;
  3427. if (!targetUrl) {
  3428. summary.verdict = '未通过:event 未返回 manifestUrl';
  3429. return summary;
  3430. }
  3431. try {
  3432. const proxy = await request('GET', '/dev/manifest-summary?url=' + encodeURIComponent(targetUrl));
  3433. const result = proxy && proxy.data ? proxy.data : {};
  3434. summary.schemaVersion = result.schemaVersion || '-';
  3435. summary.playfieldKind = result.playfieldKind || '-';
  3436. summary.gameMode = result.gameMode || '-';
  3437. if (summary.schemaVersion !== '-' && summary.playfieldKind !== '-' && summary.gameMode !== '-') {
  3438. summary.verdict = '通过:已读取 event 当前 manifest 摘要';
  3439. } else {
  3440. summary.verdict = '未通过:manifest 已读取,但关键信息不完整';
  3441. }
  3442. return summary;
  3443. } catch (error) {
  3444. summary.verdict = '未通过:manifest 读取异常';
  3445. summary.error = error && error.message ? error.message : String(error);
  3446. return summary;
  3447. }
  3448. }
  3449. function setLaunchConfigSummary(summary) {
  3450. const data = summary || {};
  3451. $('launch-config-url').textContent = data.configUrl || '-';
  3452. $('launch-config-release-id').textContent = data.releaseId || '-';
  3453. $('launch-config-manifest-url').textContent = data.manifestUrl || '-';
  3454. $('launch-config-schema-version').textContent = data.schemaVersion || '-';
  3455. $('launch-config-playfield-kind').textContent = data.playfieldKind || '-';
  3456. $('launch-config-game-mode').textContent = data.gameMode || '-';
  3457. $('launch-config-verdict').textContent = data.verdict || '待执行';
  3458. }
  3459. function scheduleMasonryLayout() {
  3460. if (window.__cmrMasonryFrame) {
  3461. cancelAnimationFrame(window.__cmrMasonryFrame);
  3462. }
  3463. window.__cmrMasonryFrame = requestAnimationFrame(function() {
  3464. document.querySelectorAll('.masonry').forEach(function(container) {
  3465. const panels = Array.from(container.querySelectorAll(':scope > .panel')).filter(function(panel) {
  3466. return !panel.classList.contains('mode-hidden');
  3467. });
  3468. container.style.height = '';
  3469. panels.forEach(function(panel) {
  3470. panel.style.position = '';
  3471. panel.style.width = '';
  3472. panel.style.left = '';
  3473. panel.style.top = '';
  3474. });
  3475. if (window.innerWidth <= 900 || panels.length <= 1) {
  3476. return;
  3477. }
  3478. const gap = 16;
  3479. const minWidth = 320;
  3480. const width = container.clientWidth;
  3481. if (!width) {
  3482. return;
  3483. }
  3484. const columnCount = Math.max(1, Math.floor((width + gap) / (minWidth + gap)));
  3485. if (columnCount <= 1) {
  3486. return;
  3487. }
  3488. const columnWidth = Math.floor((width - gap * (columnCount - 1)) / columnCount);
  3489. const heights = Array(columnCount).fill(0);
  3490. panels.forEach(function(panel) {
  3491. let targetColumn = 0;
  3492. for (let i = 1; i < heights.length; i += 1) {
  3493. if (heights[i] < heights[targetColumn]) {
  3494. targetColumn = i;
  3495. }
  3496. }
  3497. panel.style.position = 'absolute';
  3498. panel.style.width = columnWidth + 'px';
  3499. panel.style.left = ((columnWidth + gap) * targetColumn) + 'px';
  3500. panel.style.top = heights[targetColumn] + 'px';
  3501. heights[targetColumn] += panel.offsetHeight + gap;
  3502. });
  3503. container.style.height = Math.max.apply(null, heights.map(function(height) {
  3504. return Math.max(0, height - gap);
  3505. })) + 'px';
  3506. });
  3507. });
  3508. }
  3509. function renderClientLogs(items) {
  3510. const logs = Array.isArray(items) ? items : [];
  3511. if (!logs.length) {
  3512. $('client-logs').textContent = '暂无前端调试日志';
  3513. scheduleMasonryLayout();
  3514. return;
  3515. }
  3516. $('client-logs').textContent = logs.map(function(item) {
  3517. const lines = [];
  3518. lines.push('[' + (item.receivedAt || '-') + '] #' + (item.id || '-') + ' ' + (item.level || 'info').toUpperCase() + ' ' + (item.source || 'unknown'));
  3519. if (item.category) {
  3520. lines.push('category: ' + item.category);
  3521. }
  3522. lines.push('message: ' + (item.message || '-'));
  3523. if (item.eventId || item.releaseId || item.sessionId) {
  3524. lines.push('event/release/session: ' + (item.eventId || '-') + ' / ' + (item.releaseId || '-') + ' / ' + (item.sessionId || '-'));
  3525. }
  3526. if (item.manifestUrl) {
  3527. lines.push('manifest: ' + item.manifestUrl);
  3528. }
  3529. if (item.route) {
  3530. lines.push('route: ' + item.route);
  3531. }
  3532. if (item.details && Object.keys(item.details).length) {
  3533. lines.push('details: ' + JSON.stringify(item.details, null, 2));
  3534. }
  3535. return lines.join('\n');
  3536. }).join('\n\n---\n\n');
  3537. scheduleMasonryLayout();
  3538. }
  3539. async function refreshClientLogs() {
  3540. const result = await request('GET', '/dev/client-logs?limit=50');
  3541. renderClientLogs(result.data);
  3542. return result;
  3543. }
  3544. function selectBootstrapContextForEvent(bootstrap, eventId) {
  3545. const data = bootstrap || {};
  3546. if (eventId && data.scoreOEventId && eventId === data.scoreOEventId) {
  3547. return {
  3548. eventId: data.scoreOEventId,
  3549. releaseId: data.scoreOReleaseId || '',
  3550. sourceId: data.scoreOSourceId || '',
  3551. buildId: data.scoreOBuildId || '',
  3552. courseSetId: data.scoreOCourseSetId || '',
  3553. courseVariantId: data.scoreOCourseVariantId || '',
  3554. runtimeBindingId: data.scoreORuntimeBindingId || ''
  3555. };
  3556. }
  3557. if (eventId && data.variantManualEventId && eventId === data.variantManualEventId) {
  3558. return {
  3559. eventId: data.variantManualEventId,
  3560. releaseId: data.variantManualReleaseId || '',
  3561. sourceId: data.variantManualSourceId || '',
  3562. buildId: data.variantManualBuildId || '',
  3563. courseSetId: data.variantManualCourseSetId || '',
  3564. courseVariantId: data.variantManualCourseVariantId || '',
  3565. runtimeBindingId: data.variantManualRuntimeBindingId || ''
  3566. };
  3567. }
  3568. return {
  3569. eventId: data.eventId || '',
  3570. releaseId: data.releaseId || '',
  3571. sourceId: data.sourceId || '',
  3572. buildId: data.buildId || '',
  3573. courseSetId: data.courseSetId || '',
  3574. courseVariantId: data.courseVariantId || '',
  3575. runtimeBindingId: data.runtimeBindingId || ''
  3576. };
  3577. }
  3578. function applyBootstrapContext(bootstrap, explicitEventId) {
  3579. const eventId = explicitEventId || $('event-id').value || $('admin-event-ref-id').value || '';
  3580. const selected = selectBootstrapContextForEvent(bootstrap, eventId);
  3581. state.sourceId = selected.sourceId || state.sourceId;
  3582. state.buildId = selected.buildId || state.buildId;
  3583. state.releaseId = selected.releaseId || state.releaseId;
  3584. $('admin-pipeline-source-id').value = selected.sourceId || $('admin-pipeline-source-id').value;
  3585. $('admin-pipeline-build-id').value = selected.buildId || $('admin-pipeline-build-id').value;
  3586. $('admin-pipeline-release-id').value = selected.releaseId || $('admin-pipeline-release-id').value;
  3587. $('event-release-id').value = selected.releaseId || $('event-release-id').value;
  3588. $('prod-runtime-event-id').value = selected.eventId || $('prod-runtime-event-id').value;
  3589. $('prod-place-id').value = bootstrap.placeId || $('prod-place-id').value;
  3590. $('prod-map-asset-id').value = bootstrap.mapAssetId || $('prod-map-asset-id').value;
  3591. $('prod-tile-release-id').value = bootstrap.tileReleaseId || $('prod-tile-release-id').value;
  3592. $('prod-course-source-id').value = bootstrap.courseSourceId || $('prod-course-source-id').value;
  3593. $('prod-course-set-id').value = selected.courseSetId || $('prod-course-set-id').value;
  3594. $('prod-course-variant-id').value = selected.courseVariantId || $('prod-course-variant-id').value;
  3595. $('prod-runtime-binding-id').value = selected.runtimeBindingId || $('prod-runtime-binding-id').value;
  3596. syncState();
  3597. return selected;
  3598. }
  3599. function extractList(payload) {
  3600. if (Array.isArray(payload)) {
  3601. return payload;
  3602. }
  3603. if (!payload || typeof payload !== 'object') {
  3604. return [];
  3605. }
  3606. if (Array.isArray(payload.items)) {
  3607. return payload.items;
  3608. }
  3609. if (Array.isArray(payload.results)) {
  3610. return payload.results;
  3611. }
  3612. if (Array.isArray(payload.sessions)) {
  3613. return payload.sessions;
  3614. }
  3615. return [];
  3616. }
  3617. function listContainsSession(list, sessionId) {
  3618. if (!sessionId) {
  3619. return false;
  3620. }
  3621. return list.some(function(item) {
  3622. if (!item || typeof item !== 'object') {
  3623. return false;
  3624. }
  3625. if (item.id && item.id === sessionId) {
  3626. return true;
  3627. }
  3628. if (item.session && item.session.id && item.session.id === sessionId) {
  3629. return true;
  3630. }
  3631. return false;
  3632. });
  3633. }
  3634. async function runAdminDefaultPublishFlow(options) {
  3635. const ensureRuntime = options && options.ensureRuntime === true;
  3636. const bootstrapDemo = options && options.bootstrapDemo === true;
  3637. const flowTitle = ensureRuntime ? 'flow-admin-runtime-publish' : 'flow-admin-default-publish';
  3638. const eventId = $('admin-event-ref-id').value || $('event-id').value;
  3639. if (!trimmedOrUndefined(eventId)) {
  3640. throw new Error('admin event id is required');
  3641. }
  3642. if (bootstrapDemo) {
  3643. markFlowStep(flowTitle, 'bootstrap-demo');
  3644. const bootstrap = await request('POST', '/dev/bootstrap-demo');
  3645. if (bootstrap.data) {
  3646. applyBootstrapContext(bootstrap.data, eventId);
  3647. }
  3648. }
  3649. markFlowStep(flowTitle, 'get-event', { eventId: eventId });
  3650. const eventDetail = await request('GET', '/admin/events/' + encodeURIComponent(eventId), undefined, true);
  3651. if (eventDetail.data && eventDetail.data.event) {
  3652. $('admin-event-ref-id').value = eventDetail.data.event.id || $('admin-event-ref-id').value;
  3653. $('event-id').value = eventDetail.data.event.id || $('event-id').value;
  3654. $('prod-runtime-event-id').value = eventDetail.data.event.id || $('prod-runtime-event-id').value;
  3655. if (eventDetail.data.latestSource && eventDetail.data.latestSource.id) {
  3656. state.sourceId = eventDetail.data.latestSource.id;
  3657. $('admin-pipeline-source-id').value = eventDetail.data.latestSource.id;
  3658. }
  3659. if (eventDetail.data.currentRuntime && eventDetail.data.currentRuntime.runtimeBindingId) {
  3660. $('admin-release-runtime-binding-id').value = eventDetail.data.currentRuntime.runtimeBindingId;
  3661. $('prod-runtime-binding-id').value = eventDetail.data.currentRuntime.runtimeBindingId;
  3662. } else {
  3663. $('admin-release-runtime-binding-id').value = '';
  3664. $('prod-runtime-binding-id').value = '';
  3665. }
  3666. }
  3667. markFlowStep(flowTitle, 'import-presentation', { eventId: eventId });
  3668. const importedPresentation = await request('POST', '/admin/events/' + encodeURIComponent(eventId) + '/presentations/import', {
  3669. title: $('admin-presentation-import-title').value,
  3670. templateKey: $('admin-presentation-import-template-key').value,
  3671. sourceType: $('admin-presentation-import-source-type').value,
  3672. schemaUrl: $('admin-presentation-import-schema-url').value,
  3673. version: $('admin-presentation-import-version').value,
  3674. status: 'active',
  3675. isDefault: true
  3676. }, true);
  3677. if (importedPresentation.data) {
  3678. $('admin-presentation-id').value = importedPresentation.data.id || $('admin-presentation-id').value;
  3679. $('admin-release-presentation-id').value = importedPresentation.data.id || $('admin-release-presentation-id').value;
  3680. $('config-presentation-id').value = importedPresentation.data.id || $('config-presentation-id').value;
  3681. }
  3682. markFlowStep(flowTitle, 'import-bundle', { eventId: eventId });
  3683. const importedBundle = await request('POST', '/admin/events/' + encodeURIComponent(eventId) + '/content-bundles/import', {
  3684. title: $('admin-content-import-title').value,
  3685. bundleType: $('admin-content-import-bundle-type').value,
  3686. sourceType: $('admin-content-import-source-type').value,
  3687. manifestUrl: $('admin-content-import-manifest-url').value,
  3688. version: $('admin-content-import-version').value,
  3689. status: 'active',
  3690. isDefault: true,
  3691. assetManifest: parseJSONObjectOrUndefined($('admin-content-import-asset-manifest-json').value, 'Content Asset Manifest JSON')
  3692. }, true);
  3693. if (importedBundle.data) {
  3694. $('admin-content-bundle-id').value = importedBundle.data.id || $('admin-content-bundle-id').value;
  3695. $('admin-release-content-bundle-id').value = importedBundle.data.id || $('admin-release-content-bundle-id').value;
  3696. $('config-content-bundle-id').value = importedBundle.data.id || $('config-content-bundle-id').value;
  3697. }
  3698. if (ensureRuntime && !trimmedOrUndefined($('admin-release-runtime-binding-id').value || $('prod-runtime-binding-id').value)) {
  3699. const missing = [];
  3700. if (!trimmedOrUndefined($('prod-place-id').value)) {
  3701. missing.push('Place ID');
  3702. }
  3703. if (!trimmedOrUndefined($('prod-map-asset-id').value)) {
  3704. missing.push('Map Asset ID');
  3705. }
  3706. if (!trimmedOrUndefined($('prod-tile-release-id').value)) {
  3707. missing.push('Tile Release ID');
  3708. }
  3709. if (!trimmedOrUndefined($('prod-course-set-id').value)) {
  3710. missing.push('Course Set ID');
  3711. }
  3712. if (!trimmedOrUndefined($('prod-course-variant-id').value)) {
  3713. missing.push('Course Variant ID');
  3714. }
  3715. if (missing.length > 0) {
  3716. throw new Error('创建 runtime binding 前缺少字段: ' + missing.join(', '));
  3717. }
  3718. markFlowStep(flowTitle, 'create-runtime-binding', {
  3719. eventId: eventId,
  3720. placeId: $('prod-place-id').value,
  3721. mapAssetId: $('prod-map-asset-id').value,
  3722. tileReleaseId: $('prod-tile-release-id').value,
  3723. courseSetId: $('prod-course-set-id').value,
  3724. courseVariantId: $('prod-course-variant-id').value
  3725. });
  3726. const createdRuntime = await request('POST', '/admin/runtime-bindings', {
  3727. eventId: $('prod-runtime-event-id').value || eventId,
  3728. placeId: $('prod-place-id').value,
  3729. mapAssetId: $('prod-map-asset-id').value,
  3730. tileReleaseId: $('prod-tile-release-id').value,
  3731. courseSetId: $('prod-course-set-id').value,
  3732. courseVariantId: $('prod-course-variant-id').value,
  3733. status: $('prod-runtime-binding-status').value,
  3734. notes: trimmedOrUndefined($('prod-runtime-notes').value)
  3735. }, true);
  3736. if (createdRuntime.data && createdRuntime.data.id) {
  3737. $('prod-runtime-binding-id').value = createdRuntime.data.id;
  3738. $('admin-release-runtime-binding-id').value = createdRuntime.data.id;
  3739. }
  3740. }
  3741. markFlowStep(flowTitle, 'save-defaults', { eventId: eventId });
  3742. const defaults = await request('POST', '/admin/events/' + encodeURIComponent(eventId) + '/defaults', {
  3743. presentationId: trimmedOrUndefined($('admin-release-presentation-id').value),
  3744. contentBundleId: trimmedOrUndefined($('admin-release-content-bundle-id').value),
  3745. runtimeBindingId: trimmedOrUndefined($('admin-release-runtime-binding-id').value || $('prod-runtime-binding-id').value)
  3746. }, true);
  3747. if (defaults.data && defaults.data.currentPresentation && defaults.data.currentPresentation.presentationId) {
  3748. $('admin-presentation-id').value = defaults.data.currentPresentation.presentationId;
  3749. $('admin-release-presentation-id').value = defaults.data.currentPresentation.presentationId;
  3750. $('config-presentation-id').value = defaults.data.currentPresentation.presentationId;
  3751. }
  3752. if (defaults.data && defaults.data.currentContentBundle && defaults.data.currentContentBundle.contentBundleId) {
  3753. $('admin-content-bundle-id').value = defaults.data.currentContentBundle.contentBundleId;
  3754. $('admin-release-content-bundle-id').value = defaults.data.currentContentBundle.contentBundleId;
  3755. $('config-content-bundle-id').value = defaults.data.currentContentBundle.contentBundleId;
  3756. }
  3757. if (defaults.data && defaults.data.currentRuntime && defaults.data.currentRuntime.runtimeBindingId) {
  3758. $('admin-release-runtime-binding-id').value = defaults.data.currentRuntime.runtimeBindingId;
  3759. $('prod-runtime-binding-id').value = defaults.data.currentRuntime.runtimeBindingId;
  3760. }
  3761. const sourceId = $('admin-pipeline-source-id').value || state.sourceId;
  3762. if (!trimmedOrUndefined(sourceId)) {
  3763. throw new Error('no source id available for build');
  3764. }
  3765. markFlowStep(flowTitle, 'build-source', { sourceId: sourceId });
  3766. const build = await request('POST', '/admin/sources/' + encodeURIComponent(sourceId) + '/build', undefined, true);
  3767. state.sourceId = build.data.sourceId || state.sourceId;
  3768. state.buildId = build.data.id || state.buildId;
  3769. $('admin-pipeline-build-id').value = build.data.id || $('admin-pipeline-build-id').value;
  3770. $('admin-pipeline-source-id').value = build.data.sourceId || $('admin-pipeline-source-id').value;
  3771. markFlowStep(flowTitle, 'publish-build', { buildId: $('admin-pipeline-build-id').value || state.buildId });
  3772. const published = await request('POST', '/admin/builds/' + encodeURIComponent($('admin-pipeline-build-id').value || state.buildId) + '/publish', {}, true);
  3773. state.releaseId = published.data.release.releaseId || state.releaseId;
  3774. $('admin-pipeline-release-id').value = published.data.release.releaseId || $('admin-pipeline-release-id').value;
  3775. if (published.data.runtime && published.data.runtime.runtimeBindingId) {
  3776. $('admin-release-runtime-binding-id').value = published.data.runtime.runtimeBindingId;
  3777. $('prod-runtime-binding-id').value = published.data.runtime.runtimeBindingId;
  3778. }
  3779. if (published.data.presentation && published.data.presentation.presentationId) {
  3780. $('admin-release-presentation-id').value = published.data.presentation.presentationId;
  3781. $('admin-presentation-id').value = published.data.presentation.presentationId;
  3782. $('config-presentation-id').value = published.data.presentation.presentationId;
  3783. }
  3784. if (published.data.contentBundle && published.data.contentBundle.contentBundleId) {
  3785. $('admin-release-content-bundle-id').value = published.data.contentBundle.contentBundleId;
  3786. $('admin-content-bundle-id').value = published.data.contentBundle.contentBundleId;
  3787. $('config-content-bundle-id').value = published.data.contentBundle.contentBundleId;
  3788. }
  3789. markFlowStep(flowTitle, 'get-release', { releaseId: $('admin-pipeline-release-id').value || state.releaseId });
  3790. const releaseDetail = await request('GET', '/admin/releases/' + encodeURIComponent($('admin-pipeline-release-id').value || state.releaseId), undefined, true);
  3791. setDefaultPublishExpectation(releaseDetail.data);
  3792. writeLog(flowTitle + '.expected', {
  3793. releaseId: $('flow-admin-release-result').textContent,
  3794. presentationId: $('flow-admin-presentation-result').textContent,
  3795. contentBundleId: $('flow-admin-content-bundle-result').textContent,
  3796. runtimeBindingId: $('flow-admin-runtime-result').textContent,
  3797. verdict: $('flow-admin-verdict').textContent
  3798. });
  3799. syncState();
  3800. persistState();
  3801. return releaseDetail;
  3802. }
  3803. async function runStandardRegressionFlow() {
  3804. const flowTitle = 'flow-standard-regression';
  3805. const eventId = $('event-id').value || $('admin-event-ref-id').value;
  3806. if (!trimmedOrUndefined(eventId)) {
  3807. throw new Error('event id is required');
  3808. }
  3809. resetStandardRegressionExpectation();
  3810. markFlowStep(flowTitle, 'prepare-release', { eventId: eventId });
  3811. const releaseDetail = await runAdminDefaultPublishFlow({ ensureRuntime: true, bootstrapDemo: true });
  3812. const publishPass = $('flow-admin-verdict').textContent.indexOf('通过') === 0;
  3813. markFlowStep(flowTitle, 'login-wechat', {
  3814. code: $('wechat-code').value,
  3815. deviceKey: $('wechat-device').value
  3816. });
  3817. const login = await request('POST', '/auth/login/wechat-mini', {
  3818. code: $('wechat-code').value,
  3819. clientType: 'wechat',
  3820. deviceKey: $('wechat-device').value
  3821. });
  3822. state.accessToken = login.data.tokens.accessToken;
  3823. state.refreshToken = login.data.tokens.refreshToken;
  3824. markFlowStep(flowTitle, 'event-play', {
  3825. eventId: eventId
  3826. });
  3827. const play = await request('GET', '/events/' + encodeURIComponent(eventId) + '/play', undefined, true);
  3828. setCurrentFlowStatusFromPlayResponse(play);
  3829. if (play && play.data) {
  3830. setCurrentPreviewStatusFromPayload(play.data);
  3831. }
  3832. const playPass = !!(play.data && play.data.play && play.data.resolvedRelease && play.data.resolvedRelease.manifestUrl);
  3833. markFlowStep(flowTitle, 'event-launch', {
  3834. eventId: eventId,
  3835. releaseId: $('event-release-id').value || state.releaseId,
  3836. variantId: trimmedOrUndefined($('event-variant-id').value)
  3837. });
  3838. const launch = await request('POST', '/events/' + encodeURIComponent(eventId) + '/launch', {
  3839. releaseId: $('event-release-id').value,
  3840. variantId: trimmedOrUndefined($('event-variant-id').value),
  3841. clientType: $('sms-client-type').value,
  3842. deviceKey: $('event-device').value
  3843. }, true);
  3844. state.sessionId = launch.data.launch.business.sessionId;
  3845. state.sessionToken = launch.data.launch.business.sessionToken;
  3846. syncState();
  3847. const launchPass = !!(
  3848. launch.data &&
  3849. launch.data.launch &&
  3850. launch.data.launch.business &&
  3851. launch.data.launch.business.sessionId &&
  3852. launch.data.launch.resolvedRelease &&
  3853. launch.data.launch.resolvedRelease.manifestUrl
  3854. );
  3855. const launchConfigSummary = await resolveLaunchConfigSummary(launch.data);
  3856. setLaunchConfigSummary(launchConfigSummary);
  3857. setCurrentFlowStatusFromLaunch(launchConfigSummary, launch.data);
  3858. writeLog(flowTitle + '.launch-summary', launchConfigSummary);
  3859. markFlowStep(flowTitle, 'session-start', {
  3860. sessionId: state.sessionId
  3861. });
  3862. await request('POST', '/sessions/' + encodeURIComponent(state.sessionId) + '/start', {
  3863. sessionToken: state.sessionToken
  3864. });
  3865. markFlowStep(flowTitle, 'session-finish', {
  3866. sessionId: state.sessionId,
  3867. status: $('finish-status').value
  3868. });
  3869. await request('POST', '/sessions/' + encodeURIComponent(state.sessionId) + '/finish', {
  3870. sessionToken: state.sessionToken,
  3871. status: $('finish-status').value,
  3872. summary: buildFinishSummary()
  3873. });
  3874. markFlowStep(flowTitle, 'session-result', {
  3875. sessionId: state.sessionId
  3876. });
  3877. const sessionResult = await request('GET', '/sessions/' + encodeURIComponent(state.sessionId) + '/result', undefined, true);
  3878. const resultPass = !!(
  3879. sessionResult.data &&
  3880. sessionResult.data.session &&
  3881. sessionResult.data.session.id === state.sessionId &&
  3882. sessionResult.data.result
  3883. );
  3884. markFlowStep(flowTitle, 'history-check', {
  3885. sessionId: state.sessionId
  3886. });
  3887. const mySessions = await request('GET', '/me/sessions?limit=10', undefined, true);
  3888. const myResults = await request('GET', '/me/results?limit=10', undefined, true);
  3889. const sessionsList = extractList(mySessions.data);
  3890. const resultsList = extractList(myResults.data);
  3891. const historyPass = listContainsSession(sessionsList, state.sessionId) && listContainsSession(resultsList, state.sessionId);
  3892. const summary = {
  3893. publish: publishPass ? '通过:发布链可重复跑通' : '未通过:发布链未返回通过判定',
  3894. play: playPass ? '通过:play 返回 resolvedRelease / play 摘要' : '未通过:play 缺少关键摘要',
  3895. launch: launchPass ? '通过:launch 返回 manifest + session' : '未通过:launch 缺少 manifest 或 session',
  3896. result: resultPass ? '通过:单局 result 可直接回查' : '未通过:单局 result 未回查成功',
  3897. history: historyPass ? '通过:me/sessions + me/results 均收录本局' : '未通过:history 未同时收录本局',
  3898. sessionId: state.sessionId || '-',
  3899. overall: publishPass && playPass && launchPass && resultPass && historyPass ? '通过:launch / play / result / history 回归已跑通' : '未通过:请看上面分项和日志'
  3900. };
  3901. setStandardRegressionExpectation(summary);
  3902. writeLog(flowTitle + '.expected', {
  3903. eventId: eventId,
  3904. releaseId: releaseDetail && releaseDetail.data ? releaseDetail.data.id : state.releaseId,
  3905. sessionId: state.sessionId,
  3906. publish: summary.publish,
  3907. play: summary.play,
  3908. launch: summary.launch,
  3909. result: summary.result,
  3910. history: summary.history,
  3911. overall: summary.overall
  3912. });
  3913. persistState();
  3914. return {
  3915. data: {
  3916. eventId: eventId,
  3917. releaseId: releaseDetail && releaseDetail.data ? releaseDetail.data.id : state.releaseId,
  3918. sessionId: state.sessionId,
  3919. publish: publishPass,
  3920. play: playPass,
  3921. launch: launchPass,
  3922. result: resultPass,
  3923. history: historyPass,
  3924. overall: publishPass && playPass && launchPass && resultPass && historyPass
  3925. }
  3926. };
  3927. }
  3928. function applyFrontendDemoSelection(options) {
  3929. resetLaunchConfigSummary();
  3930. $('entry-channel-code').value = 'mini-demo';
  3931. $('entry-channel-type').value = 'wechat_mini';
  3932. $('event-id').value = options.eventId;
  3933. $('event-release-id').value = options.releaseId;
  3934. $('event-variant-id').value = options.variantId || '';
  3935. $('config-event-id').value = options.eventId;
  3936. $('admin-event-ref-id').value = options.eventId;
  3937. $('local-config-file').value = options.localConfigFile || $('local-config-file').value;
  3938. if (options.gameModeCode) {
  3939. $('admin-game-mode-code').value = options.gameModeCode;
  3940. $('prod-course-mode').value = options.gameModeCode;
  3941. }
  3942. $('prod-runtime-event-id').value = options.eventId;
  3943. $('prod-course-set-id').value = options.courseSetId || $('prod-course-set-id').value;
  3944. $('prod-course-variant-id').value = options.courseVariantId || $('prod-course-variant-id').value;
  3945. $('prod-runtime-binding-id').value = options.runtimeBindingId || '';
  3946. $('admin-release-runtime-binding-id').value = options.runtimeBindingId || '';
  3947. $('admin-pipeline-source-id').value = options.sourceId || '';
  3948. $('admin-pipeline-build-id').value = options.buildId || '';
  3949. applyDemoImportInputs(options.demoKind || 'classic');
  3950. state.sourceId = options.sourceId || '';
  3951. state.buildId = options.buildId || '';
  3952. state.releaseId = options.releaseId || state.releaseId;
  3953. localStorage.setItem(MODE_KEY, 'frontend');
  3954. syncWorkbenchMode();
  3955. writeLog(options.logTitle, {
  3956. eventId: $('event-id').value,
  3957. releaseId: $('event-release-id').value,
  3958. variantId: $('event-variant-id').value || null,
  3959. localConfigFile: $('local-config-file').value
  3960. });
  3961. setStatus(options.statusText);
  3962. }
  3963. function setStatus(text, isError = false, isRunning = false) {
  3964. statusEl.textContent = text;
  3965. statusEl.className = 'status' + (isError ? ' error' : '') + (isRunning ? ' running' : '');
  3966. }
  3967. function resetProgress() {
  3968. progressLabelEl.textContent = '当前进度:待执行';
  3969. progressStepEl.textContent = '0 / 0';
  3970. progressFillEl.style.width = '0%';
  3971. progressNoteEl.textContent = '长流程会在这里显示当前步骤。';
  3972. }
  3973. function updateFlowProgress(flowTitle, stepKey, detailText) {
  3974. const plan = FLOW_STEP_PLANS[flowTitle];
  3975. if (!plan || !plan.length) {
  3976. return;
  3977. }
  3978. const index = stepKey ? plan.indexOf(stepKey) : -1;
  3979. const current = index >= 0 ? index + 1 : 0;
  3980. const total = plan.length;
  3981. const percent = current > 0 ? Math.max(8, Math.round(current / total * 100)) : 0;
  3982. progressLabelEl.textContent = '当前进度:' + flowTitle;
  3983. progressStepEl.textContent = current + ' / ' + total;
  3984. progressFillEl.style.width = percent + '%';
  3985. progressNoteEl.textContent = detailText || (stepKey ? ('正在执行:' + stepKey) : '长流程执行中');
  3986. }
  3987. function completeFlowProgress(flowTitle, success, detailText) {
  3988. const plan = FLOW_STEP_PLANS[flowTitle];
  3989. if (!plan || !plan.length) {
  3990. return;
  3991. }
  3992. progressLabelEl.textContent = '当前进度:' + flowTitle;
  3993. progressStepEl.textContent = plan.length + ' / ' + plan.length;
  3994. progressFillEl.style.width = success ? '100%' : progressFillEl.style.width;
  3995. progressNoteEl.textContent = detailText || (success ? '长流程已完成。' : '长流程执行失败。');
  3996. }
  3997. function markFlowStep(flowTitle, stepKey, payload) {
  3998. const detail = payload && payload.eventId
  3999. ? (stepKey + ' · ' + payload.eventId)
  4000. : (payload && payload.sessionId
  4001. ? (stepKey + ' · ' + payload.sessionId)
  4002. : ('正在执行:' + stepKey));
  4003. updateFlowProgress(flowTitle, stepKey, detail);
  4004. writeLog(flowTitle + '.step', Object.assign({ step: stepKey }, payload || {}));
  4005. }
  4006. function setButtonRunning(button, running, label) {
  4007. if (!button) {
  4008. return;
  4009. }
  4010. if (!button.dataset.originalLabel) {
  4011. button.dataset.originalLabel = button.innerHTML;
  4012. }
  4013. button.disabled = !!running;
  4014. button.classList.toggle('is-running', !!running);
  4015. if (running) {
  4016. button.innerHTML = label || '执行中';
  4017. } else if (button.dataset.originalLabel) {
  4018. button.innerHTML = button.dataset.originalLabel;
  4019. }
  4020. }
  4021. function getActiveTriggerButton() {
  4022. const active = document.activeElement;
  4023. if (active && active.tagName === 'BUTTON') {
  4024. return active;
  4025. }
  4026. return null;
  4027. }
  4028. function getWorkbenchMode() {
  4029. return localStorage.getItem(MODE_KEY) || 'frontend';
  4030. }
  4031. function syncWorkbenchMode() {
  4032. const mode = getWorkbenchMode();
  4033. modeNodes.forEach(function(node) {
  4034. const supported = String(node.dataset.modes || '').split(/\s+/).filter(Boolean);
  4035. const visible = mode === 'all' || supported.includes(mode) || supported.includes('common');
  4036. node.classList.toggle('mode-hidden', !visible);
  4037. });
  4038. modeButtons.forEach(function(button) {
  4039. button.classList.toggle('active', button.dataset.modeBtn === mode);
  4040. });
  4041. scheduleMasonryLayout();
  4042. }
  4043. function normalizeLogPayload(payload) {
  4044. if (payload instanceof Error) {
  4045. return {
  4046. name: payload.name,
  4047. message: payload.message,
  4048. stack: payload.stack
  4049. };
  4050. }
  4051. if (payload && typeof payload === 'object') {
  4052. if (payload.error instanceof Error) {
  4053. const next = Object.assign({}, payload);
  4054. next.error = normalizeLogPayload(payload.error);
  4055. return next;
  4056. }
  4057. return payload;
  4058. }
  4059. return payload;
  4060. }
  4061. function writeLog(title, payload) {
  4062. logEl.textContent = '[' + new Date().toLocaleString() + '] ' + title + '\n' + JSON.stringify(normalizeLogPayload(payload), null, 2);
  4063. }
  4064. function persistState() {
  4065. const payload = {
  4066. state,
  4067. fields: collectFields()
  4068. };
  4069. localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
  4070. }
  4071. function collectFields() {
  4072. return {
  4073. smsClientType: $('sms-client-type').value,
  4074. smsScene: $('sms-scene').value,
  4075. smsMobile: $('sms-mobile').value,
  4076. smsDevice: $('sms-device').value,
  4077. smsCountry: $('sms-country').value,
  4078. smsCode: $('sms-code').value,
  4079. wechatCode: $('wechat-code').value,
  4080. wechatDevice: $('wechat-device').value,
  4081. localConfigFile: $('local-config-file').value,
  4082. configEventId: $('config-event-id').value,
  4083. configRuntimeBindingId: $('config-runtime-binding-id').value,
  4084. configPresentationId: $('config-presentation-id').value,
  4085. configContentBundleId: $('config-content-bundle-id').value,
  4086. entryChannelCode: $('entry-channel-code').value,
  4087. entryChannelType: $('entry-channel-type').value,
  4088. eventId: $('event-id').value,
  4089. eventReleaseId: $('event-release-id').value,
  4090. eventVariantId: $('event-variant-id').value,
  4091. eventDevice: $('event-device').value,
  4092. finishStatus: $('finish-status').value,
  4093. finishDuration: $('finish-duration').value,
  4094. finishScore: $('finish-score').value,
  4095. finishControlsDone: $('finish-controls-done').value,
  4096. finishControlsTotal: $('finish-controls-total').value,
  4097. finishDistance: $('finish-distance').value,
  4098. finishSpeed: $('finish-speed').value,
  4099. finishHeartRate: $('finish-heart-rate').value,
  4100. prodPlaceCode: $('prod-place-code').value,
  4101. prodPlaceName: $('prod-place-name').value,
  4102. prodPlaceId: $('prod-place-id').value,
  4103. prodPlaceStatus: $('prod-place-status').value,
  4104. prodPlaceRegion: $('prod-place-region').value,
  4105. prodPlaceCoverUrl: $('prod-place-cover-url').value,
  4106. prodMapAssetCode: $('prod-map-asset-code').value,
  4107. prodMapAssetName: $('prod-map-asset-name').value,
  4108. prodMapAssetId: $('prod-map-asset-id').value,
  4109. prodMapAssetLegacyMapId: $('prod-map-asset-legacy-map-id').value,
  4110. prodMapAssetType: $('prod-map-asset-type').value,
  4111. prodMapAssetStatus: $('prod-map-asset-status').value,
  4112. prodTileReleaseId: $('prod-tile-release-id').value,
  4113. prodTileLegacyVersionId: $('prod-tile-legacy-version-id').value,
  4114. prodTileVersionCode: $('prod-tile-version-code').value,
  4115. prodTileStatus: $('prod-tile-status').value,
  4116. prodTileBaseUrl: $('prod-tile-base-url').value,
  4117. prodTileMetaUrl: $('prod-tile-meta-url').value,
  4118. prodCourseSourceId: $('prod-course-source-id').value,
  4119. prodCourseSourceLegacyPlayfieldId: $('prod-course-source-legacy-playfield-id').value,
  4120. prodCourseSourceLegacyVersionId: $('prod-course-source-legacy-version-id').value,
  4121. prodCourseSourceType: $('prod-course-source-type').value,
  4122. prodCourseSourceFileUrl: $('prod-course-source-file-url').value,
  4123. prodCourseSourceStatus: $('prod-course-source-status').value,
  4124. prodCourseSetCode: $('prod-course-set-code').value,
  4125. prodCourseSetName: $('prod-course-set-name').value,
  4126. prodCourseSetId: $('prod-course-set-id').value,
  4127. prodCourseMode: $('prod-course-mode').value,
  4128. prodCourseSetStatus: $('prod-course-set-status').value,
  4129. prodCourseDefaultRouteCode: $('prod-course-default-route-code').value,
  4130. prodCourseRoutesJSON: $('prod-course-routes-json').value,
  4131. prodCourseVariantId: $('prod-course-variant-id').value,
  4132. prodCourseVariantName: $('prod-course-variant-name').value,
  4133. prodCourseVariantRouteCode: $('prod-course-variant-route-code').value,
  4134. prodCourseVariantStatus: $('prod-course-variant-status').value,
  4135. prodCourseVariantControlCount: $('prod-course-variant-control-count').value,
  4136. prodRuntimeBindingId: $('prod-runtime-binding-id').value,
  4137. prodRuntimeEventId: $('prod-runtime-event-id').value,
  4138. prodRuntimeBindingStatus: $('prod-runtime-binding-status').value,
  4139. prodRuntimeNotes: $('prod-runtime-notes').value,
  4140. adminMapCode: $('admin-map-code').value,
  4141. adminMapName: $('admin-map-name').value,
  4142. adminMapId: $('admin-map-id').value,
  4143. adminMapVersionId: $('admin-map-version-id').value,
  4144. adminMapVersionCode: $('admin-map-version-code').value,
  4145. adminMapStatus: $('admin-map-status').value,
  4146. adminMapmetaUrl: $('admin-mapmeta-url').value,
  4147. adminTilesRootUrl: $('admin-tiles-root-url').value,
  4148. adminPlayfieldCode: $('admin-playfield-code').value,
  4149. adminPlayfieldName: $('admin-playfield-name').value,
  4150. adminPlayfieldId: $('admin-playfield-id').value,
  4151. adminPlayfieldVersionId: $('admin-playfield-version-id').value,
  4152. adminPlayfieldKind: $('admin-playfield-kind').value,
  4153. adminPlayfieldStatus: $('admin-playfield-status').value,
  4154. adminPlayfieldVersionCode: $('admin-playfield-version-code').value,
  4155. adminPlayfieldSourceType: $('admin-playfield-source-type').value,
  4156. adminPlayfieldSourceUrl: $('admin-playfield-source-url').value,
  4157. adminPlayfieldControlCount: $('admin-playfield-control-count').value,
  4158. adminPackCode: $('admin-pack-code').value,
  4159. adminPackName: $('admin-pack-name').value,
  4160. adminPackId: $('admin-pack-id').value,
  4161. adminPackVersionId: $('admin-pack-version-id').value,
  4162. adminPackVersionCode: $('admin-pack-version-code').value,
  4163. adminPackStatus: $('admin-pack-status').value,
  4164. adminPackContentUrl: $('admin-pack-content-url').value,
  4165. adminPackAudioUrl: $('admin-pack-audio-url').value,
  4166. adminPackThemeCode: $('admin-pack-theme-code').value,
  4167. adminPublishedAssetRoot: $('admin-published-asset-root').value,
  4168. adminTenantCode: $('admin-tenant-code').value,
  4169. adminEventStatus: $('admin-event-status').value,
  4170. adminEventRefId: $('admin-event-ref-id').value,
  4171. adminEventSlug: $('admin-event-slug').value,
  4172. adminEventName: $('admin-event-name').value,
  4173. adminEventSummary: $('admin-event-summary').value,
  4174. adminGameModeCode: $('admin-game-mode-code').value,
  4175. adminRouteCode: $('admin-route-code').value,
  4176. adminSourceNotes: $('admin-source-notes').value,
  4177. adminOverridesJSON: $('admin-overrides-json').value,
  4178. adminPresentationId: $('admin-presentation-id').value,
  4179. adminPresentationCode: $('admin-presentation-code').value,
  4180. adminPresentationName: $('admin-presentation-name').value,
  4181. adminPresentationType: $('admin-presentation-type').value,
  4182. adminPresentationSchemaJSON: $('admin-presentation-schema-json').value,
  4183. adminPresentationImportTitle: $('admin-presentation-import-title').value,
  4184. adminPresentationImportTemplateKey: $('admin-presentation-import-template-key').value,
  4185. adminPresentationImportSourceType: $('admin-presentation-import-source-type').value,
  4186. adminPresentationImportVersion: $('admin-presentation-import-version').value,
  4187. adminPresentationImportSchemaURL: $('admin-presentation-import-schema-url').value,
  4188. adminContentBundleId: $('admin-content-bundle-id').value,
  4189. adminContentBundleCode: $('admin-content-bundle-code').value,
  4190. adminContentBundleName: $('admin-content-bundle-name').value,
  4191. adminContentEntryURL: $('admin-content-entry-url').value,
  4192. adminContentAssetRootURL: $('admin-content-asset-root-url').value,
  4193. adminContentMetadataJSON: $('admin-content-metadata-json').value,
  4194. adminContentImportTitle: $('admin-content-import-title').value,
  4195. adminContentImportBundleType: $('admin-content-import-bundle-type').value,
  4196. adminContentImportSourceType: $('admin-content-import-source-type').value,
  4197. adminContentImportVersion: $('admin-content-import-version').value,
  4198. adminContentImportManifestURL: $('admin-content-import-manifest-url').value,
  4199. adminContentImportAssetManifestJSON: $('admin-content-import-asset-manifest-json').value,
  4200. adminPipelineSourceId: $('admin-pipeline-source-id').value,
  4201. adminPipelineBuildId: $('admin-pipeline-build-id').value,
  4202. adminPipelineReleaseId: $('admin-pipeline-release-id').value,
  4203. adminReleaseRuntimeBindingId: $('admin-release-runtime-binding-id').value,
  4204. adminReleasePresentationId: $('admin-release-presentation-id').value,
  4205. adminReleaseContentBundleId: $('admin-release-content-bundle-id').value,
  4206. adminRollbackReleaseId: $('admin-rollback-release-id').value
  4207. };
  4208. }
  4209. function restoreState() {
  4210. const raw = localStorage.getItem(STORAGE_KEY);
  4211. if (!raw) {
  4212. return;
  4213. }
  4214. try {
  4215. const payload = JSON.parse(raw);
  4216. if (payload.state) {
  4217. state.accessToken = payload.state.accessToken || '';
  4218. state.refreshToken = payload.state.refreshToken || '';
  4219. state.sourceId = payload.state.sourceId || '';
  4220. state.buildId = payload.state.buildId || '';
  4221. state.releaseId = payload.state.releaseId || '';
  4222. state.sessionId = payload.state.sessionId || '';
  4223. state.sessionToken = payload.state.sessionToken || '';
  4224. state.lastCurl = payload.state.lastCurl || '';
  4225. }
  4226. applyFields(payload.fields || {});
  4227. } catch (_) {}
  4228. }
  4229. function applyFields(fields) {
  4230. $('sms-client-type').value = fields.smsClientType || $('sms-client-type').value;
  4231. $('sms-scene').value = fields.smsScene || $('sms-scene').value;
  4232. $('sms-mobile').value = fields.smsMobile || $('sms-mobile').value;
  4233. $('sms-device').value = fields.smsDevice || $('sms-device').value;
  4234. $('sms-country').value = fields.smsCountry || $('sms-country').value;
  4235. $('sms-code').value = fields.smsCode || '';
  4236. $('wechat-code').value = fields.wechatCode || $('wechat-code').value;
  4237. $('wechat-device').value = fields.wechatDevice || $('wechat-device').value;
  4238. $('local-config-file').value = fields.localConfigFile || $('local-config-file').value;
  4239. $('config-event-id').value = fields.configEventId || $('config-event-id').value;
  4240. $('config-runtime-binding-id').value = fields.configRuntimeBindingId || $('config-runtime-binding-id').value;
  4241. $('config-presentation-id').value = fields.configPresentationId || $('config-presentation-id').value;
  4242. $('config-content-bundle-id').value = fields.configContentBundleId || $('config-content-bundle-id').value;
  4243. $('entry-channel-code').value = fields.entryChannelCode || $('entry-channel-code').value;
  4244. $('entry-channel-type').value = fields.entryChannelType || $('entry-channel-type').value;
  4245. $('event-id').value = fields.eventId || $('event-id').value;
  4246. $('event-release-id').value = fields.eventReleaseId || $('event-release-id').value;
  4247. $('event-variant-id').value = fields.eventVariantId || $('event-variant-id').value;
  4248. $('event-device').value = fields.eventDevice || $('event-device').value;
  4249. $('finish-status').value = fields.finishStatus || $('finish-status').value;
  4250. $('finish-duration').value = fields.finishDuration || $('finish-duration').value;
  4251. $('finish-score').value = fields.finishScore || $('finish-score').value;
  4252. $('finish-controls-done').value = fields.finishControlsDone || $('finish-controls-done').value;
  4253. $('finish-controls-total').value = fields.finishControlsTotal || $('finish-controls-total').value;
  4254. $('finish-distance').value = fields.finishDistance || $('finish-distance').value;
  4255. $('finish-speed').value = fields.finishSpeed || $('finish-speed').value;
  4256. $('finish-heart-rate').value = fields.finishHeartRate || $('finish-heart-rate').value;
  4257. $('prod-place-code').value = fields.prodPlaceCode || $('prod-place-code').value;
  4258. $('prod-place-name').value = fields.prodPlaceName || $('prod-place-name').value;
  4259. $('prod-place-id').value = fields.prodPlaceId || $('prod-place-id').value;
  4260. $('prod-place-status').value = fields.prodPlaceStatus || $('prod-place-status').value;
  4261. $('prod-place-region').value = fields.prodPlaceRegion || $('prod-place-region').value;
  4262. $('prod-place-cover-url').value = fields.prodPlaceCoverUrl || $('prod-place-cover-url').value;
  4263. $('prod-map-asset-code').value = fields.prodMapAssetCode || $('prod-map-asset-code').value;
  4264. $('prod-map-asset-name').value = fields.prodMapAssetName || $('prod-map-asset-name').value;
  4265. $('prod-map-asset-id').value = fields.prodMapAssetId || $('prod-map-asset-id').value;
  4266. $('prod-map-asset-legacy-map-id').value = fields.prodMapAssetLegacyMapId || $('prod-map-asset-legacy-map-id').value;
  4267. $('prod-map-asset-type').value = fields.prodMapAssetType || $('prod-map-asset-type').value;
  4268. $('prod-map-asset-status').value = fields.prodMapAssetStatus || $('prod-map-asset-status').value;
  4269. $('prod-tile-release-id').value = fields.prodTileReleaseId || $('prod-tile-release-id').value;
  4270. $('prod-tile-legacy-version-id').value = fields.prodTileLegacyVersionId || $('prod-tile-legacy-version-id').value;
  4271. $('prod-tile-version-code').value = fields.prodTileVersionCode || $('prod-tile-version-code').value;
  4272. $('prod-tile-status').value = fields.prodTileStatus || $('prod-tile-status').value;
  4273. $('prod-tile-base-url').value = fields.prodTileBaseUrl || $('prod-tile-base-url').value;
  4274. $('prod-tile-meta-url').value = fields.prodTileMetaUrl || $('prod-tile-meta-url').value;
  4275. $('prod-course-source-id').value = fields.prodCourseSourceId || $('prod-course-source-id').value;
  4276. $('prod-course-source-legacy-playfield-id').value = fields.prodCourseSourceLegacyPlayfieldId || $('prod-course-source-legacy-playfield-id').value;
  4277. $('prod-course-source-legacy-version-id').value = fields.prodCourseSourceLegacyVersionId || $('prod-course-source-legacy-version-id').value;
  4278. $('prod-course-source-type').value = fields.prodCourseSourceType || $('prod-course-source-type').value;
  4279. $('prod-course-source-file-url').value = fields.prodCourseSourceFileUrl || $('prod-course-source-file-url').value;
  4280. $('prod-course-source-status').value = fields.prodCourseSourceStatus || $('prod-course-source-status').value;
  4281. $('prod-course-set-code').value = fields.prodCourseSetCode || $('prod-course-set-code').value;
  4282. $('prod-course-set-name').value = fields.prodCourseSetName || $('prod-course-set-name').value;
  4283. $('prod-course-set-id').value = fields.prodCourseSetId || $('prod-course-set-id').value;
  4284. $('prod-course-mode').value = fields.prodCourseMode || $('prod-course-mode').value;
  4285. $('prod-course-set-status').value = fields.prodCourseSetStatus || $('prod-course-set-status').value;
  4286. $('prod-course-default-route-code').value = fields.prodCourseDefaultRouteCode || $('prod-course-default-route-code').value;
  4287. $('prod-course-routes-json').value = fields.prodCourseRoutesJSON || $('prod-course-routes-json').value;
  4288. $('prod-course-variant-id').value = fields.prodCourseVariantId || $('prod-course-variant-id').value;
  4289. $('prod-course-variant-name').value = fields.prodCourseVariantName || $('prod-course-variant-name').value;
  4290. $('prod-course-variant-route-code').value = fields.prodCourseVariantRouteCode || $('prod-course-variant-route-code').value;
  4291. $('prod-course-variant-status').value = fields.prodCourseVariantStatus || $('prod-course-variant-status').value;
  4292. $('prod-course-variant-control-count').value = fields.prodCourseVariantControlCount || $('prod-course-variant-control-count').value;
  4293. $('prod-runtime-binding-id').value = fields.prodRuntimeBindingId || $('prod-runtime-binding-id').value;
  4294. $('prod-runtime-event-id').value = fields.prodRuntimeEventId || $('prod-runtime-event-id').value;
  4295. $('prod-runtime-binding-status').value = fields.prodRuntimeBindingStatus || $('prod-runtime-binding-status').value;
  4296. $('prod-runtime-notes').value = fields.prodRuntimeNotes || $('prod-runtime-notes').value;
  4297. $('admin-map-code').value = fields.adminMapCode || $('admin-map-code').value;
  4298. $('admin-map-name').value = fields.adminMapName || $('admin-map-name').value;
  4299. $('admin-map-id').value = fields.adminMapId || $('admin-map-id').value;
  4300. $('admin-map-version-id').value = fields.adminMapVersionId || $('admin-map-version-id').value;
  4301. $('admin-map-version-code').value = fields.adminMapVersionCode || $('admin-map-version-code').value;
  4302. $('admin-map-status').value = fields.adminMapStatus || $('admin-map-status').value;
  4303. $('admin-mapmeta-url').value = fields.adminMapmetaUrl || $('admin-mapmeta-url').value;
  4304. $('admin-tiles-root-url').value = fields.adminTilesRootUrl || $('admin-tiles-root-url').value;
  4305. $('admin-playfield-code').value = fields.adminPlayfieldCode || $('admin-playfield-code').value;
  4306. $('admin-playfield-name').value = fields.adminPlayfieldName || $('admin-playfield-name').value;
  4307. $('admin-playfield-id').value = fields.adminPlayfieldId || $('admin-playfield-id').value;
  4308. $('admin-playfield-version-id').value = fields.adminPlayfieldVersionId || $('admin-playfield-version-id').value;
  4309. $('admin-playfield-kind').value = fields.adminPlayfieldKind || $('admin-playfield-kind').value;
  4310. $('admin-playfield-status').value = fields.adminPlayfieldStatus || $('admin-playfield-status').value;
  4311. $('admin-playfield-version-code').value = fields.adminPlayfieldVersionCode || $('admin-playfield-version-code').value;
  4312. $('admin-playfield-source-type').value = fields.adminPlayfieldSourceType || $('admin-playfield-source-type').value;
  4313. $('admin-playfield-source-url').value = fields.adminPlayfieldSourceUrl || $('admin-playfield-source-url').value;
  4314. $('admin-playfield-control-count').value = fields.adminPlayfieldControlCount || $('admin-playfield-control-count').value;
  4315. $('admin-pack-code').value = fields.adminPackCode || $('admin-pack-code').value;
  4316. $('admin-pack-name').value = fields.adminPackName || $('admin-pack-name').value;
  4317. $('admin-pack-id').value = fields.adminPackId || $('admin-pack-id').value;
  4318. $('admin-pack-version-id').value = fields.adminPackVersionId || $('admin-pack-version-id').value;
  4319. $('admin-pack-version-code').value = fields.adminPackVersionCode || $('admin-pack-version-code').value;
  4320. $('admin-pack-status').value = fields.adminPackStatus || $('admin-pack-status').value;
  4321. $('admin-pack-content-url').value = fields.adminPackContentUrl || $('admin-pack-content-url').value;
  4322. $('admin-pack-audio-url').value = fields.adminPackAudioUrl || $('admin-pack-audio-url').value;
  4323. $('admin-pack-theme-code').value = fields.adminPackThemeCode || $('admin-pack-theme-code').value;
  4324. $('admin-published-asset-root').value = fields.adminPublishedAssetRoot || $('admin-published-asset-root').value;
  4325. $('admin-tenant-code').value = fields.adminTenantCode || $('admin-tenant-code').value;
  4326. $('admin-event-status').value = fields.adminEventStatus || $('admin-event-status').value;
  4327. $('admin-event-ref-id').value = fields.adminEventRefId || $('admin-event-ref-id').value;
  4328. $('admin-event-slug').value = fields.adminEventSlug || $('admin-event-slug').value;
  4329. $('admin-event-name').value = fields.adminEventName || $('admin-event-name').value;
  4330. $('admin-event-summary').value = fields.adminEventSummary || $('admin-event-summary').value;
  4331. $('admin-game-mode-code').value = fields.adminGameModeCode || $('admin-game-mode-code').value;
  4332. $('admin-route-code').value = fields.adminRouteCode || $('admin-route-code').value;
  4333. $('admin-source-notes').value = fields.adminSourceNotes || $('admin-source-notes').value;
  4334. $('admin-overrides-json').value = fields.adminOverridesJSON || $('admin-overrides-json').value;
  4335. $('admin-presentation-id').value = fields.adminPresentationId || $('admin-presentation-id').value;
  4336. $('admin-presentation-code').value = fields.adminPresentationCode || $('admin-presentation-code').value;
  4337. $('admin-presentation-name').value = fields.adminPresentationName || $('admin-presentation-name').value;
  4338. $('admin-presentation-type').value = fields.adminPresentationType || $('admin-presentation-type').value;
  4339. $('admin-presentation-schema-json').value = fields.adminPresentationSchemaJSON || $('admin-presentation-schema-json').value;
  4340. $('admin-presentation-import-title').value = fields.adminPresentationImportTitle || $('admin-presentation-import-title').value;
  4341. $('admin-presentation-import-template-key').value = fields.adminPresentationImportTemplateKey || $('admin-presentation-import-template-key').value;
  4342. $('admin-presentation-import-source-type').value = fields.adminPresentationImportSourceType || $('admin-presentation-import-source-type').value;
  4343. $('admin-presentation-import-version').value = fields.adminPresentationImportVersion || $('admin-presentation-import-version').value;
  4344. $('admin-presentation-import-schema-url').value = fields.adminPresentationImportSchemaURL || $('admin-presentation-import-schema-url').value;
  4345. $('admin-content-bundle-id').value = fields.adminContentBundleId || $('admin-content-bundle-id').value;
  4346. $('admin-content-bundle-code').value = fields.adminContentBundleCode || $('admin-content-bundle-code').value;
  4347. $('admin-content-bundle-name').value = fields.adminContentBundleName || $('admin-content-bundle-name').value;
  4348. $('admin-content-entry-url').value = fields.adminContentEntryURL || $('admin-content-entry-url').value;
  4349. $('admin-content-asset-root-url').value = fields.adminContentAssetRootURL || $('admin-content-asset-root-url').value;
  4350. $('admin-content-metadata-json').value = fields.adminContentMetadataJSON || $('admin-content-metadata-json').value;
  4351. $('admin-content-import-title').value = fields.adminContentImportTitle || $('admin-content-import-title').value;
  4352. $('admin-content-import-bundle-type').value = fields.adminContentImportBundleType || $('admin-content-import-bundle-type').value;
  4353. $('admin-content-import-source-type').value = fields.adminContentImportSourceType || $('admin-content-import-source-type').value;
  4354. $('admin-content-import-version').value = fields.adminContentImportVersion || $('admin-content-import-version').value;
  4355. $('admin-content-import-manifest-url').value = fields.adminContentImportManifestURL || $('admin-content-import-manifest-url').value;
  4356. $('admin-content-import-asset-manifest-json').value = fields.adminContentImportAssetManifestJSON || $('admin-content-import-asset-manifest-json').value;
  4357. $('admin-pipeline-source-id').value = fields.adminPipelineSourceId || $('admin-pipeline-source-id').value;
  4358. $('admin-pipeline-build-id').value = fields.adminPipelineBuildId || $('admin-pipeline-build-id').value;
  4359. $('admin-pipeline-release-id').value = fields.adminPipelineReleaseId || $('admin-pipeline-release-id').value;
  4360. $('admin-release-runtime-binding-id').value = fields.adminReleaseRuntimeBindingId || $('admin-release-runtime-binding-id').value;
  4361. $('admin-release-presentation-id').value = fields.adminReleasePresentationId || $('admin-release-presentation-id').value;
  4362. $('admin-release-content-bundle-id').value = fields.adminReleaseContentBundleId || $('admin-release-content-bundle-id').value;
  4363. $('admin-rollback-release-id').value = fields.adminRollbackReleaseId || $('admin-rollback-release-id').value;
  4364. }
  4365. function parseIntOrNull(value) {
  4366. if (value === '' || value === null || value === undefined) {
  4367. return null;
  4368. }
  4369. const parsed = parseInt(value, 10);
  4370. return Number.isNaN(parsed) ? null : parsed;
  4371. }
  4372. function parseFloatOrNull(value) {
  4373. if (value === '' || value === null || value === undefined) {
  4374. return null;
  4375. }
  4376. const parsed = parseFloat(value);
  4377. return Number.isNaN(parsed) ? null : parsed;
  4378. }
  4379. function trimmedOrUndefined(value) {
  4380. if (value === null || value === undefined) {
  4381. return undefined;
  4382. }
  4383. const trimmed = String(value).trim();
  4384. return trimmed === '' ? undefined : trimmed;
  4385. }
  4386. function parseJSONObjectOrUndefined(value, label) {
  4387. const trimmed = trimmedOrUndefined(value);
  4388. if (trimmed === undefined) {
  4389. return undefined;
  4390. }
  4391. try {
  4392. return JSON.parse(trimmed);
  4393. } catch (_) {
  4394. throw new Error(label + ' must be valid JSON');
  4395. }
  4396. }
  4397. function parseJSONArray(value, label) {
  4398. const trimmed = trimmedOrUndefined(value);
  4399. if (trimmed === undefined) {
  4400. return [];
  4401. }
  4402. try {
  4403. const parsed = JSON.parse(trimmed);
  4404. if (!Array.isArray(parsed)) {
  4405. throw new Error(label + ' must be a JSON array');
  4406. }
  4407. return parsed;
  4408. } catch (error) {
  4409. if (error && error.message && error.message.indexOf(label + ' must be a JSON array') >= 0) {
  4410. throw error;
  4411. }
  4412. throw new Error(label + ' must be valid JSON array');
  4413. }
  4414. }
  4415. function buildFinishSummary() {
  4416. const summary = {
  4417. finalDurationSec: parseIntOrNull($('finish-duration').value),
  4418. finalScore: parseIntOrNull($('finish-score').value),
  4419. completedControls: parseIntOrNull($('finish-controls-done').value),
  4420. totalControls: parseIntOrNull($('finish-controls-total').value),
  4421. distanceMeters: parseFloatOrNull($('finish-distance').value),
  4422. averageSpeedKmh: parseFloatOrNull($('finish-speed').value),
  4423. maxHeartRateBpm: parseIntOrNull($('finish-heart-rate').value)
  4424. };
  4425. Object.keys(summary).forEach(function(key) {
  4426. if (summary[key] === null) {
  4427. delete summary[key];
  4428. }
  4429. });
  4430. return summary;
  4431. }
  4432. function buildCurl(method, url, body, headers) {
  4433. let curl = 'curl -X ' + method + ' "' + window.location.origin + url + '"';
  4434. Object.entries(headers || {}).forEach(function(entry) {
  4435. curl += ' -H "' + entry[0] + ': ' + String(entry[1]).replace(/"/g, '\\"') + '"';
  4436. });
  4437. if (body !== undefined) {
  4438. curl += " --data-raw '" + JSON.stringify(body).replace(/'/g, "'\"'\"'") + "'";
  4439. }
  4440. return curl;
  4441. }
  4442. function getHistory() {
  4443. const raw = localStorage.getItem(HISTORY_KEY);
  4444. if (!raw) {
  4445. return [];
  4446. }
  4447. try {
  4448. const list = JSON.parse(raw);
  4449. return Array.isArray(list) ? list : [];
  4450. } catch (_) {
  4451. return [];
  4452. }
  4453. }
  4454. function pushHistory(item) {
  4455. const next = [item].concat(getHistory()).slice(0, 12);
  4456. localStorage.setItem(HISTORY_KEY, JSON.stringify(next));
  4457. renderHistory();
  4458. }
  4459. function renderHistory() {
  4460. const history = getHistory();
  4461. historyEl.innerHTML = '';
  4462. if (!history.length) {
  4463. historyEl.innerHTML = '<div class="muted-note">No requests yet.</div>';
  4464. scheduleMasonryLayout();
  4465. return;
  4466. }
  4467. history.forEach(function(item) {
  4468. const node = document.createElement('div');
  4469. node.className = 'history-item';
  4470. node.innerHTML =
  4471. '<strong>' + item.title + '</strong><br>' +
  4472. item.time + '<br>' +
  4473. 'status=' + item.status + '<br>' +
  4474. 'url=' + item.url;
  4475. historyEl.appendChild(node);
  4476. });
  4477. scheduleMasonryLayout();
  4478. }
  4479. function applyAPIFilter() {
  4480. const keyword = $('api-filter').value.trim().toLowerCase();
  4481. document.querySelectorAll('.api-item').forEach(function(node) {
  4482. const haystack = String(node.dataset.api || '').toLowerCase();
  4483. if (!keyword || haystack.indexOf(keyword) >= 0) {
  4484. node.classList.remove('hidden');
  4485. } else {
  4486. node.classList.add('hidden');
  4487. }
  4488. });
  4489. syncAPICounts();
  4490. }
  4491. function categorizeApiPath(path) {
  4492. if (path === '/healthz') {
  4493. return '基础';
  4494. }
  4495. if (path.indexOf('/public/') === 0 || path.indexOf('/experience-maps') === 0) {
  4496. return '游客链';
  4497. }
  4498. if (path.indexOf('/dev/') === 0 || path === '/dev/workbench') {
  4499. return '调试链';
  4500. }
  4501. if (path.indexOf('/ops/') === 0 || path.indexOf('/admin/') === 0) {
  4502. return '运维链';
  4503. }
  4504. if (
  4505. path.indexOf('/auth/') === 0 ||
  4506. path.indexOf('/entry/') === 0 ||
  4507. path === '/home' ||
  4508. path === '/cards' ||
  4509. path.indexOf('/events/') === 0 ||
  4510. path.indexOf('/config-sources/') === 0 ||
  4511. path.indexOf('/config-builds/') === 0 ||
  4512. path.indexOf('/sessions/') === 0 ||
  4513. path.indexOf('/me') === 0
  4514. ) {
  4515. return '玩家链';
  4516. }
  4517. return '其他';
  4518. }
  4519. function syncAPICounts() {
  4520. const items = Array.from(document.querySelectorAll('.api-item'));
  4521. const total = items.length;
  4522. const visibleItems = items.filter(function(node) {
  4523. return !node.classList.contains('hidden');
  4524. });
  4525. $('nav-api-count').textContent = '(' + total + ')';
  4526. $('api-total-count').textContent = '(' + total + ')';
  4527. $('api-filter-meta').textContent = '当前 ' + visibleItems.length + ' / 总计 ' + total + ' 个接口,支持按关键词筛选。';
  4528. const order = ['基础', '玩家链', '游客链', '调试链', '运维链', '其他'];
  4529. const counts = {};
  4530. order.forEach(function(key) { counts[key] = 0; });
  4531. items.forEach(function(node) {
  4532. const pathEl = node.querySelector('.api-path');
  4533. const path = pathEl ? pathEl.textContent.trim() : '';
  4534. const category = categorizeApiPath(path);
  4535. counts[category] = (counts[category] || 0) + 1;
  4536. });
  4537. $('api-summary').innerHTML = order.map(function(key) {
  4538. const value = counts[key] || 0;
  4539. return '<span class=\"api-chip\">' + key + ' ' + value + '</span>';
  4540. }).join('');
  4541. }
  4542. function getSavedScenarios() {
  4543. const raw = localStorage.getItem(SCENARIO_KEY);
  4544. if (!raw) {
  4545. return [];
  4546. }
  4547. try {
  4548. const list = JSON.parse(raw);
  4549. return Array.isArray(list) ? list : [];
  4550. } catch (_) {
  4551. return [];
  4552. }
  4553. }
  4554. function setSavedScenarios(items) {
  4555. localStorage.setItem(SCENARIO_KEY, JSON.stringify(items));
  4556. renderScenarioOptions();
  4557. }
  4558. function allScenarios() {
  4559. return builtInScenarios.concat(getSavedScenarios());
  4560. }
  4561. function renderScenarioOptions() {
  4562. const select = $('scenario-select');
  4563. const scenarios = allScenarios();
  4564. select.innerHTML = '';
  4565. if (!scenarios.length) {
  4566. select.innerHTML = '<option value="">No scenarios</option>';
  4567. return;
  4568. }
  4569. scenarios.forEach(function(item) {
  4570. const option = document.createElement('option');
  4571. option.value = item.id;
  4572. option.textContent = item.name + (item.builtin ? ' [preset]' : '');
  4573. select.appendChild(option);
  4574. });
  4575. }
  4576. function findScenario(id) {
  4577. return allScenarios().find(function(item) {
  4578. return item.id === id;
  4579. }) || null;
  4580. }
  4581. function saveCurrentScenario() {
  4582. const name = $('scenario-name').value.trim();
  4583. if (!name) {
  4584. setStatus('error: scenario name required', true);
  4585. return;
  4586. }
  4587. const saved = getSavedScenarios();
  4588. const scenario = {
  4589. id: 'custom-' + Date.now(),
  4590. builtin: false,
  4591. name: name,
  4592. fields: collectFields()
  4593. };
  4594. saved.unshift(scenario);
  4595. setSavedScenarios(saved.slice(0, 20));
  4596. $('scenario-select').value = scenario.id;
  4597. $('scenario-json').value = JSON.stringify(scenario, null, 2);
  4598. setStatus('ok: scenario saved');
  4599. }
  4600. function loadSelectedScenario() {
  4601. const scenario = findScenario($('scenario-select').value);
  4602. if (!scenario) {
  4603. setStatus('error: scenario not found', true);
  4604. return;
  4605. }
  4606. applyFields(scenario.fields || {});
  4607. $('scenario-name').value = scenario.name || '';
  4608. $('scenario-json').value = JSON.stringify(scenario, null, 2);
  4609. persistState();
  4610. setStatus('ok: scenario loaded');
  4611. }
  4612. function deleteSelectedScenario() {
  4613. const id = $('scenario-select').value;
  4614. const scenario = findScenario(id);
  4615. if (!scenario || scenario.builtin) {
  4616. setStatus('error: builtin scenario cannot be deleted', true);
  4617. return;
  4618. }
  4619. const next = getSavedScenarios().filter(function(item) {
  4620. return item.id !== id;
  4621. });
  4622. setSavedScenarios(next);
  4623. $('scenario-json').value = '';
  4624. setStatus('ok: scenario deleted');
  4625. }
  4626. function exportSelectedScenario() {
  4627. const scenario = findScenario($('scenario-select').value);
  4628. if (!scenario) {
  4629. setStatus('error: scenario not found', true);
  4630. return;
  4631. }
  4632. $('scenario-json').value = JSON.stringify(scenario, null, 2);
  4633. setStatus('ok: scenario exported');
  4634. }
  4635. function importScenarioFromJSON() {
  4636. const raw = $('scenario-json').value.trim();
  4637. if (!raw) {
  4638. setStatus('error: scenario json is empty', true);
  4639. return;
  4640. }
  4641. try {
  4642. const scenario = JSON.parse(raw);
  4643. if (!scenario.name || !scenario.fields) {
  4644. throw new Error('scenario must include name and fields');
  4645. }
  4646. const saved = getSavedScenarios();
  4647. saved.unshift({
  4648. id: 'custom-' + Date.now(),
  4649. builtin: false,
  4650. name: String(scenario.name),
  4651. fields: scenario.fields
  4652. });
  4653. setSavedScenarios(saved.slice(0, 20));
  4654. setStatus('ok: scenario imported');
  4655. } catch (err) {
  4656. setStatus('error: invalid scenario json', true);
  4657. }
  4658. }
  4659. async function request(method, url, body, needAuth = false) {
  4660. const headers = {};
  4661. if (body !== undefined) {
  4662. headers['Content-Type'] = 'application/json';
  4663. }
  4664. if (needAuth) {
  4665. headers['Authorization'] = 'Bearer ' + state.accessToken;
  4666. }
  4667. state.lastCurl = buildCurl(method, url, body, headers);
  4668. syncState();
  4669. const resp = await fetch(url, {
  4670. method,
  4671. headers,
  4672. body: body === undefined ? undefined : JSON.stringify(body)
  4673. });
  4674. const data = await resp.json().catch(() => ({}));
  4675. if (!resp.ok) {
  4676. throw { status: resp.status, body: data, url: url, method: method };
  4677. }
  4678. return data;
  4679. }
  4680. async function run(title, fn) {
  4681. const triggerButton = getActiveTriggerButton();
  4682. setButtonRunning(triggerButton, true, '执行中');
  4683. setStatus('running: ' + title, false, true);
  4684. if (FLOW_STEP_PLANS[title]) {
  4685. resetProgress();
  4686. updateFlowProgress(title, null, '长流程已开始,等待第一步...');
  4687. }
  4688. writeLog(title + '.start', {
  4689. startedAt: new Date().toLocaleString(),
  4690. note: 'request accepted and running'
  4691. });
  4692. try {
  4693. const result = await fn();
  4694. setStatus('ok: ' + title);
  4695. writeLog(title, result);
  4696. pushHistory({
  4697. title: title,
  4698. time: new Date().toLocaleString(),
  4699. status: 'ok',
  4700. url: state.lastCurl
  4701. });
  4702. syncState();
  4703. } catch (err) {
  4704. setStatus('error: ' + title + ' -> ' + (err && err.message ? err.message : 'unknown error'), true);
  4705. if (FLOW_STEP_PLANS[title]) {
  4706. completeFlowProgress(title, false, '失败:' + (err && err.message ? err.message : 'unknown error'));
  4707. }
  4708. writeLog(title, {
  4709. error: err,
  4710. lastCurl: state.lastCurl || null
  4711. });
  4712. pushHistory({
  4713. title: title,
  4714. time: new Date().toLocaleString(),
  4715. status: 'error',
  4716. url: state.lastCurl
  4717. });
  4718. } finally {
  4719. if (FLOW_STEP_PLANS[title] && statusEl.className.indexOf('error') < 0) {
  4720. completeFlowProgress(title, true, '长流程已完成。');
  4721. }
  4722. setButtonRunning(triggerButton, false);
  4723. scheduleMasonryLayout();
  4724. }
  4725. }
  4726. $('btn-clear-state').onclick = () => {
  4727. state.accessToken = '';
  4728. state.refreshToken = '';
  4729. state.sourceId = '';
  4730. state.buildId = '';
  4731. state.releaseId = '';
  4732. state.sessionId = '';
  4733. state.sessionToken = '';
  4734. state.lastCurl = '';
  4735. syncState();
  4736. resetProgress();
  4737. writeLog('clear-state', { ok: true });
  4738. setStatus('ready');
  4739. };
  4740. modeButtons.forEach(function(button) {
  4741. button.onclick = () => {
  4742. localStorage.setItem(MODE_KEY, button.dataset.modeBtn);
  4743. syncWorkbenchMode();
  4744. writeLog('workbench-mode', { mode: button.dataset.modeBtn });
  4745. setStatus('ok: mode -> ' + button.dataset.modeBtn);
  4746. };
  4747. });
  4748. navLinks.forEach(function(link) {
  4749. link.onclick = function(event) {
  4750. event.preventDefault();
  4751. const targetId = link.dataset.navTarget;
  4752. const targetMode = link.dataset.navMode;
  4753. if (targetMode) {
  4754. localStorage.setItem(MODE_KEY, targetMode);
  4755. syncWorkbenchMode();
  4756. }
  4757. const targetEl = document.getElementById(targetId);
  4758. if (targetEl) {
  4759. targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
  4760. writeLog('workbench-nav', { target: targetId, mode: targetMode || getWorkbenchMode() });
  4761. setStatus('ok: nav -> ' + targetId);
  4762. }
  4763. };
  4764. });
  4765. $('btn-config-files').onclick = () => run('config/local-files', () =>
  4766. request('GET', '/dev/config/local-files')
  4767. );
  4768. $('btn-config-import').onclick = () => run('config/import-local', async () => {
  4769. const result = await request('POST', '/dev/events/' + encodeURIComponent($('config-event-id').value) + '/config-sources/import-local', {
  4770. fileName: $('local-config-file').value
  4771. });
  4772. state.sourceId = result.data.id;
  4773. return result;
  4774. });
  4775. $('btn-config-preview').onclick = () => run('config/build-preview', async () => {
  4776. const result = await request('POST', '/dev/config-builds/preview', {
  4777. sourceId: $('config-source-id').value
  4778. });
  4779. state.buildId = result.data.id;
  4780. return result;
  4781. });
  4782. $('btn-config-publish').onclick = () => run('config/publish-build', async () => {
  4783. const result = await request('POST', '/dev/config-builds/publish', {
  4784. buildId: $('config-build-id').value,
  4785. runtimeBindingId: trimmedOrUndefined($('config-runtime-binding-id').value),
  4786. presentationId: trimmedOrUndefined($('config-presentation-id').value),
  4787. contentBundleId: trimmedOrUndefined($('config-content-bundle-id').value)
  4788. });
  4789. state.releaseId = result.data.release.releaseId;
  4790. $('event-release-id').value = result.data.release.releaseId;
  4791. if (result.data.runtime && result.data.runtime.runtimeBindingId) {
  4792. $('config-runtime-binding-id').value = result.data.runtime.runtimeBindingId;
  4793. }
  4794. if (result.data.presentation && result.data.presentation.presentationId) {
  4795. $('config-presentation-id').value = result.data.presentation.presentationId;
  4796. $('admin-release-presentation-id').value = result.data.presentation.presentationId;
  4797. }
  4798. if (result.data.contentBundle && result.data.contentBundle.contentBundleId) {
  4799. $('config-content-bundle-id').value = result.data.contentBundle.contentBundleId;
  4800. $('admin-release-content-bundle-id').value = result.data.contentBundle.contentBundleId;
  4801. }
  4802. return result;
  4803. });
  4804. $('btn-config-source').onclick = () => run('config/get-source', () =>
  4805. request('GET', '/config-sources/' + encodeURIComponent($('config-source-id').value), undefined, true)
  4806. );
  4807. $('btn-config-build').onclick = () => run('config/get-build', () =>
  4808. request('GET', '/config-builds/' + encodeURIComponent($('config-build-id').value), undefined, true)
  4809. );
  4810. $('btn-send-sms').onclick = () => run('auth/sms/send', async () => {
  4811. const result = await request('POST', '/auth/sms/send', {
  4812. countryCode: $('sms-country').value,
  4813. mobile: $('sms-mobile').value,
  4814. clientType: $('sms-client-type').value,
  4815. deviceKey: $('sms-device').value,
  4816. scene: $('sms-scene').value
  4817. });
  4818. if (result.data && result.data.devCode) {
  4819. $('sms-code').value = result.data.devCode;
  4820. }
  4821. return result;
  4822. });
  4823. $('btn-login-sms').onclick = () => run('auth/login/sms', async () => {
  4824. const result = await request('POST', '/auth/login/sms', {
  4825. countryCode: $('sms-country').value,
  4826. mobile: $('sms-mobile').value,
  4827. code: $('sms-code').value,
  4828. clientType: $('sms-client-type').value,
  4829. deviceKey: $('sms-device').value
  4830. });
  4831. state.accessToken = result.data.tokens.accessToken;
  4832. state.refreshToken = result.data.tokens.refreshToken;
  4833. return result;
  4834. });
  4835. $('btn-bind-mobile').onclick = () => run('auth/bind/mobile', async () => {
  4836. const result = await request('POST', '/auth/bind/mobile', {
  4837. countryCode: $('sms-country').value,
  4838. mobile: $('sms-mobile').value,
  4839. code: $('sms-code').value,
  4840. clientType: $('sms-client-type').value,
  4841. deviceKey: $('sms-device').value
  4842. }, true);
  4843. state.accessToken = result.data.tokens.accessToken;
  4844. state.refreshToken = result.data.tokens.refreshToken;
  4845. return result;
  4846. });
  4847. $('btn-login-wechat').onclick = () => run('auth/login/wechat-mini', async () => {
  4848. const result = await request('POST', '/auth/login/wechat-mini', {
  4849. code: $('wechat-code').value,
  4850. clientType: 'wechat',
  4851. deviceKey: $('wechat-device').value
  4852. });
  4853. state.accessToken = result.data.tokens.accessToken;
  4854. state.refreshToken = result.data.tokens.refreshToken;
  4855. return result;
  4856. });
  4857. $('btn-resolve-entry').onclick = () => run('entry/resolve', () =>
  4858. request('GET', '/entry/resolve?channelCode=' + encodeURIComponent($('entry-channel-code').value) + '&channelType=' + encodeURIComponent($('entry-channel-type').value))
  4859. );
  4860. $('btn-home').onclick = () => run('home', () =>
  4861. request('GET', '/home?channelCode=' + encodeURIComponent($('entry-channel-code').value) + '&channelType=' + encodeURIComponent($('entry-channel-type').value))
  4862. );
  4863. $('btn-entry-home').onclick = () => run('me/entry-home', () =>
  4864. request('GET', '/me/entry-home?channelCode=' + encodeURIComponent($('entry-channel-code').value) + '&channelType=' + encodeURIComponent($('entry-channel-type').value), undefined, true)
  4865. );
  4866. $('btn-event-detail').onclick = () => run('event-detail', async () => {
  4867. const result = await request('GET', '/events/' + encodeURIComponent($('event-id').value));
  4868. if (result && result.data) {
  4869. setCurrentPreviewStatusFromPayload(result.data);
  4870. const configSummary = await resolveEventManifestSummary(result.data);
  4871. setLaunchConfigSummary(configSummary);
  4872. if (configSummary.gameMode) {
  4873. currentFlowStatus.gameMode = configSummary.gameMode;
  4874. }
  4875. if (configSummary.playfieldKind) {
  4876. currentFlowStatus.playfieldKind = configSummary.playfieldKind;
  4877. }
  4878. renderCurrentFlowStatus();
  4879. }
  4880. return result;
  4881. });
  4882. $('btn-event-play').onclick = () => run('event-play', async () => {
  4883. const result = await request('GET', '/events/' + encodeURIComponent($('event-id').value) + '/play', undefined, true);
  4884. setCurrentFlowStatusFromPlayResponse(result);
  4885. if (result && result.data) {
  4886. setCurrentPreviewStatusFromPayload(result.data);
  4887. const configSummary = await resolveEventManifestSummary(result.data);
  4888. setLaunchConfigSummary(configSummary);
  4889. if (configSummary.gameMode) {
  4890. currentFlowStatus.gameMode = configSummary.gameMode;
  4891. }
  4892. if (configSummary.playfieldKind) {
  4893. currentFlowStatus.playfieldKind = configSummary.playfieldKind;
  4894. }
  4895. renderCurrentFlowStatus();
  4896. writeLog('event-play.summary', configSummary);
  4897. }
  4898. return result;
  4899. });
  4900. $('btn-launch').onclick = () => run('event-launch', async () => {
  4901. const result = await request('POST', '/events/' + encodeURIComponent($('event-id').value) + '/launch', {
  4902. releaseId: $('event-release-id').value,
  4903. variantId: trimmedOrUndefined($('event-variant-id').value),
  4904. clientType: $('sms-client-type').value,
  4905. deviceKey: $('event-device').value
  4906. }, true);
  4907. state.sessionId = result.data.launch.business.sessionId;
  4908. state.sessionToken = result.data.launch.business.sessionToken;
  4909. syncState();
  4910. const configSummary = await resolveLaunchConfigSummary(result.data);
  4911. setLaunchConfigSummary(configSummary);
  4912. setCurrentFlowStatusFromLaunch(configSummary, result.data);
  4913. writeLog('event-launch.summary', configSummary);
  4914. return result;
  4915. });
  4916. $('btn-session-detail').onclick = () => run('session-detail', () =>
  4917. request('GET', '/sessions/' + encodeURIComponent($('session-id').value), undefined, true)
  4918. );
  4919. $('btn-session-start').onclick = () => run('session-start', () =>
  4920. request('POST', '/sessions/' + encodeURIComponent($('session-id').value) + '/start', {
  4921. sessionToken: $('session-token').value
  4922. })
  4923. );
  4924. $('btn-session-finish').onclick = () => run('session-finish', () =>
  4925. request('POST', '/sessions/' + encodeURIComponent($('session-id').value) + '/finish', {
  4926. sessionToken: $('session-token').value,
  4927. status: $('finish-status').value,
  4928. summary: buildFinishSummary()
  4929. })
  4930. );
  4931. $('btn-my-sessions').onclick = () => run('me/sessions', () =>
  4932. request('GET', '/me/sessions?limit=10', undefined, true)
  4933. );
  4934. $('btn-session-result').onclick = () => run('session-result', () =>
  4935. request('GET', '/sessions/' + encodeURIComponent($('session-id').value) + '/result', undefined, true)
  4936. );
  4937. $('btn-my-results').onclick = () => run('me/results', () =>
  4938. request('GET', '/me/results?limit=10', undefined, true)
  4939. );
  4940. $('btn-me').onclick = () => run('me', () =>
  4941. request('GET', '/me', undefined, true)
  4942. );
  4943. $('btn-profile').onclick = () => run('me/profile', () =>
  4944. request('GET', '/me/profile', undefined, true)
  4945. );
  4946. $('btn-prod-places-list').onclick = () => run('admin/places/list', async () => {
  4947. const result = await request('GET', '/admin/places?limit=20', undefined, true);
  4948. const first = result.data && result.data[0];
  4949. if (first) {
  4950. $('prod-place-id').value = first.id || $('prod-place-id').value;
  4951. }
  4952. persistState();
  4953. return result;
  4954. });
  4955. $('btn-prod-place-create').onclick = () => run('admin/places/create', async () => {
  4956. const result = await request('POST', '/admin/places', {
  4957. code: $('prod-place-code').value,
  4958. name: $('prod-place-name').value,
  4959. region: trimmedOrUndefined($('prod-place-region').value),
  4960. coverUrl: trimmedOrUndefined($('prod-place-cover-url').value),
  4961. status: $('prod-place-status').value
  4962. }, true);
  4963. $('prod-place-id').value = result.data.id || $('prod-place-id').value;
  4964. persistState();
  4965. return result;
  4966. });
  4967. $('btn-prod-place-detail').onclick = () => run('admin/places/detail', async () => {
  4968. const result = await request('GET', '/admin/places/' + encodeURIComponent($('prod-place-id').value), undefined, true);
  4969. if (result.data && result.data.place) {
  4970. $('prod-place-id').value = result.data.place.id || $('prod-place-id').value;
  4971. }
  4972. if (result.data && result.data.mapAssets && result.data.mapAssets[0]) {
  4973. $('prod-map-asset-id').value = result.data.mapAssets[0].id || $('prod-map-asset-id').value;
  4974. }
  4975. persistState();
  4976. return result;
  4977. });
  4978. $('btn-prod-map-asset-create').onclick = () => run('admin/map-assets/create', async () => {
  4979. const result = await request('POST', '/admin/places/' + encodeURIComponent($('prod-place-id').value) + '/map-assets', {
  4980. code: $('prod-map-asset-code').value,
  4981. name: $('prod-map-asset-name').value,
  4982. mapType: $('prod-map-asset-type').value,
  4983. legacyMapId: trimmedOrUndefined($('prod-map-asset-legacy-map-id').value),
  4984. status: $('prod-map-asset-status').value
  4985. }, true);
  4986. $('prod-map-asset-id').value = result.data.id || $('prod-map-asset-id').value;
  4987. persistState();
  4988. return result;
  4989. });
  4990. $('btn-prod-map-asset-detail').onclick = () => run('admin/map-assets/detail', async () => {
  4991. const result = await request('GET', '/admin/map-assets/' + encodeURIComponent($('prod-map-asset-id').value), undefined, true);
  4992. if (result.data && result.data.mapAsset) {
  4993. $('prod-map-asset-id').value = result.data.mapAsset.id || $('prod-map-asset-id').value;
  4994. }
  4995. if (result.data && result.data.mapAsset && result.data.mapAsset.currentTileRelease) {
  4996. $('prod-tile-release-id').value = result.data.mapAsset.currentTileRelease.id || $('prod-tile-release-id').value;
  4997. } else if (result.data && result.data.tileReleases && result.data.tileReleases[0]) {
  4998. $('prod-tile-release-id').value = result.data.tileReleases[0].id || $('prod-tile-release-id').value;
  4999. }
  5000. if (result.data && result.data.courseSets && result.data.courseSets[0]) {
  5001. $('prod-course-set-id').value = result.data.courseSets[0].id || $('prod-course-set-id').value;
  5002. }
  5003. persistState();
  5004. return result;
  5005. });
  5006. $('btn-prod-tile-create').onclick = () => run('admin/tile-releases/create', async () => {
  5007. const result = await request('POST', '/admin/map-assets/' + encodeURIComponent($('prod-map-asset-id').value) + '/tile-releases', {
  5008. legacyVersionId: trimmedOrUndefined($('prod-tile-legacy-version-id').value),
  5009. versionCode: $('prod-tile-version-code').value,
  5010. status: $('prod-tile-status').value,
  5011. tileBaseUrl: $('prod-tile-base-url').value,
  5012. metaUrl: $('prod-tile-meta-url').value,
  5013. setAsCurrent: true
  5014. }, true);
  5015. $('prod-tile-release-id').value = result.data.id || $('prod-tile-release-id').value;
  5016. persistState();
  5017. return result;
  5018. });
  5019. const btnProdTileImport = $('btn-prod-tile-import');
  5020. if (btnProdTileImport) {
  5021. btnProdTileImport.onclick = () => run('admin/ops/tile-releases/import', async () => {
  5022. const result = await request('POST', '/admin/ops/tile-releases/import', {
  5023. placeCode: $('prod-place-code').value,
  5024. placeName: $('prod-place-name').value,
  5025. placeRegion: trimmedOrUndefined($('prod-place-region').value),
  5026. placeCoverUrl: trimmedOrUndefined($('prod-place-cover-url').value),
  5027. mapAssetCode: $('prod-map-asset-code').value,
  5028. mapAssetName: $('prod-map-asset-name').value,
  5029. mapType: $('prod-map-asset-type').value,
  5030. versionCode: $('prod-tile-version-code').value,
  5031. status: $('prod-tile-status').value,
  5032. tileBaseUrl: $('prod-tile-base-url').value,
  5033. metaUrl: $('prod-tile-meta-url').value,
  5034. setAsCurrent: true
  5035. }, true);
  5036. if (result.data) {
  5037. $('prod-place-id').value = result.data.place && result.data.place.id ? result.data.place.id : $('prod-place-id').value;
  5038. $('prod-map-asset-id').value = result.data.mapAsset && result.data.mapAsset.id ? result.data.mapAsset.id : $('prod-map-asset-id').value;
  5039. $('prod-tile-release-id').value = result.data.tileRelease && result.data.tileRelease.id ? result.data.tileRelease.id : $('prod-tile-release-id').value;
  5040. }
  5041. persistState();
  5042. return result;
  5043. });
  5044. }
  5045. $('btn-prod-course-sources-list').onclick = () => run('admin/course-sources/list', async () => {
  5046. const result = await request('GET', '/admin/course-sources?limit=20', undefined, true);
  5047. const first = result.data && result.data[0];
  5048. if (first) {
  5049. $('prod-course-source-id').value = first.id || $('prod-course-source-id').value;
  5050. }
  5051. persistState();
  5052. return result;
  5053. });
  5054. $('btn-prod-course-source-create').onclick = () => run('admin/course-sources/create', async () => {
  5055. const result = await request('POST', '/admin/course-sources', {
  5056. legacyPlayfieldId: trimmedOrUndefined($('prod-course-source-legacy-playfield-id').value),
  5057. legacyVersionId: trimmedOrUndefined($('prod-course-source-legacy-version-id').value),
  5058. sourceType: $('prod-course-source-type').value,
  5059. fileUrl: $('prod-course-source-file-url').value,
  5060. importStatus: $('prod-course-source-status').value
  5061. }, true);
  5062. $('prod-course-source-id').value = result.data.id || $('prod-course-source-id').value;
  5063. persistState();
  5064. return result;
  5065. });
  5066. $('btn-prod-course-source-detail').onclick = () => run('admin/course-sources/detail', async () => {
  5067. const result = await request('GET', '/admin/course-sources/' + encodeURIComponent($('prod-course-source-id').value), undefined, true);
  5068. if (result.data) {
  5069. $('prod-course-source-id').value = result.data.id || $('prod-course-source-id').value;
  5070. }
  5071. persistState();
  5072. return result;
  5073. });
  5074. $('btn-prod-course-set-create').onclick = () => run('admin/course-sets/create', async () => {
  5075. const result = await request('POST', '/admin/map-assets/' + encodeURIComponent($('prod-map-asset-id').value) + '/course-sets', {
  5076. code: $('prod-course-set-code').value,
  5077. mode: $('prod-course-mode').value,
  5078. name: $('prod-course-set-name').value,
  5079. status: $('prod-course-set-status').value
  5080. }, true);
  5081. $('prod-course-set-id').value = result.data.id || $('prod-course-set-id').value;
  5082. persistState();
  5083. return result;
  5084. });
  5085. $('btn-prod-course-set-detail').onclick = () => run('admin/course-sets/detail', async () => {
  5086. const result = await request('GET', '/admin/course-sets/' + encodeURIComponent($('prod-course-set-id').value), undefined, true);
  5087. if (result.data && result.data.courseSet) {
  5088. $('prod-course-set-id').value = result.data.courseSet.id || $('prod-course-set-id').value;
  5089. if (result.data.courseSet.currentVariant) {
  5090. $('prod-course-variant-id').value = result.data.courseSet.currentVariant.id || $('prod-course-variant-id').value;
  5091. }
  5092. }
  5093. if (result.data && result.data.variants && result.data.variants[0]) {
  5094. $('prod-course-variant-id').value = result.data.variants[0].id || $('prod-course-variant-id').value;
  5095. }
  5096. persistState();
  5097. return result;
  5098. });
  5099. $('btn-prod-course-variant-create').onclick = () => run('admin/course-variants/create', async () => {
  5100. const result = await request('POST', '/admin/course-sets/' + encodeURIComponent($('prod-course-set-id').value) + '/variants', {
  5101. sourceId: trimmedOrUndefined($('prod-course-source-id').value),
  5102. name: $('prod-course-variant-name').value,
  5103. routeCode: trimmedOrUndefined($('prod-course-variant-route-code').value),
  5104. mode: $('prod-course-mode').value,
  5105. controlCount: parseIntOrNull($('prod-course-variant-control-count').value),
  5106. status: $('prod-course-variant-status').value,
  5107. isDefault: true
  5108. }, true);
  5109. $('prod-course-variant-id').value = result.data.id || $('prod-course-variant-id').value;
  5110. persistState();
  5111. return result;
  5112. });
  5113. const btnProdCourseBatchImport = $('btn-prod-course-batch-import');
  5114. if (btnProdCourseBatchImport) {
  5115. btnProdCourseBatchImport.onclick = () => run('admin/ops/course-sets/import-kml-batch', async () => {
  5116. const routes = parseJSONArray($('prod-course-routes-json').value, 'KML Batch JSON');
  5117. const result = await request('POST', '/admin/ops/course-sets/import-kml-batch', {
  5118. placeCode: $('prod-place-code').value,
  5119. placeName: $('prod-place-name').value,
  5120. mapAssetCode: $('prod-map-asset-code').value,
  5121. mapAssetName: $('prod-map-asset-name').value,
  5122. mapType: $('prod-map-asset-type').value,
  5123. courseSetCode: $('prod-course-set-code').value,
  5124. courseSetName: $('prod-course-set-name').value,
  5125. mode: $('prod-course-mode').value,
  5126. status: $('prod-course-set-status').value,
  5127. defaultRouteCode: trimmedOrUndefined($('prod-course-default-route-code').value),
  5128. routes: routes
  5129. }, true);
  5130. if (result.data) {
  5131. $('prod-place-id').value = result.data.place && result.data.place.id ? result.data.place.id : $('prod-place-id').value;
  5132. $('prod-map-asset-id').value = result.data.mapAsset && result.data.mapAsset.id ? result.data.mapAsset.id : $('prod-map-asset-id').value;
  5133. $('prod-course-set-id').value = result.data.courseSet && result.data.courseSet.id ? result.data.courseSet.id : $('prod-course-set-id').value;
  5134. if (result.data.courseSet && result.data.courseSet.currentVariant) {
  5135. $('prod-course-variant-id').value = result.data.courseSet.currentVariant.id || $('prod-course-variant-id').value;
  5136. } else if (result.data.variants && result.data.variants[0]) {
  5137. $('prod-course-variant-id').value = result.data.variants[0].id || $('prod-course-variant-id').value;
  5138. }
  5139. }
  5140. persistState();
  5141. return result;
  5142. });
  5143. }
  5144. $('btn-prod-runtime-bindings-list').onclick = () => run('admin/runtime-bindings/list', async () => {
  5145. const result = await request('GET', '/admin/runtime-bindings?limit=20', undefined, true);
  5146. const first = result.data && result.data[0];
  5147. if (first) {
  5148. $('prod-runtime-binding-id').value = first.id || $('prod-runtime-binding-id').value;
  5149. }
  5150. persistState();
  5151. return result;
  5152. });
  5153. $('btn-prod-runtime-binding-create').onclick = () => run('admin/runtime-bindings/create', async () => {
  5154. const result = await request('POST', '/admin/runtime-bindings', {
  5155. eventId: $('prod-runtime-event-id').value,
  5156. placeId: $('prod-place-id').value,
  5157. mapAssetId: $('prod-map-asset-id').value,
  5158. tileReleaseId: $('prod-tile-release-id').value,
  5159. courseSetId: $('prod-course-set-id').value,
  5160. courseVariantId: $('prod-course-variant-id').value,
  5161. status: $('prod-runtime-binding-status').value,
  5162. notes: trimmedOrUndefined($('prod-runtime-notes').value)
  5163. }, true);
  5164. $('prod-runtime-binding-id').value = result.data.id || $('prod-runtime-binding-id').value;
  5165. persistState();
  5166. return result;
  5167. });
  5168. $('btn-prod-runtime-binding-detail').onclick = () => run('admin/runtime-bindings/detail', async () => {
  5169. const result = await request('GET', '/admin/runtime-bindings/' + encodeURIComponent($('prod-runtime-binding-id').value), undefined, true);
  5170. if (result.data) {
  5171. $('prod-runtime-binding-id').value = result.data.id || $('prod-runtime-binding-id').value;
  5172. }
  5173. persistState();
  5174. return result;
  5175. });
  5176. $('btn-admin-maps-list').onclick = () => run('admin/maps/list', async () => {
  5177. const result = await request('GET', '/admin/maps?limit=20', undefined, true);
  5178. const first = result.data && result.data[0];
  5179. if (first) {
  5180. $('admin-map-id').value = first.id || $('admin-map-id').value;
  5181. $('admin-map-version-id').value = first.currentVersionId || $('admin-map-version-id').value;
  5182. }
  5183. persistState();
  5184. return result;
  5185. });
  5186. $('btn-admin-map-create').onclick = () => run('admin/maps/create', async () => {
  5187. const result = await request('POST', '/admin/maps', {
  5188. code: $('admin-map-code').value,
  5189. name: $('admin-map-name').value,
  5190. status: $('admin-map-status').value
  5191. }, true);
  5192. $('admin-map-id').value = result.data.id || $('admin-map-id').value;
  5193. persistState();
  5194. return result;
  5195. });
  5196. $('btn-admin-map-version').onclick = () => run('admin/maps/version', async () => {
  5197. const result = await request('POST', '/admin/maps/' + encodeURIComponent($('admin-map-id').value) + '/versions', {
  5198. versionCode: $('admin-map-version-code').value,
  5199. status: $('admin-map-status').value,
  5200. mapmetaUrl: $('admin-mapmeta-url').value,
  5201. tilesRootUrl: $('admin-tiles-root-url').value,
  5202. publishedAssetRoot: trimmedOrUndefined($('admin-published-asset-root').value),
  5203. setAsCurrent: true
  5204. }, true);
  5205. $('admin-map-version-id').value = result.data.id || $('admin-map-version-id').value;
  5206. persistState();
  5207. return result;
  5208. });
  5209. $('btn-admin-map-detail').onclick = () => run('admin/maps/detail', async () => {
  5210. const result = await request('GET', '/admin/maps/' + encodeURIComponent($('admin-map-id').value), undefined, true);
  5211. if (result.data && result.data.map) {
  5212. $('admin-map-id').value = result.data.map.id || $('admin-map-id').value;
  5213. $('admin-map-version-id').value = result.data.map.currentVersionId || $('admin-map-version-id').value;
  5214. }
  5215. persistState();
  5216. return result;
  5217. });
  5218. $('btn-admin-playfields-list').onclick = () => run('admin/playfields/list', async () => {
  5219. const result = await request('GET', '/admin/playfields?limit=20', undefined, true);
  5220. const first = result.data && result.data[0];
  5221. if (first) {
  5222. $('admin-playfield-id').value = first.id || $('admin-playfield-id').value;
  5223. $('admin-playfield-version-id').value = first.currentVersionId || $('admin-playfield-version-id').value;
  5224. }
  5225. persistState();
  5226. return result;
  5227. });
  5228. $('btn-admin-playfield-create').onclick = () => run('admin/playfields/create', async () => {
  5229. const result = await request('POST', '/admin/playfields', {
  5230. code: $('admin-playfield-code').value,
  5231. name: $('admin-playfield-name').value,
  5232. kind: $('admin-playfield-kind').value,
  5233. status: $('admin-playfield-status').value
  5234. }, true);
  5235. $('admin-playfield-id').value = result.data.id || $('admin-playfield-id').value;
  5236. persistState();
  5237. return result;
  5238. });
  5239. $('btn-admin-playfield-version').onclick = () => run('admin/playfields/version', async () => {
  5240. const result = await request('POST', '/admin/playfields/' + encodeURIComponent($('admin-playfield-id').value) + '/versions', {
  5241. versionCode: $('admin-playfield-version-code').value,
  5242. status: $('admin-playfield-status').value,
  5243. sourceType: $('admin-playfield-source-type').value,
  5244. sourceUrl: $('admin-playfield-source-url').value,
  5245. publishedAssetRoot: trimmedOrUndefined($('admin-published-asset-root').value),
  5246. controlCount: parseIntOrNull($('admin-playfield-control-count').value),
  5247. setAsCurrent: true
  5248. }, true);
  5249. $('admin-playfield-version-id').value = result.data.id || $('admin-playfield-version-id').value;
  5250. persistState();
  5251. return result;
  5252. });
  5253. $('btn-admin-playfield-detail').onclick = () => run('admin/playfields/detail', async () => {
  5254. const result = await request('GET', '/admin/playfields/' + encodeURIComponent($('admin-playfield-id').value), undefined, true);
  5255. if (result.data && result.data.playfield) {
  5256. $('admin-playfield-id').value = result.data.playfield.id || $('admin-playfield-id').value;
  5257. $('admin-playfield-version-id').value = result.data.playfield.currentVersionId || $('admin-playfield-version-id').value;
  5258. }
  5259. persistState();
  5260. return result;
  5261. });
  5262. $('btn-admin-packs-list').onclick = () => run('admin/resource-packs/list', async () => {
  5263. const result = await request('GET', '/admin/resource-packs?limit=20', undefined, true);
  5264. const first = result.data && result.data[0];
  5265. if (first) {
  5266. $('admin-pack-id').value = first.id || $('admin-pack-id').value;
  5267. $('admin-pack-version-id').value = first.currentVersionId || $('admin-pack-version-id').value;
  5268. }
  5269. persistState();
  5270. return result;
  5271. });
  5272. $('btn-admin-pack-create').onclick = () => run('admin/resource-packs/create', async () => {
  5273. const result = await request('POST', '/admin/resource-packs', {
  5274. code: $('admin-pack-code').value,
  5275. name: $('admin-pack-name').value,
  5276. status: $('admin-pack-status').value
  5277. }, true);
  5278. $('admin-pack-id').value = result.data.id || $('admin-pack-id').value;
  5279. persistState();
  5280. return result;
  5281. });
  5282. $('btn-admin-pack-version').onclick = () => run('admin/resource-packs/version', async () => {
  5283. const result = await request('POST', '/admin/resource-packs/' + encodeURIComponent($('admin-pack-id').value) + '/versions', {
  5284. versionCode: $('admin-pack-version-code').value,
  5285. status: $('admin-pack-status').value,
  5286. contentEntryUrl: trimmedOrUndefined($('admin-pack-content-url').value),
  5287. audioRootUrl: trimmedOrUndefined($('admin-pack-audio-url').value),
  5288. themeProfileCode: trimmedOrUndefined($('admin-pack-theme-code').value),
  5289. publishedAssetRoot: trimmedOrUndefined($('admin-published-asset-root').value),
  5290. setAsCurrent: true
  5291. }, true);
  5292. $('admin-pack-version-id').value = result.data.id || $('admin-pack-version-id').value;
  5293. persistState();
  5294. return result;
  5295. });
  5296. $('btn-admin-pack-detail').onclick = () => run('admin/resource-packs/detail', async () => {
  5297. const result = await request('GET', '/admin/resource-packs/' + encodeURIComponent($('admin-pack-id').value), undefined, true);
  5298. if (result.data && result.data.resourcePack) {
  5299. $('admin-pack-id').value = result.data.resourcePack.id || $('admin-pack-id').value;
  5300. $('admin-pack-version-id').value = result.data.resourcePack.currentVersionId || $('admin-pack-version-id').value;
  5301. }
  5302. persistState();
  5303. return result;
  5304. });
  5305. $('btn-admin-events-list').onclick = () => run('admin/events/list', async () => {
  5306. const result = await request('GET', '/admin/events?limit=20', undefined, true);
  5307. const first = result.data && result.data[0];
  5308. if (first) {
  5309. $('admin-event-ref-id').value = first.id || $('admin-event-ref-id').value;
  5310. $('event-id').value = first.id || $('event-id').value;
  5311. }
  5312. persistState();
  5313. return result;
  5314. });
  5315. $('btn-admin-event-create').onclick = () => run('admin/events/create', async () => {
  5316. const result = await request('POST', '/admin/events', {
  5317. tenantCode: trimmedOrUndefined($('admin-tenant-code').value),
  5318. slug: $('admin-event-slug').value,
  5319. displayName: $('admin-event-name').value,
  5320. summary: trimmedOrUndefined($('admin-event-summary').value),
  5321. status: $('admin-event-status').value
  5322. }, true);
  5323. $('admin-event-ref-id').value = result.data.id || $('admin-event-ref-id').value;
  5324. $('event-id').value = result.data.id || $('event-id').value;
  5325. persistState();
  5326. return result;
  5327. });
  5328. $('btn-admin-event-update').onclick = () => run('admin/events/update', () =>
  5329. request('PUT', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value), {
  5330. tenantCode: trimmedOrUndefined($('admin-tenant-code').value),
  5331. slug: $('admin-event-slug').value,
  5332. displayName: $('admin-event-name').value,
  5333. summary: trimmedOrUndefined($('admin-event-summary').value),
  5334. status: $('admin-event-status').value
  5335. }, true)
  5336. );
  5337. $('btn-admin-event-detail').onclick = () => run('admin/events/detail', async () => {
  5338. const result = await request('GET', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value), undefined, true);
  5339. if (result.data && result.data.event) {
  5340. $('admin-event-ref-id').value = result.data.event.id || $('admin-event-ref-id').value;
  5341. $('event-id').value = result.data.event.id || $('event-id').value;
  5342. if (result.data.event.currentRelease && result.data.event.currentRelease.id) {
  5343. state.releaseId = result.data.event.currentRelease.id;
  5344. }
  5345. if (result.data.latestSource && result.data.latestSource.id) {
  5346. state.sourceId = result.data.latestSource.id;
  5347. }
  5348. if (result.data.currentPresentation && result.data.currentPresentation.presentationId) {
  5349. $('admin-presentation-id').value = result.data.currentPresentation.presentationId;
  5350. $('admin-release-presentation-id').value = result.data.currentPresentation.presentationId;
  5351. $('config-presentation-id').value = result.data.currentPresentation.presentationId;
  5352. }
  5353. if (result.data.currentContentBundle && result.data.currentContentBundle.contentBundleId) {
  5354. $('admin-content-bundle-id').value = result.data.currentContentBundle.contentBundleId;
  5355. $('admin-release-content-bundle-id').value = result.data.currentContentBundle.contentBundleId;
  5356. $('config-content-bundle-id').value = result.data.currentContentBundle.contentBundleId;
  5357. }
  5358. if (result.data.currentRuntime && result.data.currentRuntime.runtimeBindingId) {
  5359. $('admin-release-runtime-binding-id').value = result.data.currentRuntime.runtimeBindingId;
  5360. $('prod-runtime-binding-id').value = result.data.currentRuntime.runtimeBindingId;
  5361. }
  5362. }
  5363. syncState();
  5364. return result;
  5365. });
  5366. $('btn-admin-presentations-list').onclick = () => run('admin/presentations/list', async () => {
  5367. const result = await request('GET', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/presentations?limit=20', undefined, true);
  5368. const first = result.data && result.data[0];
  5369. if (first) {
  5370. $('admin-presentation-id').value = first.id || $('admin-presentation-id').value;
  5371. $('admin-release-presentation-id').value = first.id || $('admin-release-presentation-id').value;
  5372. $('config-presentation-id').value = first.id || $('config-presentation-id').value;
  5373. }
  5374. persistState();
  5375. return result;
  5376. });
  5377. $('btn-admin-presentation-create').onclick = () => run('admin/presentations/create', async () => {
  5378. const result = await request('POST', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/presentations', {
  5379. code: $('admin-presentation-code').value,
  5380. name: $('admin-presentation-name').value,
  5381. presentationType: $('admin-presentation-type').value,
  5382. status: 'active',
  5383. isDefault: true,
  5384. schema: parseJSONObjectOrUndefined($('admin-presentation-schema-json').value, 'Presentation Schema JSON') || {}
  5385. }, true);
  5386. $('admin-presentation-id').value = result.data.id || $('admin-presentation-id').value;
  5387. $('admin-release-presentation-id').value = result.data.id || $('admin-release-presentation-id').value;
  5388. $('config-presentation-id').value = result.data.id || $('config-presentation-id').value;
  5389. persistState();
  5390. return result;
  5391. });
  5392. $('btn-admin-presentation-import').onclick = () => run('admin/presentations/import', async () => {
  5393. const result = await request('POST', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/presentations/import', {
  5394. title: $('admin-presentation-import-title').value,
  5395. templateKey: $('admin-presentation-import-template-key').value,
  5396. sourceType: $('admin-presentation-import-source-type').value,
  5397. schemaUrl: $('admin-presentation-import-schema-url').value,
  5398. version: $('admin-presentation-import-version').value,
  5399. status: 'active',
  5400. isDefault: true
  5401. }, true);
  5402. $('admin-presentation-id').value = result.data.id || $('admin-presentation-id').value;
  5403. $('admin-presentation-name').value = result.data.name || $('admin-presentation-name').value;
  5404. $('admin-release-presentation-id').value = result.data.id || $('admin-release-presentation-id').value;
  5405. $('config-presentation-id').value = result.data.id || $('config-presentation-id').value;
  5406. persistState();
  5407. return result;
  5408. });
  5409. $('btn-admin-presentation-detail').onclick = () => run('admin/presentations/detail', async () => {
  5410. const result = await request('GET', '/admin/presentations/' + encodeURIComponent($('admin-presentation-id').value), undefined, true);
  5411. if (result.data) {
  5412. $('admin-presentation-id').value = result.data.id || $('admin-presentation-id').value;
  5413. $('admin-release-presentation-id').value = result.data.id || $('admin-release-presentation-id').value;
  5414. $('config-presentation-id').value = result.data.id || $('config-presentation-id').value;
  5415. }
  5416. persistState();
  5417. return result;
  5418. });
  5419. $('btn-admin-content-bundles-list').onclick = () => run('admin/content-bundles/list', async () => {
  5420. const result = await request('GET', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/content-bundles?limit=20', undefined, true);
  5421. const first = result.data && result.data[0];
  5422. if (first) {
  5423. $('admin-content-bundle-id').value = first.id || $('admin-content-bundle-id').value;
  5424. $('admin-release-content-bundle-id').value = first.id || $('admin-release-content-bundle-id').value;
  5425. $('config-content-bundle-id').value = first.id || $('config-content-bundle-id').value;
  5426. }
  5427. persistState();
  5428. return result;
  5429. });
  5430. $('btn-admin-content-bundle-create').onclick = () => run('admin/content-bundles/create', async () => {
  5431. const result = await request('POST', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/content-bundles', {
  5432. code: $('admin-content-bundle-code').value,
  5433. name: $('admin-content-bundle-name').value,
  5434. status: 'active',
  5435. isDefault: true,
  5436. entryUrl: trimmedOrUndefined($('admin-content-entry-url').value),
  5437. assetRootUrl: trimmedOrUndefined($('admin-content-asset-root-url').value),
  5438. metadata: parseJSONObjectOrUndefined($('admin-content-metadata-json').value, 'Content Metadata JSON') || {}
  5439. }, true);
  5440. $('admin-content-bundle-id').value = result.data.id || $('admin-content-bundle-id').value;
  5441. $('admin-release-content-bundle-id').value = result.data.id || $('admin-release-content-bundle-id').value;
  5442. $('config-content-bundle-id').value = result.data.id || $('config-content-bundle-id').value;
  5443. persistState();
  5444. return result;
  5445. });
  5446. $('btn-admin-content-bundle-import').onclick = () => run('admin/content-bundles/import', async () => {
  5447. const result = await request('POST', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/content-bundles/import', {
  5448. title: $('admin-content-import-title').value,
  5449. bundleType: $('admin-content-import-bundle-type').value,
  5450. sourceType: $('admin-content-import-source-type').value,
  5451. manifestUrl: $('admin-content-import-manifest-url').value,
  5452. version: $('admin-content-import-version').value,
  5453. status: 'active',
  5454. isDefault: true,
  5455. assetManifest: parseJSONObjectOrUndefined($('admin-content-import-asset-manifest-json').value, 'Content Asset Manifest JSON')
  5456. }, true);
  5457. $('admin-content-bundle-id').value = result.data.id || $('admin-content-bundle-id').value;
  5458. $('admin-content-bundle-name').value = result.data.name || $('admin-content-bundle-name').value;
  5459. $('admin-release-content-bundle-id').value = result.data.id || $('admin-release-content-bundle-id').value;
  5460. $('config-content-bundle-id').value = result.data.id || $('config-content-bundle-id').value;
  5461. persistState();
  5462. return result;
  5463. });
  5464. $('btn-admin-content-bundle-detail').onclick = () => run('admin/content-bundles/detail', async () => {
  5465. const result = await request('GET', '/admin/content-bundles/' + encodeURIComponent($('admin-content-bundle-id').value), undefined, true);
  5466. if (result.data) {
  5467. $('admin-content-bundle-id').value = result.data.id || $('admin-content-bundle-id').value;
  5468. $('admin-release-content-bundle-id').value = result.data.id || $('admin-release-content-bundle-id').value;
  5469. $('config-content-bundle-id').value = result.data.id || $('config-content-bundle-id').value;
  5470. }
  5471. persistState();
  5472. return result;
  5473. });
  5474. $('btn-admin-event-source').onclick = () => run('admin/events/source', async () => {
  5475. const result = await request('POST', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/source', {
  5476. map: {
  5477. mapId: $('admin-map-id').value,
  5478. versionId: $('admin-map-version-id').value
  5479. },
  5480. playfield: {
  5481. playfieldId: $('admin-playfield-id').value,
  5482. versionId: $('admin-playfield-version-id').value
  5483. },
  5484. resourcePack: $('admin-pack-id').value && $('admin-pack-version-id').value ? {
  5485. resourcePackId: $('admin-pack-id').value,
  5486. versionId: $('admin-pack-version-id').value
  5487. } : undefined,
  5488. gameModeCode: $('admin-game-mode-code').value,
  5489. routeCode: trimmedOrUndefined($('admin-route-code').value),
  5490. overrides: parseJSONObjectOrUndefined($('admin-overrides-json').value, 'Overrides JSON'),
  5491. notes: trimmedOrUndefined($('admin-source-notes').value)
  5492. }, true);
  5493. state.sourceId = result.data.id || state.sourceId;
  5494. syncState();
  5495. return result;
  5496. });
  5497. $('btn-admin-pipeline').onclick = () => run('admin/events/pipeline', async () => {
  5498. const result = await request('GET', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/pipeline?limit=20', undefined, true);
  5499. if (result.data) {
  5500. if (result.data.currentRelease && result.data.currentRelease.id) {
  5501. state.releaseId = result.data.currentRelease.id;
  5502. $('admin-pipeline-release-id').value = result.data.currentRelease.id;
  5503. }
  5504. if (result.data.currentRelease && result.data.currentRelease.runtime && result.data.currentRelease.runtime.runtimeBindingId) {
  5505. $('admin-release-runtime-binding-id').value = result.data.currentRelease.runtime.runtimeBindingId;
  5506. }
  5507. if (result.data.currentRelease && result.data.currentRelease.presentation && result.data.currentRelease.presentation.presentationId) {
  5508. $('admin-release-presentation-id').value = result.data.currentRelease.presentation.presentationId;
  5509. $('admin-presentation-id').value = result.data.currentRelease.presentation.presentationId;
  5510. $('config-presentation-id').value = result.data.currentRelease.presentation.presentationId;
  5511. }
  5512. if (result.data.currentRelease && result.data.currentRelease.contentBundle && result.data.currentRelease.contentBundle.contentBundleId) {
  5513. $('admin-release-content-bundle-id').value = result.data.currentRelease.contentBundle.contentBundleId;
  5514. $('admin-content-bundle-id').value = result.data.currentRelease.contentBundle.contentBundleId;
  5515. $('config-content-bundle-id').value = result.data.currentRelease.contentBundle.contentBundleId;
  5516. }
  5517. if (result.data.sources && result.data.sources[0] && result.data.sources[0].id) {
  5518. state.sourceId = result.data.sources[0].id;
  5519. }
  5520. if (result.data.builds && result.data.builds[0] && result.data.builds[0].id) {
  5521. state.buildId = result.data.builds[0].id;
  5522. }
  5523. }
  5524. syncState();
  5525. return result;
  5526. });
  5527. $('btn-admin-release-detail').onclick = () => run('admin/releases/detail', async () => {
  5528. const result = await request('GET', '/admin/releases/' + encodeURIComponent($('admin-pipeline-release-id').value || state.releaseId), undefined, true);
  5529. if (result.data) {
  5530. state.releaseId = result.data.id || state.releaseId;
  5531. if (result.data.runtime && result.data.runtime.runtimeBindingId) {
  5532. $('admin-release-runtime-binding-id').value = result.data.runtime.runtimeBindingId;
  5533. }
  5534. if (result.data.presentation && result.data.presentation.presentationId) {
  5535. $('admin-release-presentation-id').value = result.data.presentation.presentationId;
  5536. $('admin-presentation-id').value = result.data.presentation.presentationId;
  5537. $('config-presentation-id').value = result.data.presentation.presentationId;
  5538. }
  5539. if (result.data.contentBundle && result.data.contentBundle.contentBundleId) {
  5540. $('admin-release-content-bundle-id').value = result.data.contentBundle.contentBundleId;
  5541. $('admin-content-bundle-id').value = result.data.contentBundle.contentBundleId;
  5542. $('config-content-bundle-id').value = result.data.contentBundle.contentBundleId;
  5543. }
  5544. setDefaultPublishExpectation(result.data);
  5545. }
  5546. syncState();
  5547. return result;
  5548. });
  5549. $('btn-admin-bind-runtime').onclick = () => run('admin/releases/bind-runtime', async () => {
  5550. const runtimeBindingId = $('admin-release-runtime-binding-id').value || $('prod-runtime-binding-id').value;
  5551. const result = await request('POST', '/admin/releases/' + encodeURIComponent($('admin-pipeline-release-id').value || state.releaseId) + '/runtime-binding', {
  5552. runtimeBindingId: runtimeBindingId
  5553. }, true);
  5554. if (result.data) {
  5555. state.releaseId = result.data.id || state.releaseId;
  5556. if (result.data.runtime && result.data.runtime.runtimeBindingId) {
  5557. $('admin-release-runtime-binding-id').value = result.data.runtime.runtimeBindingId;
  5558. }
  5559. }
  5560. syncState();
  5561. return result;
  5562. });
  5563. $('btn-admin-event-defaults').onclick = () => run('admin/events/defaults', async () => {
  5564. const result = await request('POST', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/defaults', {
  5565. presentationId: trimmedOrUndefined($('admin-release-presentation-id').value),
  5566. contentBundleId: trimmedOrUndefined($('admin-release-content-bundle-id').value),
  5567. runtimeBindingId: trimmedOrUndefined($('admin-release-runtime-binding-id').value || $('prod-runtime-binding-id').value)
  5568. }, true);
  5569. if (result.data && result.data.currentPresentation && result.data.currentPresentation.presentationId) {
  5570. $('admin-presentation-id').value = result.data.currentPresentation.presentationId;
  5571. $('admin-release-presentation-id').value = result.data.currentPresentation.presentationId;
  5572. $('config-presentation-id').value = result.data.currentPresentation.presentationId;
  5573. }
  5574. if (result.data && result.data.currentContentBundle && result.data.currentContentBundle.contentBundleId) {
  5575. $('admin-content-bundle-id').value = result.data.currentContentBundle.contentBundleId;
  5576. $('admin-release-content-bundle-id').value = result.data.currentContentBundle.contentBundleId;
  5577. $('config-content-bundle-id').value = result.data.currentContentBundle.contentBundleId;
  5578. }
  5579. if (result.data && result.data.currentRuntime && result.data.currentRuntime.runtimeBindingId) {
  5580. $('admin-release-runtime-binding-id').value = result.data.currentRuntime.runtimeBindingId;
  5581. $('prod-runtime-binding-id').value = result.data.currentRuntime.runtimeBindingId;
  5582. }
  5583. persistState();
  5584. return result;
  5585. });
  5586. $('btn-admin-build-source').onclick = () => run('admin/sources/build', async () => {
  5587. const result = await request('POST', '/admin/sources/' + encodeURIComponent($('admin-pipeline-source-id').value || state.sourceId) + '/build', undefined, true);
  5588. state.buildId = result.data.id || state.buildId;
  5589. state.sourceId = result.data.sourceId || state.sourceId;
  5590. syncState();
  5591. return result;
  5592. });
  5593. $('btn-admin-build-detail').onclick = () => run('admin/builds/detail', async () => {
  5594. const result = await request('GET', '/admin/builds/' + encodeURIComponent($('admin-pipeline-build-id').value || state.buildId), undefined, true);
  5595. state.buildId = result.data.id || state.buildId;
  5596. state.sourceId = result.data.sourceId || state.sourceId;
  5597. syncState();
  5598. return result;
  5599. });
  5600. $('btn-admin-build-publish').onclick = () => run('admin/builds/publish', async () => {
  5601. const result = await request('POST', '/admin/builds/' + encodeURIComponent($('admin-pipeline-build-id').value || state.buildId) + '/publish', {
  5602. runtimeBindingId: trimmedOrUndefined($('admin-release-runtime-binding-id').value || $('prod-runtime-binding-id').value),
  5603. presentationId: trimmedOrUndefined($('admin-release-presentation-id').value),
  5604. contentBundleId: trimmedOrUndefined($('admin-release-content-bundle-id').value)
  5605. }, true);
  5606. state.releaseId = result.data.release.releaseId || state.releaseId;
  5607. if (result.data.runtime && result.data.runtime.runtimeBindingId) {
  5608. $('admin-release-runtime-binding-id').value = result.data.runtime.runtimeBindingId;
  5609. }
  5610. if (result.data.presentation && result.data.presentation.presentationId) {
  5611. $('admin-release-presentation-id').value = result.data.presentation.presentationId;
  5612. $('admin-presentation-id').value = result.data.presentation.presentationId;
  5613. $('config-presentation-id').value = result.data.presentation.presentationId;
  5614. }
  5615. if (result.data.contentBundle && result.data.contentBundle.contentBundleId) {
  5616. $('admin-release-content-bundle-id').value = result.data.contentBundle.contentBundleId;
  5617. $('admin-content-bundle-id').value = result.data.contentBundle.contentBundleId;
  5618. $('config-content-bundle-id').value = result.data.contentBundle.contentBundleId;
  5619. }
  5620. syncState();
  5621. return result;
  5622. });
  5623. $('btn-admin-rollback').onclick = () => run('admin/events/rollback', async () => {
  5624. const result = await request('POST', '/admin/events/' + encodeURIComponent($('admin-event-ref-id').value) + '/rollback', {
  5625. releaseId: $('admin-rollback-release-id').value
  5626. }, true);
  5627. state.releaseId = result.data.id || state.releaseId;
  5628. syncState();
  5629. return result;
  5630. });
  5631. $('btn-copy-curl').onclick = async () => {
  5632. if (!state.lastCurl) {
  5633. setStatus('error: no curl to copy', true);
  5634. return;
  5635. }
  5636. try {
  5637. await navigator.clipboard.writeText(state.lastCurl);
  5638. setStatus('ok: curl copied');
  5639. } catch (_) {
  5640. setStatus('error: clipboard unavailable', true);
  5641. }
  5642. };
  5643. $('btn-clear-history').onclick = () => {
  5644. localStorage.removeItem(HISTORY_KEY);
  5645. renderHistory();
  5646. setStatus('ok: history cleared');
  5647. };
  5648. $('btn-client-logs-refresh').onclick = () => run('dev/client-logs', async () => {
  5649. return await refreshClientLogs();
  5650. });
  5651. $('btn-client-logs-clear').onclick = () => run('dev/client-logs/clear', async () => {
  5652. const result = await request('DELETE', '/dev/client-logs');
  5653. renderClientLogs([]);
  5654. return result;
  5655. });
  5656. $('btn-scenario-save').onclick = saveCurrentScenario;
  5657. $('btn-scenario-load').onclick = loadSelectedScenario;
  5658. $('btn-scenario-delete').onclick = deleteSelectedScenario;
  5659. $('btn-scenario-export').onclick = exportSelectedScenario;
  5660. $('btn-scenario-import').onclick = importScenarioFromJSON;
  5661. $('btn-flow-home').onclick = () => run('flow-home', async () => {
  5662. const bootstrap = await request('POST', '/dev/bootstrap-demo');
  5663. if (bootstrap.data) {
  5664. applyBootstrapContext(bootstrap.data);
  5665. }
  5666. const login = await request('POST', '/auth/login/wechat-mini', {
  5667. code: $('wechat-code').value,
  5668. clientType: 'wechat',
  5669. deviceKey: $('wechat-device').value
  5670. });
  5671. state.accessToken = login.data.tokens.accessToken;
  5672. state.refreshToken = login.data.tokens.refreshToken;
  5673. return await request('GET', '/me/entry-home?channelCode=' + encodeURIComponent($('entry-channel-code').value) + '&channelType=' + encodeURIComponent($('entry-channel-type').value), undefined, true);
  5674. });
  5675. $('btn-bootstrap').onclick = () => run('bootstrap-demo', async () => {
  5676. const result = await request('POST', '/dev/bootstrap-demo');
  5677. applyBootstrapContext(result.data);
  5678. return result;
  5679. });
  5680. $('btn-bootstrap-publish').onclick = () => run('bootstrap-publish-current', async () => {
  5681. return await runAdminDefaultPublishFlow({ ensureRuntime: true, bootstrapDemo: true });
  5682. });
  5683. $('btn-use-classic-demo').onclick = () => run('use-classic-demo', async () => {
  5684. const result = await request('POST', '/dev/bootstrap-demo');
  5685. applyFrontendDemoSelection({
  5686. eventId: result.data.eventId || 'evt_demo_001',
  5687. releaseId: result.data.releaseId || 'rel_demo_001',
  5688. localConfigFile: 'classic-sequential.json',
  5689. gameModeCode: 'classic-sequential',
  5690. demoKind: 'classic',
  5691. sourceId: result.data.sourceId || '',
  5692. buildId: result.data.buildId || '',
  5693. courseSetId: result.data.courseSetId || '',
  5694. courseVariantId: result.data.courseVariantId || '',
  5695. runtimeBindingId: result.data.runtimeBindingId || '',
  5696. logTitle: 'classic-demo-ready',
  5697. statusText: 'ok: classic demo loaded'
  5698. });
  5699. return result;
  5700. });
  5701. $('btn-use-score-o-demo').onclick = () => run('use-score-o-demo', async () => {
  5702. const result = await request('POST', '/dev/bootstrap-demo');
  5703. applyFrontendDemoSelection({
  5704. eventId: result.data.scoreOEventId || 'evt_demo_score_o_001',
  5705. releaseId: result.data.scoreOReleaseId || 'rel_demo_score_o_001',
  5706. localConfigFile: 'score-o.json',
  5707. gameModeCode: 'score-o',
  5708. demoKind: 'score-o',
  5709. sourceId: result.data.scoreOSourceId || '',
  5710. buildId: result.data.scoreOBuildId || '',
  5711. courseSetId: result.data.scoreOCourseSetId || '',
  5712. courseVariantId: result.data.scoreOCourseVariantId || '',
  5713. runtimeBindingId: result.data.scoreORuntimeBindingId || '',
  5714. logTitle: 'score-o-demo-ready',
  5715. statusText: 'ok: score-o demo loaded'
  5716. });
  5717. return result;
  5718. });
  5719. $('btn-use-variant-manual-demo').onclick = () => run('use-variant-manual-demo', async () => {
  5720. const result = await request('POST', '/dev/bootstrap-demo');
  5721. applyFrontendDemoSelection({
  5722. eventId: result.data.variantManualEventId || 'evt_demo_variant_manual_001',
  5723. releaseId: result.data.variantManualReleaseId || 'rel_demo_variant_manual_001',
  5724. variantId: 'variant_d',
  5725. localConfigFile: 'classic-sequential.json',
  5726. gameModeCode: 'classic-sequential',
  5727. demoKind: 'manual-variant',
  5728. sourceId: result.data.variantManualSourceId || '',
  5729. buildId: result.data.variantManualBuildId || '',
  5730. courseSetId: result.data.variantManualCourseSetId || '',
  5731. courseVariantId: result.data.variantManualCourseVariantId || '',
  5732. runtimeBindingId: result.data.variantManualRuntimeBindingId || '',
  5733. logTitle: 'variant-manual-demo-ready',
  5734. statusText: 'ok: manual variant demo loaded'
  5735. });
  5736. return result;
  5737. });
  5738. $('btn-flow-launch').onclick = () => run('flow-launch', async () => {
  5739. const bootstrap = await request('POST', '/dev/bootstrap-demo');
  5740. if (bootstrap.data) {
  5741. applyBootstrapContext(bootstrap.data);
  5742. }
  5743. const smsSend = await request('POST', '/auth/sms/send', {
  5744. countryCode: $('sms-country').value,
  5745. mobile: $('sms-mobile').value,
  5746. clientType: $('sms-client-type').value,
  5747. deviceKey: $('sms-device').value,
  5748. scene: 'login'
  5749. });
  5750. if (smsSend.data && smsSend.data.devCode) {
  5751. $('sms-code').value = smsSend.data.devCode;
  5752. }
  5753. const login = await request('POST', '/auth/login/sms', {
  5754. countryCode: $('sms-country').value,
  5755. mobile: $('sms-mobile').value,
  5756. code: $('sms-code').value,
  5757. clientType: $('sms-client-type').value,
  5758. deviceKey: $('sms-device').value
  5759. });
  5760. state.accessToken = login.data.tokens.accessToken;
  5761. state.refreshToken = login.data.tokens.refreshToken;
  5762. const launch = await request('POST', '/events/' + encodeURIComponent($('event-id').value) + '/launch', {
  5763. releaseId: $('event-release-id').value,
  5764. variantId: trimmedOrUndefined($('event-variant-id').value),
  5765. clientType: $('sms-client-type').value,
  5766. deviceKey: $('event-device').value
  5767. }, true);
  5768. state.sessionId = launch.data.launch.business.sessionId;
  5769. state.sessionToken = launch.data.launch.business.sessionToken;
  5770. return await request('POST', '/sessions/' + encodeURIComponent(state.sessionId) + '/start', {
  5771. sessionToken: state.sessionToken
  5772. });
  5773. });
  5774. $('btn-flow-finish').onclick = () => run('flow-finish', async () => {
  5775. return await request('POST', '/sessions/' + encodeURIComponent($('session-id').value) + '/finish', {
  5776. sessionToken: $('session-token').value,
  5777. status: $('finish-status').value,
  5778. summary: buildFinishSummary()
  5779. });
  5780. });
  5781. $('btn-flow-result').onclick = () => run('flow-result', async () => {
  5782. await request('POST', '/sessions/' + encodeURIComponent($('session-id').value) + '/finish', {
  5783. sessionToken: $('session-token').value,
  5784. status: $('finish-status').value,
  5785. summary: buildFinishSummary()
  5786. });
  5787. return await request('GET', '/sessions/' + encodeURIComponent($('session-id').value) + '/result', undefined, true);
  5788. });
  5789. $('btn-flow-admin-default-publish').onclick = () => run('flow-admin-default-publish', async () => {
  5790. return await runAdminDefaultPublishFlow({ ensureRuntime: false, bootstrapDemo: false });
  5791. });
  5792. $('btn-flow-admin-runtime-publish').onclick = () => run('flow-admin-runtime-publish', async () => {
  5793. return await runAdminDefaultPublishFlow({ ensureRuntime: true, bootstrapDemo: false });
  5794. });
  5795. $('btn-flow-standard-regression').onclick = () => run('flow-standard-regression', async () => {
  5796. return await runStandardRegressionFlow();
  5797. });
  5798. [
  5799. 'sms-client-type', 'sms-scene', 'sms-mobile', 'sms-device', 'sms-country', 'sms-code',
  5800. 'wechat-code', 'wechat-device', 'local-config-file', 'config-event-id', 'config-runtime-binding-id', 'config-presentation-id', 'config-content-bundle-id', 'entry-channel-code', 'entry-channel-type',
  5801. 'event-id', 'event-release-id', 'event-device', 'finish-status', 'finish-duration', 'finish-score',
  5802. 'finish-controls-done', 'finish-controls-total', 'finish-distance', 'finish-speed',
  5803. 'finish-heart-rate', 'prod-place-code', 'prod-place-name', 'prod-place-id', 'prod-place-status',
  5804. 'prod-place-region', 'prod-place-cover-url', 'prod-map-asset-code', 'prod-map-asset-name',
  5805. 'prod-map-asset-id', 'prod-map-asset-legacy-map-id', 'prod-map-asset-type', 'prod-map-asset-status',
  5806. 'prod-tile-release-id', 'prod-tile-legacy-version-id', 'prod-tile-version-code', 'prod-tile-status',
  5807. 'prod-tile-base-url', 'prod-tile-meta-url', 'prod-course-source-id',
  5808. 'prod-course-source-legacy-playfield-id', 'prod-course-source-legacy-version-id',
  5809. 'prod-course-source-type', 'prod-course-source-file-url', 'prod-course-source-status',
  5810. 'prod-course-set-code', 'prod-course-set-name', 'prod-course-set-id', 'prod-course-mode',
  5811. 'prod-course-set-status', 'prod-course-default-route-code', 'prod-course-routes-json',
  5812. 'prod-course-variant-id', 'prod-course-variant-name',
  5813. 'prod-course-variant-route-code', 'prod-course-variant-status', 'prod-course-variant-control-count',
  5814. 'prod-runtime-binding-id', 'prod-runtime-event-id', 'prod-runtime-binding-status', 'prod-runtime-notes',
  5815. 'admin-map-code', 'admin-map-name', 'admin-map-id', 'admin-map-version-id',
  5816. 'admin-map-version-code', 'admin-map-status', 'admin-mapmeta-url', 'admin-tiles-root-url',
  5817. 'admin-playfield-code', 'admin-playfield-name', 'admin-playfield-id', 'admin-playfield-version-id',
  5818. 'admin-playfield-kind', 'admin-playfield-status', 'admin-playfield-version-code', 'admin-playfield-source-type',
  5819. 'admin-playfield-source-url', 'admin-playfield-control-count', 'admin-pack-code', 'admin-pack-name',
  5820. 'admin-pack-id', 'admin-pack-version-id', 'admin-pack-version-code', 'admin-pack-status',
  5821. 'admin-pack-content-url', 'admin-pack-audio-url', 'admin-pack-theme-code', 'admin-published-asset-root',
  5822. 'admin-tenant-code', 'admin-event-status', 'admin-event-ref-id', 'admin-event-slug', 'admin-event-name',
  5823. 'admin-event-summary', 'admin-game-mode-code', 'admin-route-code', 'admin-source-notes',
  5824. 'admin-overrides-json', 'admin-presentation-id', 'admin-presentation-code', 'admin-presentation-name',
  5825. 'admin-presentation-type', 'admin-presentation-schema-json', 'admin-presentation-import-title',
  5826. 'admin-presentation-import-template-key', 'admin-presentation-import-source-type',
  5827. 'admin-presentation-import-version', 'admin-presentation-import-schema-url', 'admin-content-bundle-id',
  5828. 'admin-content-bundle-code', 'admin-content-bundle-name', 'admin-content-entry-url',
  5829. 'admin-content-asset-root-url', 'admin-content-metadata-json', 'admin-content-import-title',
  5830. 'admin-content-import-bundle-type', 'admin-content-import-source-type', 'admin-content-import-version',
  5831. 'admin-content-import-manifest-url', 'admin-content-import-asset-manifest-json', 'admin-pipeline-source-id',
  5832. 'admin-pipeline-build-id', 'admin-pipeline-release-id', 'admin-release-runtime-binding-id',
  5833. 'admin-release-presentation-id', 'admin-release-content-bundle-id', 'admin-rollback-release-id'
  5834. ].forEach(function(id) {
  5835. $(id).addEventListener('change', function() {
  5836. persistState();
  5837. syncCurrentFlowStatusFromForms();
  5838. });
  5839. $(id).addEventListener('input', function() {
  5840. persistState();
  5841. syncCurrentFlowStatusFromForms();
  5842. });
  5843. });
  5844. $('api-filter').addEventListener('input', applyAPIFilter);
  5845. restoreState();
  5846. if (
  5847. !$('admin-presentation-import-schema-url').value ||
  5848. $('admin-presentation-import-schema-url').value.indexOf('example.com') >= 0 ||
  5849. !$('admin-content-import-manifest-url').value ||
  5850. $('admin-content-import-manifest-url').value.indexOf('example.com') >= 0
  5851. ) {
  5852. applyDemoImportInputs('classic');
  5853. }
  5854. syncWorkbenchMode();
  5855. syncState();
  5856. renderHistory();
  5857. renderScenarioOptions();
  5858. applyAPIFilter();
  5859. syncAPICounts();
  5860. resetProgress();
  5861. renderClientLogs([]);
  5862. writeLog('workbench-ready', { ok: true, hint: 'Use Bootstrap Demo first on a fresh database.' });
  5863. window.addEventListener('resize', scheduleMasonryLayout);
  5864. scheduleMasonryLayout();
  5865. </script>
  5866. </body>
  5867. </html>`