diagnostics.ts 2.5 KB

123456789101112131415161718192021222324252627
  1. import { ErrorHandler, Injectable } from '@angular/core';
  2. import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
  3. import { catchError, throwError } from 'rxjs';
  4. export const DIAGNOSTIC_VERSION = '2026-10-13-local-review-v1';
  5. interface Diagnostic {time:string;version:string;kind:'http'|'runtime';requestId:string;status:number;}
  6. const key='xiaoshu.safe-diagnostics.v1';
  7. export function recordDiagnostic(kind:Diagnostic['kind'],requestId='',status=0):void {
  8. try {const previous=JSON.parse(sessionStorage.getItem(key)||'[]');const rows=Array.isArray(previous)?previous.slice(-49):[];
  9. rows.push({time:new Date().toISOString(),version:DIAGNOSTIC_VERSION,kind,requestId:/^[a-z0-9-]{1,64}$/i.test(requestId)?requestId:'',status:Number(status)||0});sessionStorage.setItem(key,JSON.stringify(rows));
  10. }catch{/* Diagnostics must not break the original interaction. */}
  11. }
  12. export function downloadDiagnostics():void {
  13. const blob=new Blob([sessionStorage.getItem(key)||'[]'],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='xiaoshu-diagnostics.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
  14. }
  15. @Injectable()
  16. export class SafeErrorHandler implements ErrorHandler {
  17. handleError(_error:unknown):void {recordDiagnostic('runtime');if(document.getElementById('runtime-notice'))return;
  18. const box=document.createElement('aside');box.id='runtime-notice';box.setAttribute('role','alert');box.style.cssText='position:fixed;bottom:16px;left:16px;right:16px;z-index:99999;background:#fff4dc;color:#573b00;padding:16px;border:1px solid #d9b45b;border-radius:12px';
  19. const text=document.createElement('span');text.textContent='页面遇到问题,请刷新后重试。';const button=document.createElement('button');button.textContent='下载诊断';button.onclick=downloadDiagnostics;const close=document.createElement('button');close.textContent='关闭提示';close.onclick=()=>box.remove();box.append(text,button,close);document.body.append(box);
  20. }
  21. }
  22. export const diagnosticInterceptor:HttpInterceptorFn=(request,next)=>{
  23. const id=crypto.randomUUID(),body=request.body;
  24. // Carry only the correlation ID to our gateways; do not add CORS headers or log request bodies.
  25. const correlated=body&&typeof body==='object'&&'params' in body&&body.params&&typeof body.params==='object'?request.clone({body:{...body,params:{...body.params,requestId:id}}}):request;
  26. return next(correlated).pipe(catchError(error=>{recordDiagnostic('http',id,error instanceof HttpErrorResponse?error.status:0);return throwError(()=>error);}));
  27. };