sync-alerts.mjs 1.9 KB

1234567891011121314151617181920212223
  1. import { readFile, writeFile, rename } from 'node:fs/promises';
  2. import { join } from 'node:path';
  3. import { execFile } from 'node:child_process';
  4. import { promisify } from 'node:util';
  5. const execute=promisify(execFile);
  6. export async function macNotify(message) {
  7. if(process.platform!=='darwin')throw Error('notification_platform_unavailable');
  8. await execute('/usr/bin/osascript',['-e','on run argv\ndisplay notification (item 1 of argv) with title "小树夜间同步"\nend run',message],{timeout:10000});
  9. }
  10. // Only fixed text and aggregate counts are shown; raw errors and credentials never enter notifications.
  11. export async function syncAlert(report,root,notify=macNotify,now=Date.now()) {
  12. const path=join(root,'direct-alert-state.json');let prior={};try{prior=JSON.parse(await readFile(path,'utf8'));}catch(e){if(e.code!=='ENOENT')throw e;}
  13. const healthy=['completed','completed-with-preserved-conflicts','already_completed'].includes(report.status);
  14. if(!healthy&&report.status!=='blocked')return;
  15. const count=Number(report.conflicts??report.previous?.conflicts??0),growth=healthy&&Number.isFinite(prior.conflicts)&&count>=prior.conflicts+Math.max(100,Math.ceil(prior.conflicts*.25));
  16. let kind=!healthy?'failure':prior.failed?'recovered':growth?'conflicts':'';
  17. const key=kind==='failure'?'failure':kind==='conflicts'?'conflicts:'+count:kind;
  18. const due=kind&&(prior.lastKey!==key||now-Number(prior.notifiedAt||0)>=24*3600000);
  19. const next={...prior,failed:!healthy,...(healthy?{conflicts:count,lastSuccessAt:now}:{})};
  20. if(due){await notify(kind==='failure'?'今晚的数据同步未完成,系统会重试。请查看同步报告并检查登录凭据。':kind==='recovered'?'数据同步已恢复,请查看本次报告。':'同步保留差异明显增加('+count+' 条),请在后台核对。');next.lastKey=key;next.notifiedAt=now;}
  21. await writeFile(path+'.tmp',JSON.stringify(next),{mode:0o600});await rename(path+'.tmp',path);
  22. return {notified:Boolean(due),kind};
  23. }