import { ErrorHandler, Injectable } from '@angular/core'; import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http'; import { catchError, throwError } from 'rxjs'; export const DIAGNOSTIC_VERSION = '2026-10-13-local-review-v1'; interface Diagnostic {time:string;version:string;kind:'http'|'runtime';requestId:string;status:number;} const key='xiaoshu.safe-diagnostics.v1'; export function recordDiagnostic(kind:Diagnostic['kind'],requestId='',status=0):void { try {const previous=JSON.parse(sessionStorage.getItem(key)||'[]');const rows=Array.isArray(previous)?previous.slice(-49):[]; 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)); }catch{/* Diagnostics must not break the original interaction. */} } export function downloadDiagnostics():void { 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); } @Injectable() export class SafeErrorHandler implements ErrorHandler { handleError(_error:unknown):void {recordDiagnostic('runtime');if(document.getElementById('runtime-notice'))return; 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'; 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); } } export const diagnosticInterceptor:HttpInterceptorFn=(request,next)=>{ const id=crypto.randomUUID(),body=request.body; // Carry only the correlation ID to our gateways; do not add CORS headers or log request bodies. 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; return next(correlated).pipe(catchError(error=>{recordDiagnostic('http',id,error instanceof HttpErrorResponse?error.status:0);return throwError(()=>error);})); };