| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- #!/usr/bin/env node
- // Scope both patches to the actual online baseline and migrate only the reply ledger.
- import {readFileSync,writeFileSync,mkdirSync} from 'node:fs';
- import {createHash,randomBytes} from 'node:crypto';
- import {resolve} from 'node:path';
- import {pathToFileURL} from 'node:url';
- const hash=s=>createHash('sha256').update(s).digest('hex');
- const adminHelper=readFileSync(new URL('./cloud/support-requests.js',import.meta.url),'utf8').trim();
- const appHelper=readFileSync(new URL('./cloud/support-requests-app.js',import.meta.url),'utf8').trim();
- const processDispatch=" if(operation==='ops/support-requests/process') return response.json({success:true,data:await supportRequestProcess(context,input)});";
- const listDispatch=" if(operation==='ops/support-requests/list') return response.json({success:true,data:await supportRequestsPage(context,input)});";
- const appDispatch=" if (['app_support_requests_list','app_support_request_read'].includes(action)) return response.json(envelope(await appSupportRequests(input,current)));";
- function replaceOnce(code,anchor,value){if(code.split(anchor).length!==2)throw Error('发布锚点缺失或不唯一');return code.replace(anchor,value);}
- function supportCapabilities(code){if(!code.includes("action === 'migration_status'"))return code;const prefix="implemented: ['app_support_requests_list','app_support_request_read',";return code.includes(prefix)?code:replaceOnce(code,'implemented: [',prefix);}
- export function patchWorkflow(code,kind){
- let next=code;
- if(kind==='admin'){
- if(code.includes(adminHelper)&&code.includes(processDispatch))return code;
- if(code.includes('supportRequestProcess('))throw Error('线上已有不同版本处理逻辑');
- const start=code.indexOf('// Both mobile roles submit category 22');
- if(start<0)throw Error('未找到已发布的反馈看板');
- const end=code.indexOf('\n}',start)+2,old=code.slice(start,end);
- if(hash(old+'\n')!=='6414de94dd69ee55cecb634ccf9ba08a837397d316c0d6ba0d482047f03db7e0')throw Error('线上反馈模块已变化,停止覆盖');
- next=replaceOnce(next,old,adminHelper);
- next=replaceOnce(next,listDispatch,processDispatch+'\n'+listDispatch);
- }else if(kind==='app'){
- if(code.includes(appHelper)&&code.includes(appDispatch))return supportCapabilities(code);
- if(code.includes('appSupportRequests('))throw Error('线上已有不同版本回复逻辑');
- const anchor=" if (action === 'guestbook_add')";
- next=replaceOnce(next,anchor,appDispatch+'\n'+anchor)+'\n'+appHelper+'\n';
- }else throw Error('无效网关类型');
- if(kind==='app')next=supportCapabilities(next);
- if(new Function(next+'\nreturn typeof handler;')()!=='function')throw Error('网关语法校验失败');
- return next;
- }
- export async function deployWorkflow(commit=false){
- const master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN;
- if(!master||!token)throw Error('缺少受信任部署配置');
- const base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',functionBase=(process.env.XIAOSHU_FUNCTION_URL||base.replace(/\/parse$/,'/api/functions')).replace(/\/$/,'');
- const headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master};
- const parse=async(path,method='GET',body)=>{const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(30000)});const d=await r.json();if(!r.ok||d.error)throw Error('发布接口失败 '+path.split('?')[0]+' HTTP '+r.status);return d;};
- const call=async(path,params={})=>{const r=await fetch(functionBase+'/'+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params}),signal:AbortSignal.timeout(60000)});const d=await r.json();if(!r.ok||d.success===false)throw Error(d.message||'网关验证失败 HTTP '+r.status);return d;};
- const meResponse=await fetch(base+'/users/me',{headers:{'X-Parse-Application-Id':app,'X-Parse-Session-Token':token}}),me=await meResponse.json();
- if(!meResponse.ok||!me.objectId||!me.company?.objectId)throw Error('无法确认发布账号归属');
- const idle=await call('xiaoshu/sync/direct-v1',{action:'status'});
- if(!Array.isArray(idle.data?.runs)||idle.data.runs.some(r=>r.status==='running'))throw Error('同步任务状态不可用或正在运行');
- const root=resolve('dist/support-workflow-release',new Date().toISOString().replace(/[:.]/g,'-'));mkdirSync(root,{recursive:true,mode:0o700});
- const candidates=[];
- for(const [kind,path] of [['app','xiaoshu/app/gateway'],['admin','xiaoshu/ops/gateway-v3']]){
- const rows=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({path}),limit:'2'}))).results;
- if(rows?.length!==1)throw Error('未找到唯一网关 '+path);
- const before=rows[0],code=patchWorkflow(before.code,kind);
- writeFileSync(root+'/'+kind+'-before.json',JSON.stringify(before,null,2),{mode:0o600});writeFileSync(root+'/'+kind+'-candidate.js',code,{mode:0o600});candidates.push({kind,before,code});
- }
- if(!commit){console.log(JSON.stringify({dryRun:true,backup:root}));return;}
- let helper;
- try{
- const sql=readFileSync(new URL('./sql/support-requests.sql',import.meta.url),'utf8'),path='xiaoshu/system/support-migration-'+randomBytes(12).toString('hex');
- const code=`async function handler(request,response){try{const u=request.user;if(!u||u.id!==${JSON.stringify(me.objectId)}||Date.now()>${Date.now()+600000})return response.status(403).json({success:false});await u.fetch({useMasterKey:true});if(u.get('company')?.id!==${JSON.stringify(me.company.objectId)})return response.status(403).json({success:false});await Psql.tx(async t=>{await t.none(${JSON.stringify(sql)});});return response.json({success:true});}catch(e){return response.status(500).json({success:false,message:e.message});}}`;
- helper=await parse('/classes/Function','POST',{name:path,path,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'});
- await call(path);
- for(const {kind,before,code} of candidates){
- const latest=await parse('/classes/Function/'+before.objectId);
- if(latest.code!==before.code||latest.updatedAt!==before.updatedAt)throw Error('网关已被并行发布修改,停止覆盖 '+kind);
- if(code!==before.code)await parse('/classes/Function/'+before.objectId,'PUT',{code});
- if((await parse('/classes/Function/'+before.objectId)).code!==code)throw Error('发布回读失败 '+kind);
- }
- const live=await call('xiaoshu/ops/gateway-v3',{operation:'ops/support-requests/list',page:1,pageSize:1});
- if(!Array.isArray(live.data?.items)||typeof live.data.canProcess!=='boolean')throw Error('真实看板查询失败');
- const proof={deployed:true,backup:root,total:live.data.total,gateways:candidates.map(x=>({kind:x.kind,id:x.before.objectId,before:hash(x.before.code),after:hash(x.code)}))};writeFileSync(root+'/proof.json',JSON.stringify(proof,null,2),{mode:0o600});console.log(JSON.stringify(proof));
- }finally{if(helper)await parse('/classes/Function/'+helper.objectId,'DELETE');}
- }
- if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href)deployWorkflow(process.argv.includes('--commit')).catch(e=>{console.error(e.message);process.exitCode=1;});
|