export type UploadProgress = { loaded: number; total: number; }; export type AdminApiOptions = { csrf?: string; }; const exactMessages: Record = { "current password is invalid": "当前密码不正确", "new password is required": "新密码不能为空", "new password must be at least 8 characters": "新密码至少需要 8 位", "new password cannot be admin": "新密码不能为 admin", "new password must be different from current password": "新密码不能与当前密码相同", "invalid password or captcha": "密码或验证码不正确", "login required": "需要登录后继续操作", "csrf token required": "页面安全令牌已失效,请刷新后重试", "csrf token invalid": "页面安全令牌无效,请刷新后重试", "code is required": "缺少反馈编号", "revisionid is required": "请选择要恢复的历史版本", "post required": "该操作需要使用 POST 请求", "get required": "该操作需要使用 GET 请求", "file is required": "请选择要上传的文件", "invalid filename": "文件名不合法", "path escape rejected": "文件路径不合法", "check job not found": "未找到心跳检测任务", "streaming is not supported": "当前运行环境不支持实时事件流", }; const codeMessages: Record = { UNAUTHORIZED: "需要登录后继续操作", LOGIN_FAILED: "登录失败,请检查密码和验证码", PASSWORD_CHANGE_FAILED: "密码修改失败", INVALID_PAYLOAD: "提交内容格式不正确", DATABASE_TEST_FAILED: "数据库连接测试失败", DATABASE_IMPORT_FAILED: "SQLite 导入远端库失败", DATABASE_SYNC_FAILED: "远端库同步回本地失败", LEGACY_SAVE_FAILED: "兼容 JSON 保存失败", LEGACY_VALIDATE_FAILED: "兼容 JSON 校验失败", LEGACY_RESTORE_FAILED: "兼容 JSON 恢复失败", NOTICE_SAVE_FAILED: "版本日志保存失败", NOTICE_VALIDATE_FAILED: "版本日志校验失败", NOTICE_RESTORE_FAILED: "版本日志恢复失败", PACKAGE_UPLOAD_FAILED: "发布包上传失败", SOURCE_SAVE_FAILED: "接口源保存失败", CHECK_FAILED: "接口健康检测失败", SYNC_FAILED: "同步操作失败", FORBIDDEN: "没有权限执行该操作", METHOD_NOT_ALLOWED: "请求方法不正确", }; export async function adminFetch(target: string, init: RequestInit = {}, options: AdminApiOptions = {}): Promise { const headers = new Headers(init.headers); if (!headers.has("Content-Type") && init.body && !(init.body instanceof FormData)) { headers.set("Content-Type", "application/json"); } if (options.csrf) headers.set("X-CSRF-Token", options.csrf); const res = await fetch(target, { ...init, headers, credentials: "include" }); const data = await res.json().catch(() => ({})); if (!res.ok || data.ok === false) { throw new Error(toChineseError(data.message || data.error || `HTTP ${res.status}`)); } return data as T; } export function uploadAdminFile(target: string, form: FormData, options: AdminApiOptions, onProgress: (progress: UploadProgress) => void): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("POST", target); xhr.withCredentials = true; if (options.csrf) xhr.setRequestHeader("X-CSRF-Token", options.csrf); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress({ loaded: event.loaded, total: event.total }); }; xhr.onload = () => { const data = parseJSONSafe(xhr.responseText, {}); if (xhr.status < 200 || xhr.status >= 300 || data.ok === false) { reject(new Error(toChineseError(data.message || data.error || `HTTP ${xhr.status}`))); return; } resolve(data as T); }; xhr.onerror = () => reject(new Error("网络异常,发布包上传失败")); xhr.onabort = () => reject(new Error("发布包上传已取消")); xhr.send(form); }); } export function toChineseError(value: string) { const raw = String(value || "").trim(); const lower = raw.toLowerCase(); if (exactMessages[lower]) return exactMessages[lower]; if (codeMessages[raw]) return codeMessages[raw]; if (/^HTTP\s+\d+/.test(raw)) return `请求失败:${raw}`; return raw || "操作失败"; } function parseJSONSafe(value: string, fallback: any) { try { return JSON.parse(value || "{}"); } catch { return fallback; } }