import type { AdminOperator, AdminOperatorCreateRequest, AdminOperatorProvisioning, AdminSecuritySummary, AdminHintPack, HintFeedGenerationResponse, HintFeedGenerationStatus, HintFeedSettings, AdminLoginResponse, AuditQuery, AuditLogEntry, CreditGrantRequest, CreditGrantResponse, LedgerQuery, LedgerEntry, ManagedProviderId, ManagedProviderOverview, ManagedProviderStatus, OperatorsQuery, Overview, PageResult, ProductAnalyticsOverview, CreateOfficialSkillRequest, OfficialSkill, OfficialSkillCatalog, ReferralsQuery, ReferralOverview, SessionResponse, UserDetail, UsersQuery, UserSummary, UpdateHintPackRequest, UpdateHintFeedSettingsRequest, UpdateOfficialSkillRequest, UpdateProviderApiKeyRequest, } from "./types"; const API_BASE = "/v1/admin"; const REQUEST_TIMEOUT_MS = 12_000; interface ApiErrorBody { code?: string; error?: { code?: string; }; } export class ApiError extends Error { constructor( public readonly code: string, message: string, public readonly status: number, ) { super(message); this.name = "ApiError"; } } let csrfToken = ""; export function setCsrfToken(token?: string): void { csrfToken = token?.trim() ?? ""; } function csrfTokenFromCookie(): string { if (typeof document === "undefined") return ""; const prefix = "osg_admin_csrf="; return ( document.cookie .split(";") .map((part) => part.trim()) .find((part) => part.startsWith(prefix)) ?.slice(prefix.length) ?? "" ); } function safeMessage(status: number, code?: string): string { const messages: Record = { INVALID_CREDENTIALS: "用户名、密码或动态验证码错误", INVALID_TOTP: "动态验证码无效或已过期", VALIDATION_ERROR: "提交内容不符合要求", USER_NOT_FOUND: "未找到该用户", INSUFFICIENT_PERMISSION: "无权执行此操作", IDEMPOTENCY_CONFLICT: "该赠送请求与已有记录冲突,请核对审计日志", ADMIN_OPERATOR_NOT_FOUND: "未找到该管理员", ADMIN_USERNAME_CONFLICT: "该管理员用户名已存在", CANNOT_DISABLE_SELF: "不能停用当前登录的管理员", LAST_SUPER_ADMIN_REQUIRED: "必须至少保留一名启用的超级管理员", CONTENT_SKILL_NOT_FOUND: "未找到该官方 Skill", CONTENT_SKILL_CONFLICT: "该官方 Skill ID 已存在", CONTENT_HINT_PACK_NOT_FOUND: "该语言的 Hint pack 尚未发布", HINT_FEED_GENERATION_IN_PROGRESS: "Hint 提示包正在生成,请稍后刷新", HINT_FEED_SETTINGS_INVALID: "Hint 自动生成配置不符合要求", HINT_FEED_GENERATION_FAILED: "Hint 提示包生成失败,旧版本仍保持可用", PROVIDER_API_KEY_INVALID: "API Key 不符合要求", PROVIDER_NOT_FOUND: "不支持该 Provider", RATE_LIMITED: "操作过于频繁,请稍后再试", }; if (code && messages[code]) return messages[code]; if (status === 401) return "登录状态已失效,请重新登录"; if (status === 403) return "无权执行此操作"; if (status === 409) return "请求冲突,请刷新后重试"; if (status === 429) return "操作过于频繁,请稍后再试"; if (status >= 500) return "服务暂时不可用,请稍后再试"; return "请求失败,请检查后重试"; } async function request( path: string, options: RequestInit = {}, ): Promise { const method = options.method?.toUpperCase() ?? "GET"; const headers = new Headers(options.headers); headers.set("Accept", "application/json"); if (options.body) headers.set("Content-Type", "application/json"); const csrfRequired = !["GET", "HEAD", "OPTIONS"].includes(method) && path !== "/auth/login"; if (csrfRequired) { const requestToken = csrfToken || csrfTokenFromCookie() || headers.get("X-CSRF-Token") || ""; if (!requestToken) { throw new ApiError("CSRF_TOKEN_MISSING", "安全令牌缺失,请重新登录", 403); } headers.set("X-CSRF-Token", requestToken); } try { const response = await fetch(`${API_BASE}${path}`, { ...options, headers, credentials: "include", signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); if (!response.ok) { let body: ApiErrorBody = {}; try { body = (await response.json()) as ApiErrorBody; } catch { // 非 JSON 错误响应不会暴露给界面。 } const apiError = new ApiError( body.code ?? body.error?.code ?? `HTTP_${response.status}`, safeMessage(response.status, body.code ?? body.error?.code), response.status, ); if ( response.status === 401 && !path.startsWith("/auth/") && typeof window !== "undefined" ) { window.dispatchEvent(new Event("admin:unauthorized")); } throw apiError; } if (response.status === 204) return undefined as T; return (await response.json()) as T; } catch (error) { if (error instanceof ApiError) throw error; if (error instanceof DOMException && error.name === "TimeoutError") { throw new ApiError("REQUEST_TIMEOUT", "请求超时,请稍后再试", 0); } throw new ApiError("NETWORK_ERROR", "网络连接失败,请检查网络", 0); } } export function encodeQuery(params: T): string { const search = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== "") search.set(key, String(value)); }); const result = search.toString(); return result ? `?${result}` : ""; } function cursorQuery(value?: string | T): CursorQuery | T { return typeof value === "string" ? { cursor: value } : (value ?? {}); } type CursorQuery = { cursor?: string }; function ledgerQuery(value?: string | LedgerQuery): CursorQuery | LedgerQuery { if (typeof value === "string") return { cursor: value }; return value ? { ...value, referenceId: value.referenceId?.trim() } : {}; } export const adminApi = { session: () => request("/auth/session"), login: (username: string, password: string, totpCode: string) => request("/auth/login", { method: "POST", body: JSON.stringify({ username, password, totpCode }), }), logout: () => request("/auth/logout", { method: "POST" }), overview: (range: string) => request(`/overview${encodeQuery({ range })}`), referrals: (value: string | ReferralsQuery) => { const params = typeof value === "string" ? { range: value } : value; return request(`/referrals${encodeQuery(params)}`); }, productAnalytics: (range: string) => request(`/analytics${encodeQuery({ range })}`), contentSkills: () => request("/content/skills"), createContentSkill: (payload: CreateOfficialSkillRequest) => request("/content/skills", { method: "POST", body: JSON.stringify(payload), }), updateContentSkill: (id: string, payload: UpdateOfficialSkillRequest) => request(`/content/skills/${encodeURIComponent(id)}`, { method: "PUT", body: JSON.stringify(payload), }), setContentSkillEnabled: (id: string, enabled: boolean) => request( `/content/skills/${encodeURIComponent(id)}/${enabled ? "enable" : "disable"}`, { method: "POST" }, ), contentHintPack: (locale: "zh" | "en") => request(`/content/hints/${locale}`), updateContentHintPack: ( locale: "zh" | "en", payload: UpdateHintPackRequest, ) => request(`/content/hints/${locale}`, { method: "PUT", body: JSON.stringify(payload), }), hintFeedSettings: () => request("/content/hints/generation/settings"), updateHintFeedSettings: (payload: UpdateHintFeedSettingsRequest) => request("/content/hints/generation/settings", { method: "PUT", body: JSON.stringify(payload), }), hintFeedStatus: () => request("/content/hints/generation/status"), regenerateHintFeed: () => request("/content/hints/generation/regenerate", { method: "POST", signal: AbortSignal.timeout(130_000), }), users: (value: string | UsersQuery = "", legacyCursor?: string) => { const params = typeof value === "string" ? { q: value.trim(), cursor: legacyCursor } : { ...value, q: value.q?.trim() }; return request>(`/users${encodeQuery(params)}`); }, user: (userId: string) => request(`/users/${encodeURIComponent(userId)}`), latestLedger: (value?: string | LedgerQuery) => request>( `/credits/ledger${encodeQuery(ledgerQuery(value))}`, ), ledger: (userId: string, value?: string | LedgerQuery) => request>( `/users/${encodeURIComponent(userId)}/ledger${encodeQuery(ledgerQuery(value))}`, ), grantCredits: (payload: CreditGrantRequest) => request("/credits/grants", { method: "POST", body: JSON.stringify({ userId: payload.userId, amount: payload.amount, reason: payload.reason, }), headers: { "Idempotency-Key": payload.idempotencyKey }, }), providers: () => request("/providers"), updateProviderApiKey: ( providerId: ManagedProviderId, payload: UpdateProviderApiKeyRequest, ) => request( `/providers/${encodeURIComponent(providerId)}/api-key`, { method: "PUT", body: JSON.stringify(payload), }, ), auditLogs: (value?: string | AuditQuery) => request>( `/audit${encodeQuery(cursorQuery(value))}`, ), operators: (value?: string | OperatorsQuery) => request>( `/operators${encodeQuery(cursorQuery(value))}`, ), operatorSummary: () => request("/operators/summary"), createOperator: (payload: AdminOperatorCreateRequest) => request("/operators", { method: "POST", body: JSON.stringify(payload), }), setOperatorEnabled: (operatorId: string, enabled: boolean) => request( `/operators/${encodeURIComponent(operatorId)}/${enabled ? "enable" : "disable"}`, { method: "POST" }, ), unlockOperator: (operatorId: string) => request(`/operators/${encodeURIComponent(operatorId)}/unlock`, { method: "POST", }), resetOperatorCredentials: (operatorId: string, password: string) => request( `/operators/${encodeURIComponent(operatorId)}/credentials/reset`, { method: "POST", body: JSON.stringify({ password }), }, ), revokeOperatorSessions: (operatorId: string) => request( `/operators/${encodeURIComponent(operatorId)}/sessions/revoke`, { method: "POST" }, ), };