#!/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;}); }