lesson-export-worker.mjs 3.1 KB

12345678910111213141516171819202122232425262728293031
  1. #!/usr/bin/env node
  2. import {readFileSync} from 'node:fs';
  3. import {setPriority} from 'node:os';
  4. import {pathToFileURL} from 'node:url';
  5. import * as XLSX from 'xlsx';
  6. import {lessonExportColumns} from '../projects/shared/lesson-export-columns.mjs';
  7. export async function processLessonExport(call){
  8. const job=await call('claim',{});if(!job.jobId)return false;
  9. const identity={jobId:job.jobId,lease:job.lease};
  10. try{
  11. if(!job.snapshotReady)await call('snapshot',identity);
  12. const sheet=XLSX.utils.json_to_sheet([]);let offset=0,total=Infinity;
  13. while(offset<total){const page=await call('chunk',{...identity,offset});total=page.total;if(!page.items.length&&offset<total)throw Error('incomplete_snapshot');
  14. XLSX.utils.sheet_add_json(sheet,page.items.map(lessonExportColumns),{origin:offset===0?0:-1,skipHeader:offset!==0});offset+=page.items.length;
  15. }
  16. const book=XLSX.utils.book_new();XLSX.utils.book_append_sheet(book,sheet,'训练记录');
  17. const base64=XLSX.write(book,{type:'base64',bookType:'xlsx',compression:true});
  18. await call('finish',{...identity,rowCount:offset,base64});return true;
  19. }catch(e){await call(e?.transient?'release':'fail',identity).catch(()=>{});throw Object.assign(Error('export_job_failed'),{transient:e?.transient===true});}
  20. }
  21. async function main(){
  22. try{setPriority(0,10);}catch{} // Background XLSX compression yields CPU to interactive requests.
  23. const config=process.env.XIAOSHU_EXPORT_ENV_FILE||'/Users/mac/.config/xiaoshu/legacy-sync-nightly.env',env={...process.env};
  24. for(const line of readFileSync(config,'utf8').split('\n')){const m=line.match(/^(?:export\s+)?(XIAOSHU_[A-Z0-9_]+)=(.*)$/);if(m&&!env[m[1]])env[m[1]]=m[2].replace(/^['"]|['"]$/g,'');}
  25. const token=env.XIAOSHU_EXPORT_OPERATOR_TOKEN||env.XIAOSHU_SYNC_OPERATOR_TOKEN;if(!token)throw Error('export_operator_missing');
  26. const base=env.XIAOSHU_EXPORT_GATEWAY_URL||'https://server.xiaoshu.pro/api/functions/xiaoshu/ops/gateway-v3';
  27. const call=async(action,params)=>{let r;try{r=await fetch(base,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{operation:'ops/lessons/export/'+action,...params}}),signal:AbortSignal.timeout(action==='snapshot'?90_000:20_000)});}catch{throw Object.assign(Error('export_transport'),{transient:true});}let data;try{data=await r.json();}catch{throw Object.assign(Error('export_gateway_'+r.status),{transient:true});}if(!r.ok||!data.success)throw Object.assign(Error('export_gateway_'+r.status),{transient:[429,500,502,503,504].includes(r.status)});return data.data;};
  28. let lastMaintenance=0;let stopped=false;for(const event of ['SIGINT','SIGTERM'])process.on(event,()=>{stopped=true;});
  29. do{try{if(Date.now()-lastMaintenance>86_400_000){await call('maintenance',{});lastMaintenance=Date.now();}await processLessonExport(call);}catch{console.error('导出任务失败,错误状态已回写或将由租约恢复');}if(process.argv.includes('--once'))break;if(!stopped)await new Promise(r=>setTimeout(r,5000));}while(!stopped);
  30. }
  31. if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href)main().catch(()=>{console.error('导出 worker 启动失败,请检查私密配置和权限');process.exitCode=1;});