| 1234567891011121314151617181920212223242526272829303132333435363738394041424344 |
- #!/usr/bin/env node
- import {randomUUID} from 'node:crypto';
- import {mkdir,writeFile,rename} from 'node:fs/promises';
- import {homedir} from 'node:os';
- import {resolve,join} from 'node:path';
- import {fileURLToPath} from 'node:url';
- export async function reconcileAppointmentDeletions(env=process.env,{sourceRunId,dryRun=false,call:providedCall,onProgress=()=>{}}={}){
- const root=resolve(env.XIAOSHU_SYNC_REPORT_DIR||join(homedir(),'.local/state/xiaoshu/legacy-sync'));
- await mkdir(root,{recursive:true,mode:0o700});
- const report={runId:randomUUID(),sourceRunId,dryRun,startedAt:new Date().toISOString(),status:'running',scanned:0,present:0,cancelled:0,wouldCancel:0,conflicts:0};
- const path=join(root,'appointment-deletions-'+report.runId+'.json');
- const save=async()=>{await writeFile(path+'.tmp',JSON.stringify(report,null,2),{mode:0o600});await rename(path+'.tmp',path);};
- const call=providedCall||(async params=>{
- if(!env.XIAOSHU_SYNC_OPERATOR_TOKEN)throw Error('missing_sync_operator_session');
- const url=(env.XIAOSHU_FUNCTION_URL||'https://server.xiaoshu.pro/api/functions').replace(/\/$/,'')+'/xiaoshu/sync/appointment-deletions-v1';
- for(let attempt=0;;attempt++)try{
- const response=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:env.XIAOSHU_SYNC_OPERATOR_TOKEN,params}),signal:AbortSignal.timeout(60000)}),value=await response.json();
- if(!response.ok||!value.success)throw Error(value.message||'appointment_deletion_request_failed');return value.data;
- }catch(error){if(attempt>=2)throw error;}
- });
- const request=params=>call({...params,runId:report.runId,...(sourceRunId?{sourceRunId}:{})});
- try{
- if((await request({action:'status'})).version!=='confirmed-ids-v1')throw Error('wrong_appointment_deletion_gateway');
- if(!sourceRunId){
- const first=await request({action:'source',page:1});
- if(!Number.isSafeInteger(first.total)||first.total<1||first.pages!==Math.ceil(first.total/1000))throw Error('incomplete_appointment_snapshot');
- for(let page=2;page<=first.pages;page++){const next=await request({action:'source',page});if(next.total!==first.total||next.pages!==first.pages)throw Error('source_shifted_restart_dataset');onProgress({phase:'source',page,pages:first.pages});}
- }
- let after=0;
- for(let batch=0;batch<10000;batch++){
- const result=await request({action:'reconcile',after,dryRun});
- for(const key of ['scanned','present','cancelled','wouldCancel','conflicts'])report[key]+=Number(result[key]||0);
- await save();onProgress({phase:'reconcile',scanned:report.scanned,cancelled:report.cancelled,conflicts:report.conflicts});
- if(!result.hasMore){report.status=dryRun?'preview':report.conflicts?'completed-with-preserved-conflicts':'completed';return report;}
- if(!Number.isSafeInteger(result.after)||result.after<=after)throw Error('appointment_deletion_cursor_stalled');after=result.after;
- }
- throw Error('appointment_deletion_batch_limit');
- }catch(error){report.status='blocked';report.reason=String(error.message||'appointment_deletion_failed').replace(/r:[A-Za-z0-9]+/g,'[redacted]');throw error;}
- finally{report.finishedAt=new Date().toISOString();await save();}
- }
- if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url)){
- reconcileAppointmentDeletions(process.env,{dryRun:!process.argv.includes('--apply'),onProgress:value=>console.log(JSON.stringify(value))}).then(value=>console.log(JSON.stringify(value))).catch(()=>{console.error('旧预约删除核对未完成,请查看受保护的运行报告');process.exitCode=1;});
- }
|