|
|
@@ -0,0 +1,54 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+// Patch the actual online baseline, never deploy unrelated local gateway edits.
|
|
|
+import {readFileSync,writeFileSync,mkdirSync} from 'node:fs';
|
|
|
+import {createHash} from 'node:crypto';
|
|
|
+import {resolve} from 'node:path';
|
|
|
+import {pathToFileURL} from 'node:url';
|
|
|
+const helper=readFileSync(new URL('./cloud/support-requests.js',import.meta.url),'utf8').trim();
|
|
|
+const dispatch=" if(operation==='ops/support-requests/list') return response.json({success:true,data:await supportRequestsPage(context,input)});";
|
|
|
+const anchor=" if(operation==='ops/live/versions')";
|
|
|
+export function patchSupportRequests(code){
|
|
|
+ if(code.includes('ops/support-requests/list')||code.includes('async function supportRequestsPage(')){
|
|
|
+ if(code.includes(dispatch)&&code.includes(helper))return code;
|
|
|
+ throw Error('线上已有不同版本的反馈入口,请先人工核对');
|
|
|
+ }
|
|
|
+ if(code.split(anchor).length!==2)throw Error('线上网关分发入口与预期不一致,停止发布');
|
|
|
+ const next=code.replace(anchor,dispatch+'\n'+anchor)+'\n'+helper+'\n';
|
|
|
+ if(new Function(next+'\nreturn typeof handler;')()!=='function')throw Error('网关语法校验失败');
|
|
|
+ return next;
|
|
|
+}
|
|
|
+async function main(){
|
|
|
+ const master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN;
|
|
|
+ if(!master)throw Error('请通过受信任的部署配置设置 XIAOSHU_MASTER_KEY;不要将密钥写入脚本或提交到 Git');
|
|
|
+ if(!token)throw Error('缺少用于发布后只读验证的 XIAOSHU_SYNC_OPERATOR_TOKEN');
|
|
|
+ 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';
|
|
|
+ const hash=s=>createHash('sha256').update(s).digest('hex');
|
|
|
+ const root=resolve(process.env.XIAOSHU_RELEASE_DIR||'dist/support-requests-release',new Date().toISOString().replace(/[:.]/g,'-'));
|
|
|
+ const headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master};
|
|
|
+ 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;}
|
|
|
+ const rows=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({path:'xiaoshu/ops/gateway-v3'}),limit:'2'}))).results;
|
|
|
+ if(rows?.length!==1)throw Error('未找到唯一的运营 V3 网关');
|
|
|
+ const before=rows[0],code=patchSupportRequests(before.code);
|
|
|
+ mkdirSync(root,{recursive:true,mode:0o700});
|
|
|
+ writeFileSync(root+'/before.json',JSON.stringify(before,null,2),{mode:0o600});
|
|
|
+ writeFileSync(root+'/candidate.js',code,{mode:0o600});
|
|
|
+ const proof={objectId:before.objectId,beforeHash:hash(before.code),afterHash:hash(code),backup:root+'/before.json'};
|
|
|
+ if(!commit){console.log(JSON.stringify({...proof,dryRun:true}));return;}
|
|
|
+ const functionBase=(process.env.XIAOSHU_FUNCTION_URL||base.replace(/\/parse$/,'/api/functions')).replace(/\/$/,'');
|
|
|
+ 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)});
|
|
|
+ const sync=await syncResponse.json();
|
|
|
+ if(!syncResponse.ok||!sync.success||!Array.isArray(sync.data?.runs))throw Error('无法确认同步任务状态,停止发布');
|
|
|
+ if(sync.data.runs.some(j=>j.status==='running'))throw Error('同步任务正在运行,请空闲后重试');
|
|
|
+ const latest=await parse('/classes/Function/'+before.objectId);
|
|
|
+ if(latest.code!==before.code||latest.updatedAt!==before.updatedAt)throw Error('线上网关已被其他发布修改,停止覆盖');
|
|
|
+ if(code!==before.code)await parse('/classes/Function/'+before.objectId,'PUT',{code});
|
|
|
+ const after=await parse('/classes/Function/'+before.objectId);
|
|
|
+ if(after.code!==code)throw Error('发布后代码回读不一致,请检查备份和线上状态');
|
|
|
+ const url=(process.env.XIAOSHU_FUNCTION_URL||base.replace(/\/parse$/,'/api/functions'))+'/xiaoshu/ops/gateway-v3';
|
|
|
+ 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();
|
|
|
+ const passed=r.ok&&result.success===true&&Array.isArray(result.data?.items);
|
|
|
+ writeFileSync(root+'/proof.json',JSON.stringify({...proof,deployed:true,readbackVerified:true,liveQueryVerified:passed,total:result.data?.total,httpStatus:r.status},null,2),{mode:0o600});
|
|
|
+ if(!passed)throw Error('代码发布成功,但真实查询未通过:'+String(result.message||result.error||r.status));
|
|
|
+ console.log(JSON.stringify({...proof,deployed:true,readbackVerified:true,liveQueryVerified:true,total:result.data.total}));
|
|
|
+}
|
|
|
+if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href)main().catch(e=>{console.error(e.message);process.exitCode=1;});
|