deploy-support-requests.mjs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. #!/usr/bin/env node
  2. // Patch the actual online baseline, never deploy unrelated local gateway edits.
  3. import {readFileSync,writeFileSync,mkdirSync} from 'node:fs';
  4. import {createHash} from 'node:crypto';
  5. import {resolve} from 'node:path';
  6. import {pathToFileURL} from 'node:url';
  7. const helper=readFileSync(new URL('./cloud/support-requests.js',import.meta.url),'utf8').trim();
  8. const dispatch=" if(operation==='ops/support-requests/list') return response.json({success:true,data:await supportRequestsPage(context,input)});";
  9. const anchor=" if(operation==='ops/live/versions')";
  10. export function patchSupportRequests(code){
  11. if(code.includes('ops/support-requests/list')||code.includes('async function supportRequestsPage(')){
  12. if(code.includes(dispatch)&&code.includes(helper))return code;
  13. throw Error('线上已有不同版本的反馈入口,请先人工核对');
  14. }
  15. if(code.split(anchor).length!==2)throw Error('线上网关分发入口与预期不一致,停止发布');
  16. const next=code.replace(anchor,dispatch+'\n'+anchor)+'\n'+helper+'\n';
  17. if(new Function(next+'\nreturn typeof handler;')()!=='function')throw Error('网关语法校验失败');
  18. return next;
  19. }
  20. async function main(){
  21. const master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN;
  22. if(!master)throw Error('请通过受信任的部署配置设置 XIAOSHU_MASTER_KEY;不要将密钥写入脚本或提交到 Git');
  23. if(!token)throw Error('缺少用于发布后只读验证的 XIAOSHU_SYNC_OPERATOR_TOKEN');
  24. const commit=process.argv.includes('--commit'),base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main';
  25. const hash=s=>createHash('sha256').update(s).digest('hex');
  26. const root=resolve(process.env.XIAOSHU_RELEASE_DIR||'dist/support-requests-release',new Date().toISOString().replace(/[:.]/g,'-'));
  27. const headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master};
  28. async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(20000)});const p=await r.json();if(!r.ok||p.error)throw Error('发布接口失败:'+path.split('?')[0]+' HTTP '+r.status+' '+JSON.stringify(p.error||p.code));return p;}
  29. const rows=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({path:'xiaoshu/ops/gateway-v3'}),limit:'2'}))).results;
  30. if(rows?.length!==1)throw Error('未找到唯一的运营 V3 网关');
  31. const before=rows[0],code=patchSupportRequests(before.code);
  32. mkdirSync(root,{recursive:true,mode:0o700});
  33. writeFileSync(root+'/before.json',JSON.stringify(before,null,2),{mode:0o600});
  34. writeFileSync(root+'/candidate.js',code,{mode:0o600});
  35. const proof={objectId:before.objectId,beforeHash:hash(before.code),afterHash:hash(code),backup:root+'/before.json'};
  36. if(!commit){console.log(JSON.stringify({...proof,dryRun:true}));return;}
  37. const functionBase=(process.env.XIAOSHU_FUNCTION_URL||base.replace(/\/parse$/,'/api/functions')).replace(/\/$/,'');
  38. const syncResponse=await fetch(functionBase+'/xiaoshu/sync/direct-v1',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{action:'status'}}),signal:AbortSignal.timeout(20000)});
  39. const sync=await syncResponse.json();
  40. if(!syncResponse.ok||!sync.success||!Array.isArray(sync.data?.runs))throw Error('无法确认同步任务状态,停止发布');
  41. if(sync.data.runs.some(j=>j.status==='running'))throw Error('同步任务正在运行,请空闲后重试');
  42. const latest=await parse('/classes/Function/'+before.objectId);
  43. if(latest.code!==before.code||latest.updatedAt!==before.updatedAt)throw Error('线上网关已被其他发布修改,停止覆盖');
  44. if(code!==before.code)await parse('/classes/Function/'+before.objectId,'PUT',{code});
  45. const after=await parse('/classes/Function/'+before.objectId);
  46. if(after.code!==code)throw Error('发布后代码回读不一致,请检查备份和线上状态');
  47. const url=(process.env.XIAOSHU_FUNCTION_URL||base.replace(/\/parse$/,'/api/functions'))+'/xiaoshu/ops/gateway-v3';
  48. const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{operation:'ops/support-requests/list',page:1,pageSize:20}}),signal:AbortSignal.timeout(20000)}),result=await r.json();
  49. const passed=r.ok&&result.success===true&&Array.isArray(result.data?.items);
  50. writeFileSync(root+'/proof.json',JSON.stringify({...proof,deployed:true,readbackVerified:true,liveQueryVerified:passed,total:result.data?.total,httpStatus:r.status},null,2),{mode:0o600});
  51. if(!passed)throw Error('代码发布成功,但真实查询未通过:'+String(result.message||result.error||r.status));
  52. console.log(JSON.stringify({...proof,deployed:true,readbackVerified:true,liveQueryVerified:true,total:result.data.total}));
  53. }
  54. if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href)main().catch(e=>{console.error(e.message);process.exitCode=1;});