Files
ttstd-infinite-canvas/web/src/services/diagnostics/diagnostics-api.ts

116 lines
4.1 KiB
TypeScript

import { http, ApiError } from "@/services/api/request";
import type { ClientDiagnosticEvent } from "./client-diagnostics";
export type DiagnosticExportInput = {
from: string;
to: string;
taskId?: string;
projectId?: string;
description?: string;
runtime: {
appVersion?: string;
buildCommit?: string;
buildTime?: string;
browser?: string;
os?: string;
timezone?: string;
};
clientEvents: ClientDiagnosticEvent[];
};
export type DiagnosticPreview = {
clientEventLimit: number;
taskCount: number;
taskLogCount: number;
apiCallCount: number;
estimatedBytes: number;
willTruncate: boolean;
};
export type DiagnosticDownload = {
blob: Blob;
bundleId: string;
fileName: string;
};
export function previewDiagnosticBundle(input: DiagnosticExportInput) {
return http.post<DiagnosticPreview>("/diagnostics/preview", input);
}
export async function exportDiagnosticBundle(input: DiagnosticExportInput): Promise<DiagnosticDownload> {
try {
const response = await http.raw<Blob>({ method: "post", url: "/diagnostics/export", data: input, responseType: "blob" });
return {
blob: response.data,
bundleId: readHeader(response.headers, "x-diagnostic-bundle-id") || "",
fileName: parseFileName(readHeader(response.headers, "content-disposition")) || "yingce-diagnostics.zip",
};
} catch (error) {
if (error instanceof ApiError) {
const status = error.status;
const raw = await readErrorBody(responseDataFromCause(error));
try {
const payload = raw ? (JSON.parse(raw) as { code?: number; msg?: string }) : null;
if (payload) {
const message = status === 404 ? "诊断接口未部署,请重启或重新构建后端" : payload.msg || "导出诊断包失败";
throw new ApiError(message, { status, code: payload.code, cause: error });
}
} catch (parseError) {
if (parseError instanceof ApiError) throw parseError;
}
if (status === 404) {
throw new ApiError("诊断接口未部署,请重启或重新构建后端", { status, cause: error });
}
if (status === 502 || status === 503 || status === 504) {
throw new ApiError("后端服务暂时不可用,请重启后端后重试", { status, cause: error });
}
}
throw error;
}
}
function responseDataFromCause(error: ApiError): unknown {
const cause = error.cause;
if (cause && typeof cause === "object" && "response" in cause) {
return (cause as { response?: { data?: unknown } }).response?.data;
}
return undefined;
}
async function readErrorBody(data: unknown) {
if (data instanceof Blob) return data.text();
return typeof data === "string" ? data : "";
}
export function downloadDiagnosticBundle(download: DiagnosticDownload) {
const url = URL.createObjectURL(download.blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = download.fileName;
anchor.style.display = "none";
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 0);
}
function readHeader(headers: unknown, name: string) {
if (!headers || typeof headers !== "object") return "";
const value = headers as { get?: (key: string) => string | null; [key: string]: unknown };
if (typeof value.get === "function") return value.get(name) || "";
const direct = value[name] ?? value[name.toLowerCase()];
return typeof direct === "string" ? direct : "";
}
function parseFileName(contentDisposition: string) {
const encoded = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
if (encoded) {
try {
return decodeURIComponent(encoded.replace(/^"|"$/g, ""));
} catch {
return encoded;
}
}
return contentDisposition.match(/filename="?([^";]+)"?/i)?.[1] || "";
}