mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
fix: include actionable error details
This commit is contained in:
@@ -12,7 +12,7 @@ export async function authenticateAgent(
|
||||
): Promise<{ nodeId: string } | NextResponse> {
|
||||
const authHeader = req.headers.get("authorization") || "";
|
||||
if (!authHeader.startsWith("Bearer ")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
return NextResponse.json({ error: "认证失败:请求头缺少 Bearer Token" }, { status: 401 });
|
||||
}
|
||||
const token = authHeader.slice(7);
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function authenticateAgent(
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
return NextResponse.json({ error: "认证失败:Token 无效、已撤销或节点未配置探测 Token" }, { status: 401 });
|
||||
}
|
||||
|
||||
/** Type guard: true when authenticateAgent returned an error response */
|
||||
|
||||
@@ -5,7 +5,7 @@ const ALGORITHM = "aes-256-gcm";
|
||||
function getKey() {
|
||||
const raw = process.env.ENCRYPTION_KEY;
|
||||
if (!raw || Buffer.byteLength(raw, "utf-8") < 32) {
|
||||
throw new Error("ENCRYPTION_KEY must be at least 32 bytes");
|
||||
throw new Error("加密配置错误:ENCRYPTION_KEY 未配置或长度不足 32 字节");
|
||||
}
|
||||
return Buffer.from(raw, "utf-8").subarray(0, 32);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function encrypt(text: string): string {
|
||||
export function decrypt(data: string): string {
|
||||
const parts = data.split(":");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid encrypted data format");
|
||||
throw new Error("解密失败:加密数据格式不正确,期望 iv:authTag:ciphertext 三段内容");
|
||||
}
|
||||
const [ivHex, authTagHex, encryptedHex] = parts;
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), Buffer.from(ivHex, "hex"));
|
||||
|
||||
@@ -1,14 +1,94 @@
|
||||
type ErrorLikeRecord = Record<string, unknown>;
|
||||
|
||||
const REDACTED_SERVER_COMPONENT_ERROR = /An error occurred in the Server Components render/i;
|
||||
const DETAIL_KEYS = ["error", "message", "detail", "details", "reason", "description"];
|
||||
const CODE_KEYS = ["code", "status", "statusCode", "digest"];
|
||||
|
||||
function normalizeMessage(message: string): string | null {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
if (REDACTED_SERVER_COMPONENT_ERROR.test(trimmed)) {
|
||||
return "服务端渲染时发生异常,生产环境已隐藏原始堆栈";
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is ErrorLikeRecord {
|
||||
return value != null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function pushUnique(messages: string[], value: unknown) {
|
||||
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return;
|
||||
const normalized = normalizeMessage(String(value));
|
||||
if (normalized && !messages.includes(normalized)) messages.push(normalized);
|
||||
}
|
||||
|
||||
function collectFromArray(messages: string[], value: unknown) {
|
||||
if (!Array.isArray(value)) return;
|
||||
|
||||
for (const item of value) {
|
||||
if (typeof item === "string") {
|
||||
pushUnique(messages, item);
|
||||
continue;
|
||||
}
|
||||
if (!isRecord(item)) continue;
|
||||
const path = Array.isArray(item.path) ? item.path.join(".") : null;
|
||||
const message = typeof item.message === "string" ? item.message : null;
|
||||
if (message) pushUnique(messages, path ? `${path}:${message}` : message);
|
||||
}
|
||||
}
|
||||
|
||||
function collectErrorMessages(error: unknown, messages: string[], seen = new WeakSet<object>()) {
|
||||
if (error instanceof Error) {
|
||||
pushUnique(messages, error.message);
|
||||
const digest = (error as Error & { digest?: unknown }).digest;
|
||||
if (digest) pushUnique(messages, `错误编号:${String(digest)}`);
|
||||
if (error.cause) collectErrorMessages(error.cause, messages, seen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
pushUnique(messages, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecord(error)) return;
|
||||
if (seen.has(error)) return;
|
||||
seen.add(error);
|
||||
|
||||
for (const key of DETAIL_KEYS) {
|
||||
const value = error[key];
|
||||
if (typeof value === "string") {
|
||||
pushUnique(messages, value);
|
||||
} else if (Array.isArray(value)) {
|
||||
collectFromArray(messages, value);
|
||||
} else if (isRecord(value)) {
|
||||
collectErrorMessages(value, messages, seen);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of CODE_KEYS) {
|
||||
const value = error[key];
|
||||
if (value == null || value === "") continue;
|
||||
const label = key === "digest" ? "错误编号" : key;
|
||||
pushUnique(messages, `${label}:${String(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage(
|
||||
error: unknown,
|
||||
fallback = "操作失败",
|
||||
): string {
|
||||
if (error instanceof Error && error.message.trim()) {
|
||||
return error.message.trim();
|
||||
const messages: string[] = [];
|
||||
collectErrorMessages(error, messages);
|
||||
|
||||
if (messages.length > 0) {
|
||||
return messages.join(";");
|
||||
}
|
||||
|
||||
if (typeof error === "string" && error.trim()) {
|
||||
return error.trim();
|
||||
}
|
||||
|
||||
return fallback;
|
||||
const fallbackMessage = normalizeMessage(fallback) ?? "操作失败";
|
||||
const errorType = error === null ? "null" : typeof error;
|
||||
return `${fallbackMessage}:请求没有返回可读错误内容(错误类型:${errorType}),请查看服务端日志定位原因。`;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ function extractApiError(payload: unknown): string | null {
|
||||
}
|
||||
|
||||
const error = (payload as { error?: unknown }).error;
|
||||
return typeof error === "string" && error.trim() ? error.trim() : null;
|
||||
if (typeof error === "string" && error.trim()) return error.trim();
|
||||
return getErrorMessage(payload, "请求失败");
|
||||
}
|
||||
|
||||
export async function fetchJson<T>(
|
||||
|
||||
Reference in New Issue
Block a user