import type { AITask, Asset, AuthPayload, BillingSummary, BillingTrend, Ledger, LoginSession, ModelConfig, Notification, NotificationList, Paginated, Product, Project, RechargeResult, ScriptVersion, Team, TeamMember, User, UserPreference } from "./types"; const API_BASE = import.meta.env.VITE_API_BASE_URL || ""; const TOKEN_KEY = "airshelf_token"; export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.name = "ApiError"; this.status = status; } } export function getToken() { return localStorage.getItem(TOKEN_KEY); } export function setToken(token: string | null) { if (token) localStorage.setItem(TOKEN_KEY, token); else localStorage.removeItem(TOKEN_KEY); } async function request(path: string, options: RequestInit = {}): Promise { const token = getToken(); const headers = new Headers(options.headers); if (!(options.body instanceof FormData)) { headers.set("Content-Type", "application/json"); } if (token) headers.set("Authorization", `Token ${token}`); const response = await fetch(`${API_BASE}${path}`, { ...options, headers }); if (!response.ok) { const text = await response.text(); throw new ApiError(response.status, text || `${response.status} ${response.statusText}`); } if (response.status === 204) return undefined as T; return response.json() as Promise; } export const api = { register(payload: { username: string; password: string; email?: string; team_name?: string }) { return request("/api/auth/register/", { method: "POST", body: JSON.stringify(payload) }); }, login(payload: { username: string; password: string }) { return request("/api/auth/login/", { method: "POST", body: JSON.stringify(payload) }); }, me() { return request<{ user: User; team: Team }>("/api/auth/me/"); }, updateProfile(payload: { name?: string; phone?: string; email?: string }) { return request<{ user: User; team: Team }>("/api/auth/me/", { method: "PATCH", body: JSON.stringify(payload) }); }, changePassword(payload: { old_password: string; new_password: string }) { return request<{ token: string }>("/api/auth/me/password/", { method: "POST", body: JSON.stringify(payload) }); }, uploadAvatar(formData: FormData) { return request("/api/auth/me/avatar/", { method: "POST", body: formData }); }, resetAvatar() { return request("/api/auth/me/avatar/", { method: "DELETE" }); }, deleteAsset(id: string) { return request(`/api/assets/${id}/`, { method: "DELETE" }); }, preferences() { return request("/api/auth/me/preferences/"); }, updatePreferences(payload: Partial) { return request("/api/auth/me/preferences/", { method: "PUT", body: JSON.stringify(payload) }); }, loginSessions() { return request("/api/auth/me/sessions/"); }, revokeSession(id: string) { return request<{ revoked: number }>(`/api/auth/me/sessions/${id}/revoke/`, { method: "POST" }); }, revokeOtherSessions() { return request<{ token: string }>("/api/auth/me/sessions/revoke-others/", { method: "POST" }); }, logout() { return request("/api/auth/logout/", { method: "POST" }); }, teamMembers() { return request("/api/auth/team/members/"); }, createTeamMember(payload: { username: string; password: string; name?: string; email?: string; role?: string; monthly_credit_limit?: number | string; }) { return request("/api/auth/team/members/", { method: "POST", body: JSON.stringify(payload) }); }, updateTeamMember(id: string, payload: { role?: string; monthly_credit_limit?: number | string; name?: string }) { return request(`/api/auth/team/members/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, removeTeamMember(id: string) { return request(`/api/auth/team/members/${id}/`, { method: "DELETE" }); }, resetMemberPassword(id: string, password: string) { return request(`/api/auth/team/members/${id}/password/`, { method: "POST", body: JSON.stringify({ password }) }); }, products() { return request>("/api/products/"); }, product(id: string) { return request(`/api/products/${id}/`); }, createProduct(payload: { title?: string; brand?: string; category?: string; target_audience?: string; description?: string; specs?: Record; selling_points?: Array<{ title: string; detail: string; sort_order: number }>; }) { return request("/api/products/", { method: "POST", body: JSON.stringify(payload) }); }, updateProduct(id: string, payload: Partial) { return request(`/api/products/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, uploadProductImage(productId: string, formData: FormData) { return request(`/api/products/${productId}/images/`, { method: "POST", body: formData }); }, deleteProductImage(productId: string, imageId: string) { return request(`/api/products/${productId}/images/${imageId}/`, { method: "DELETE" }); }, deleteProduct(id: string) { return request(`/api/products/${id}/`, { method: "DELETE" }); }, projects() { return request>("/api/projects/"); }, project(id: string) { return request(`/api/projects/${id}/`); }, createProject(payload: { name: string; product: string; metadata?: Record }) { return request("/api/projects/", { method: "POST", body: JSON.stringify(payload) }); }, deleteProject(id: string) { return request(`/api/projects/${id}/`, { method: "DELETE" }); }, generateScript(projectId: string, payload: { prompt: string; selling_point_ids?: string[] }) { return request(`/api/projects/${projectId}/generate-script/`, { method: "POST", body: JSON.stringify(payload) }); }, adoptScript(projectId: string, script_version_id: string) { return request(`/api/projects/${projectId}/adopt-script/`, { method: "POST", body: JSON.stringify({ script_version_id }) }); }, generateBaseAsset(projectId: string, payload: { kind: "product" | "person" | "scene"; prompt: string }) { return request(`/api/projects/${projectId}/generate-base-asset/`, { method: "POST", body: JSON.stringify(payload) }); }, adoptBaseAsset(projectId: string, payload: { group_id: string; asset_id: string }) { return request(`/api/projects/${projectId}/adopt-base-asset/`, { method: "POST", body: JSON.stringify(payload) }); }, generateStoryboard(projectId: string, payload: { prompt: string }) { return request(`/api/projects/${projectId}/generate-storyboard/`, { method: "POST", body: JSON.stringify(payload) }); }, pollStoryboard(projectId: string) { return request<{ status: "generating" | "succeeded" | "failed"; done: number; total: number; version_id: string; error?: string }>( `/api/projects/${projectId}/poll-storyboard/`, { method: "POST" } ); }, skipStoryboard(projectId: string) { return request(`/api/projects/${projectId}/skip-storyboard/`, { method: "POST" }); }, submitVideo(projectId: string, payload: { video_segment_id: string; prompt: string }) { return request(`/api/projects/${projectId}/submit-video-segment/`, { method: "POST", body: JSON.stringify(payload) }); }, pollVideo(projectId: string, video_segment_id: string) { return request(`/api/projects/${projectId}/poll-video-segment/`, { method: "POST", body: JSON.stringify({ video_segment_id }) }); }, submitExport(projectId: string) { return request(`/api/projects/${projectId}/submit-export/`, { method: "POST" }); }, pollExport(projectId: string) { return request(`/api/projects/${projectId}/poll-export/`, { method: "POST" }); }, uploadVideoSegment(projectId: string, segmentId: string, file: File) { const form = new FormData(); form.append("video_segment_id", segmentId); form.append("file", file); return request(`/api/projects/${projectId}/upload-video-segment/`, { method: "POST", body: form }); }, uploadBgm(projectId: string, file: File, volume?: number) { const form = new FormData(); form.append("file", file); if (volume != null) form.append("volume", String(volume)); return request(`/api/projects/${projectId}/upload-bgm/`, { method: "POST", body: form }); }, saveTimeline(projectId: string, payload: import("./types").TimelineSavePayload) { return request(`/api/projects/${projectId}/save-timeline/`, { method: "POST", body: JSON.stringify(payload) }); }, assets() { return request>("/api/assets/"); }, // 跟随 DRF 分页 next 取全部资产 —— 商品图/AI 素材/资产库都靠 asset.id 在这份列表里查 preview_url, // 只取第 1 页(20 条)会让第 20 条之后的资产解析不到图、渲染成空占位。 async allAssets(): Promise { const out: Asset[] = []; let path = "/api/assets/"; for (let guard = 0; guard < 50 && path; guard += 1) { const page: Paginated = await request>(path); out.push(...page.results); path = page.next ? new URL(page.next).pathname + new URL(page.next).search : ""; } return out; }, uploadAsset(formData: FormData) { return request("/api/assets/upload/", { method: "POST", body: formData }); }, billingSummary() { return request("/api/billing/summary/"); }, ledgers(page = 1, pageSize = 10) { return request<{ count: number; page: number; page_size: number; results: Ledger[] }>(`/api/billing/ledgers/?page=${page}&page_size=${pageSize}`); }, billingTrend(range?: "day" | "week" | "month") { return request(`/api/billing/trend/${range ? `?range=${range}` : ""}`); }, modelConfigs() { return request>("/api/ai/models/"); }, aiTasks() { return request>("/api/ai/tasks/"); }, generateImage(payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number }) { return request<{ assets: Asset[] }>("/api/ai/generate-image/", { method: "POST", body: JSON.stringify(payload) }); }, recharge(payload: { amount: number | string; bonus?: number | string; channel?: string }) { return request("/api/billing/recharge/", { method: "POST", body: JSON.stringify(payload) }); }, // 收件箱按页拉取 —— 滚动加载逐页向后端要(tab/搜索 走服务端,计数随响应回来) listNotifications(params?: { type?: string; unread?: boolean; search?: string; page?: number; pageSize?: number }) { const query = new URLSearchParams(); if (params?.type && params.type !== "all") query.set("type", params.type); if (params?.unread) query.set("unread", "1"); if (params?.search) query.set("search", params.search); if (params?.page) query.set("page", String(params.page)); if (params?.pageSize) query.set("page_size", String(params.pageSize)); const qs = query.toString(); return request(`/api/ops/notifications/${qs ? `?${qs}` : ""}`); }, // 跟着 next 把所有消息取全(给侧边栏徽标 + 团队动态用,不参与收件箱滚动渲染),与 allAssets 同套路 async allNotifications(): Promise { const out: Notification[] = []; let path = "/api/ops/notifications/?page_size=100"; let unreadCount = 0; let typeCounts: NotificationList["type_counts"]; for (let guard = 0; guard < 100 && path; guard += 1) { const page = await request(path); out.push(...page.results); unreadCount = page.unread_count; typeCounts = page.type_counts ?? typeCounts; path = page.next ? new URL(page.next).pathname + new URL(page.next).search : ""; } return { count: out.length, next: null, previous: null, results: out, unread_count: unreadCount, type_counts: typeCounts }; }, markAllNotificationsRead() { return request<{ updated: number; unread_count: number }>("/api/ops/notifications/mark-all-read/", { method: "POST" }); }, markNotificationRead(id: string) { return request(`/api/ops/notifications/${id}/mark-read/`, { method: "POST" }); } };