deploy-direct-sync.mjs 4.1 KB

1234567891011121314151617181920212223242526272829303132
  1. #!/usr/bin/env node
  2. import {readFileSync,writeFileSync,mkdirSync,readdirSync} from 'node:fs';
  3. import {randomUUID} from 'node:crypto';
  4. import {buildDirectBundle} from './lib/legacy-direct-bundle.mjs';
  5. const key=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN;
  6. const out=process.env.XIAOSHU_DIRECT_DEPLOY_REPORT_DIR;
  7. if(!key||!token||!out)throw Error('Deployment credentials and private backup directory required');
  8. mkdirSync(out,{recursive:true,mode:0o700});
  9. const base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',co=process.env.XIAOSHU_SYNC_COMPANY_ID||'7pIbDBJmKx';
  10. const headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':key};
  11. async function api(path,method='GET',body){const r=await fetch(base+path,{method,headers,...(body?{body:JSON.stringify(body)}:{}),signal:AbortSignal.timeout(90000)}),j=await r.json();if(!r.ok||j.error)throw Error('Deployment HTTP '+r.status);return j;}
  12. const sessions=(await api('/classes/_Session?include=user&limit=1000&order=-updatedAt')).results;
  13. const session=sessions.find(s=>s.sessionToken===token&&s.user?.adminRoleKey==='super-admin'&&!s.user.isDisabled&&s.user.company?.objectId===co);
  14. if(!session)throw Error('A valid scoped super-admin session is required');
  15. const path='xiaoshu/system/direct-deploy-'+randomUUID(),expiry=Date.now()+600000;
  16. const sql=readFileSync(new URL('./sql/legacy-direct-sync.sql',import.meta.url),'utf8');let helperId;
  17. try{
  18. const code=`async function handler(request,response){if(!request.user||request.user.id!==${JSON.stringify(session.user.objectId)}||Date.now()>${expiry})return response.status(403).json({success:false});try{await Psql.tx(async t=>t.none(${JSON.stringify(sql)}));return response.json({success:true,data:await Psql.query(${JSON.stringify("SELECT table_name,column_name FROM information_schema.columns WHERE table_schema='public'")})});}catch(e){return response.status(500).json({success:false,message:e.message});}}`;
  19. new Function(code);helperId=(await api('/classes/Function','POST',{name:path,path,type:'standalone',enabled:true,code,params:[],paramList:[],respType:'json'})).objectId;
  20. const r=await fetch(base.replace(/\/parse$/,'/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{}})}),j=await r.json();if(!r.ok||!j.success)throw Error('Direct sync schema installation failed');
  21. const windows=[];const directory=new URL('../docs/migration/',import.meta.url);
  22. for(const f of readdirSync(directory))if(/^core-business-backfill-commit-2026-09-0[12]T.*\.json$/.test(f)){const d=JSON.parse(readFileSync(new URL(f,directory),'utf8'));windows.push([Date.parse(d.startedAt),Date.parse(d.completedAt)]);}
  23. const bundle=buildDirectBundle(j.data,windows,co);new Function(bundle);
  24. const name='xiaoshu.sync.direct-v1',route='xiaoshu/sync/direct-v1';
  25. const current=(await api('/classes/Function?'+new URLSearchParams({where:JSON.stringify({name}),limit:'2'}))).results;if(current.length>1)throw Error('Duplicate direct sync gateway');
  26. writeFileSync(out+'/function-before.json',JSON.stringify(current,null,2),{mode:0o600});
  27. const definition={name,path:route,type:'standalone',enabled:true,code:bundle,params:[],paramList:[],respType:'json',desc:'旧WXAPP只读接口夜间同步,保留新修改与批次幂等'};
  28. const result=current.length?await api('/classes/Function/'+current[0].objectId,'PUT',definition):await api('/classes/Function','POST',definition);
  29. const actual=await api('/classes/Function/'+(current[0]?.objectId||result.objectId));if(actual.code!==bundle)throw Error('Deployed source readback mismatch');
  30. writeFileSync(out+'/function-after.json',JSON.stringify(actual,null,2),{mode:0o600});
  31. console.log('Direct API sync gateway deployed; source readback matches.');
  32. }finally{if(helperId){await api('/classes/Function/'+helperId,'DELETE');const remaining=await api('/classes/Function?'+new URLSearchParams({where:JSON.stringify({objectId:helperId}),count:'1',limit:'0'}));if(remaining.count)throw Error('Deployment helper cleanup failed');console.log('Deployment helper cleanup verified.');}}