import type { AdminIntegrity, AdminLedger, AdminModel, AdminProjectRow, AdminProvider, AdminQualityWord, AdminQuotaPolicy, AdminReviewAsset, AdminTask, AdminTaskDetail, AdminTeam, AdminTeamDetail, AdminUser, AITask, Asset, AuthPayload, BillingSummary, BillingTrend, Ledger, LoginSession, Invitation, ModelConfig, ModelEntity, Notification, NotificationList, Paginated, Product, Project, RechargeResult, ScriptVersion, Team, TeamMember, User, UserPreference, VoiceoverInfo } from "./types"; const API_BASE = import.meta.env.VITE_API_BASE_URL || ""; const TOKEN_KEY = "airshelf_token"; const REMEMBER_KEY = "airshelf_remember"; // { username, expireAt } · 勾选「记住我 7 天」时写 const REMEMBER_DAYS = 7; export type RememberInfo = { username: string; expireAt: number }; export function getRemember(): RememberInfo | null { try { const raw = localStorage.getItem(REMEMBER_KEY); return raw ? (JSON.parse(raw) as RememberInfo) : null; } catch { return null; } } // 登录成功后调用:勾选记住我则存用户名 + 7 天过期点;否则清除 export function setRemember(username: string | null, remember: boolean) { if (remember && username) { localStorage.setItem(REMEMBER_KEY, JSON.stringify({ username, expireAt: Date.now() + REMEMBER_DAYS * 86400000 })); } else { localStorage.removeItem(REMEMBER_KEY); } } export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.name = "ApiError"; this.status = status; } } export function getToken() { // 「记住我 7 天」过期则强制重新登录 const remembered = getRemember(); if (remembered && remembered.expireAt && Date.now() > remembered.expireAt) { localStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REMEMBER_KEY); return null; } // 记住我 → localStorage(跨会话);不记住 → sessionStorage(关浏览器即失效) return localStorage.getItem(TOKEN_KEY) || sessionStorage.getItem(TOKEN_KEY); } export function setToken(token: string | null, remember = true) { localStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(TOKEN_KEY); if (!token) return; if (remember) localStorage.setItem(TOKEN_KEY, token); else sessionStorage.setItem(TOKEN_KEY, token); } 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(); // DRF 错误体是 JSON({"detail": "..."} 或 {field: ["..."]}),提取人话给 toast,别把原始 JSON 怼到用户脸上 let message = text || `${response.status} ${response.statusText}`; try { const data = JSON.parse(text) as Record; const first = data.detail ?? data.error ?? data.message ?? Object.values(data)[0]; if (typeof first === "string") message = first; else if (Array.isArray(first) && typeof first[0] === "string") message = first[0]; } catch { // 非 JSON(如 DEBUG 的整页 HTML 报错栈):绝不把整页怼进 toast(会撑爆 / 白屏),只给状态行 message = `服务器错误 ${response.status} ${response.statusText}`.trim(); } throw new ApiError(response.status, message); } 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; invite_code: string }) { return request("/api/auth/register/", { method: "POST", body: JSON.stringify(payload) }); }, // 免登录验码:注册页「填码 → 验过才展开表单」。始终 200,以 valid 标志区分。 validateInvite(code: string) { return request<{ valid: boolean; kind?: "join_team" | "create_team"; role?: string; team_name?: string | null; detail?: string }>( "/api/auth/validate-invite/", { method: "POST", body: JSON.stringify({ code }) } ); }, 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 }) }); }, listInvitations() { return request("/api/auth/team/invitations/"); }, createInvitation(payload: { role?: string; email?: string; monthly_credit_limit?: number | string }) { return request("/api/auth/team/invitations/", { method: "POST", body: JSON.stringify(payload) }); }, revokeInvitation(id: string) { return request(`/api/auth/team/invitations/${id}/revoke/`, { method: "POST" }); }, 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 }>; // upload-first:创建时带上已传进桶的图(cover_asset = 主图 asset id;images = 全部图) cover_asset?: string; images?: Array<{ asset: string; sort_order: number; is_primary?: boolean }>; }) { 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) { // 软删 = 进垃圾桶(后端 status→archived,可恢复) return request(`/api/products/${id}/`, { method: "DELETE" }); }, // 商品详情结构化素材(角色/商品图/图片成品/项目包) productMaterials(id: string) { return request(`/api/products/${id}/materials/`); }, productsTrash() { return request>("/api/products/trash/"); }, restoreProduct(id: string) { return request(`/api/products/${id}/restore/`, { method: "POST" }); }, purgeProduct(id: string) { // 彻底删除(不可恢复) return request(`/api/products/${id}/purge/`, { method: "DELETE" }); }, projects() { return request>("/api/projects/"); }, project(id: string) { return request(`/api/projects/${id}/`); }, // 本项目在途的基础资产出图任务(刷新后据此重建「生成中」占位卡 loading) pendingAssets(id: string) { return request<{ pending: Array<{ id: string; kind: string; label: string; is_triview: boolean; status: string }> }>( `/api/projects/${id}/pending-assets/` ); }, createProject(payload: { name: string; product: string; metadata?: Record }) { return request("/api/projects/", { method: "POST", body: JSON.stringify(payload) }); }, // 整体替换 metadata —— 调用方务必先展开现有 project.metadata 再合并,别把别的 key 冲掉 updateProject(id: string, payload: { name?: string; metadata?: Record }) { return request(`/api/projects/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, deleteProject(id: string) { return request(`/api/projects/${id}/`, { method: "DELETE" }); }, // 对话式脚本 agent · 流式(SSE)。逐帧回调 onEvent:tool(工具卡)/delta(思考前言)/draft/saved/done/error。 // 用 fetch + ReadableStream 消费 text/event-stream(EventSource 只支持 GET,这里要 POST 带 body)。 async agentScriptStream( projectId: string, payload: { mode?: "auto" | "theme" | "revise"; prompt?: string; model_config_id?: string; selling_point_ids?: string[]; base_version_id?: string; aspect_ratio?: string; total_duration?: number; target_index?: number; }, onEvent: (evt: { type: string; [k: string]: unknown }) => void, signal?: AbortSignal ): Promise { const token = getToken(); const headers = new Headers({ "Content-Type": "application/json", Accept: "text/event-stream" }); if (token) headers.set("Authorization", `Token ${token}`); const response = await fetch(`${API_BASE}/api/projects/${projectId}/script-agent-stream/`, { method: "POST", headers, body: JSON.stringify(payload), signal // 停止按钮:abort 这个 signal 即掐断流(后端检测断连会自动释放预扣额度) }); if (!response.ok || !response.body) { const text = await response.text().catch(() => ""); throw new ApiError(response.status, text || "脚本流式生成失败"); } const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); let buffer = ""; for (;;) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let sep: number; // SSE 帧以空行分隔(\n\n);每帧取 data: 行解析 while ((sep = buffer.indexOf("\n\n")) !== -1) { const frame = buffer.slice(0, sep); buffer = buffer.slice(sep + 2); const dataLine = frame.split("\n").find((l) => l.startsWith("data:")); if (!dataLine) continue; try { onEvent(JSON.parse(dataLine.slice(5).trim())); } catch { /* 跳过解析失败的帧 */ } } } }, adoptScript(projectId: string, script_version_id: string) { return request(`/api/projects/${projectId}/adopt-script/`, { method: "POST", body: JSON.stringify({ script_version_id }) }); }, // 独立实体提取(**异步**):只提交任务、秒回 task_id(慢活在 worker 跑,不再 502);已有在途则复用(防重复扣费)。 // 进度/结果用 extractStatus 轮询。商品不提(参考图层用真实主图)。 extractEntities(projectId: string) { return request<{ task_id: string; status: string }>(`/api/projects/${projectId}/extract-entities/`, { method: "POST" }); }, // 实体提取进度:running=有在途任务(刷新后据此重建 loading);否则读最近一次成败,成功回带 entities 供「提取+生成」继续出图。 extractStatus(projectId: string) { return request<{ running: boolean; status: string; error: string; entities: Array<{ id: string; type: "character" | "scene"; name: string; visual_prompt: string; ref_index: number }>; }>(`/api/projects/${projectId}/extract-status/`); }, updateScriptSegment(projectId: string, payload: { segment_id: string; narration?: string; visual_prompt?: string; duration_seconds?: number }) { return request(`/api/projects/${projectId}/update-script-segment/`, { method: "POST", body: JSON.stringify(payload) }); }, addScriptSegment(projectId: string, payload: { after_segment_id: string; narration?: string; visual_prompt?: string; duration_seconds?: number }) { return request(`/api/projects/${projectId}/add-script-segment/`, { method: "POST", body: JSON.stringify(payload) }); }, deleteScriptSegment(projectId: string, payload: { segment_id: string }) { return request(`/api/projects/${projectId}/delete-script-segment/`, { method: "POST", body: JSON.stringify(payload) }); }, // 单条分镜重跑:后端只重写指定 segment(可带 instruction 微调),返回更新后的 ScriptVersion rerunScriptSegment(projectId: string, payload: { segment_id: string; instruction?: string }) { return request(`/api/projects/${projectId}/rerun-script-segment/`, { method: "POST", body: JSON.stringify(payload) }); }, adoptVideoVersion(projectId: string, payload: { video_segment_id: string; version_id: string }) { return request(`/api/projects/${projectId}/adopt-video-version/`, { method: "POST", body: JSON.stringify(payload) }); }, generateVoiceover(projectId: string, payload: { items: Array<{ index: number; text: string }>; voice_type?: string; speed_ratio?: number }) { return request<{ voiceover: VoiceoverInfo }>(`/api/projects/${projectId}/generate-voiceover/`, { method: "POST", body: JSON.stringify(payload) }); }, // 异步:提交后秒回 RESERVED 任务,再用 generateImageStatus 轮询取结果(慢出图在 worker 跑,Web 不被占住) generateBaseAsset(projectId: string, payload: { kind: "product" | "person" | "scene"; prompt: string; label?: string }) { return request<{ task: { id: string; status: string } }>(`/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) }); }, // 显性采用/未采用某角色/场景组(下游只取已采用) setBaseAssetAdopt(projectId: string, payload: { group_id: string; state: "adopted" | "unadopted" }) { return request(`/api/projects/${projectId}/set-base-asset-adopt/`, { method: "POST", body: JSON.stringify(payload) }); }, // 删除某角色/场景资产组(连带其三视图组) deleteBaseAsset(projectId: string, groupId: string) { return request(`/api/projects/${projectId}/delete-base-asset/`, { method: "POST", body: JSON.stringify({ group_id: groupId }) }); }, // 流程步骤4 · 用演员库现有资产替换基础资产卡(挂为候选并采用) // seed 占位卡还没 group:不传 group_id,改传 kind+label,后端按 label 命中/新建实体组再挂(不出图) attachBaseAsset(projectId: string, payload: { group_id?: string; asset_id: string; kind?: "product" | "person" | "scene"; label?: string }) { return request(`/api/projects/${projectId}/attach-base-asset/`, { method: "POST", body: JSON.stringify(payload) }); }, // 流程步骤4 · 据某一版立绘资产生成它配套的三视图(image_edit 锁角色一致性,绑定该立绘 asset) // 异步:提交后秒回 RESERVED 任务,再用 generateImageStatus 轮询取结果(慢 image_edit 在 worker 跑) generateTriview(projectId: string, payload: { portrait_asset_id: string }) { return request<{ task: { id: string; status: string } }>(`/api/projects/${projectId}/generate-triview/`, { method: "POST", body: JSON.stringify(payload) }); }, // 轮询本团队真人资产的火山审核状态(绿盾 active / 红标 failed),刷新基础资产趴徽章 pollReviews(projectId: string) { return request<{ reviews: Record }>(`/api/projects/${projectId}/poll-reviews/`, { method: "POST" }); }, 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" }); }, // 单场重跑:只重出该 shot 的分镜图(新增一条历史版本并采用) rerunStoryboardShot(projectId: string, shotId: string, prompt?: string) { return request(`/api/projects/${projectId}/rerun-storyboard-shot/`, { method: "POST", body: JSON.stringify({ shot_id: shotId, prompt: prompt || "" }) }); }, // 采用某场的某个历史版本(切换该场分镜图) adoptStoryboardShotVersion(projectId: string, shotId: string, versionId: string) { return request(`/api/projects/${projectId}/adopt-storyboard-shot-version/`, { method: "POST", body: JSON.stringify({ shot_id: shotId, version_id: versionId }) }); }, submitVideo(projectId: string, payload: { video_segment_id: string; prompt: string }) { return request(`/api/projects/${projectId}/submit-video-segment/`, { method: "POST", body: JSON.stringify(payload) }); }, // 点生成视频前的过审闸:列出未过审的人物立绘/故事板分镜。video_segment_id 给定=只校验该段,不给=全部未出片段。 videoReviewPrecheck(projectId: string, videoSegmentId?: string) { return request<{ blockers: Array<{ video_segment_id: string; sort_order: number; scene_no: number; kind: "person" | "storyboard"; name: string; asset_id: string; review_status: string }> }>( `/api/projects/${projectId}/video-review-precheck/`, { method: "POST", body: JSON.stringify(videoSegmentId ? { video_segment_id: videoSegmentId } : {}) }, ); }, 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/"); }, // 服务端分页+过滤的资产列表(各页按需懒加载,不再前端取全量再切片)。 assetsPage(params: { tab?: string; category?: string; source?: string; asset_type?: string; product?: string; q?: string; ordering?: string; page?: number; pageSize?: number; meta?: Record; } = {}) { const qs = new URLSearchParams(); const { meta, pageSize, ...rest } = params; for (const [k, v] of Object.entries(rest)) if (v !== undefined && v !== "" && v !== null) qs.set(k, String(v)); if (pageSize) qs.set("page_size", String(pageSize)); if (meta) for (const [k, v] of Object.entries(meta)) if (v) qs.set(`m_${k}`, v); return request>(`/api/assets/?${qs.toString()}`); }, // 图片趴(模特上身图/平台套图/自由创作)按生成批次成组分页:每批一张卡(封面+张数),点开看整批图 assetBatches(params: { tab?: string; source?: string; q?: string; ordering?: string; page?: number; pageSize?: number; meta?: Record; } = {}) { const qs = new URLSearchParams(); const { meta, pageSize, ...rest } = params; for (const [k, v] of Object.entries(rest)) if (v !== undefined && v !== "" && v !== null) qs.set(k, String(v)); if (pageSize) qs.set("page_size", String(pageSize)); if (meta) for (const [k, v] of Object.entries(meta)) if (v) qs.set(`m_${k}`, v); return request>(`/api/assets/batches/?${qs.toString()}`); }, // 资产库各 tab 计数(tab 徽标用,不必取全量) assetSummary() { return request>("/api/assets/summary/"); }, // 视频成品:按项目素材包分组(每项目一包,含片段列表) videoPacks() { return request("/api/assets/video-packs/"); }, // 手动兜底:把单个送审类素材(角色/三视图/分镜)送火山审核(用户点灰盾「待审核」时) submitAssetReview(id: string) { return request<{ review_status: string; review_error: string }>(`/api/assets/${id}/submit-review/`, { method: "POST" }); }, // 单图「加入资产库 / 取消加入」:置 in_library。加入 → 出现在资产库列表;取消 → 移出列表。 // 资产生成时已落库并扣费,此口只控制是否进资产库展示列表,不增删真实资产、不二次扣费。 setAssetLibrary(id: string, inLibrary: boolean) { return request<{ id: string; in_library: boolean }>(`/api/assets/${id}/set-library/`, { method: "POST", body: JSON.stringify({ in_library: inLibrary }) }); }, // 某 tab 下真实存在的筛选项(来源/类型/指定 metadata 键取值),供下拉「只列真有的」 assetFacets(tab?: string, metaKeys: string[] = []) { const qs = new URLSearchParams(); if (tab) qs.set("tab", tab); if (metaKeys.length) qs.set("meta_keys", metaKeys.join(",")); return request<{ sources: string[]; kinds: string[]; metadata: Record }>(`/api/assets/facets/?${qs.toString()}`); }, // 导出全部:把项目所有已采用视频片段打成一个 zip 下载(后端逐段从 TOS 拉取打包) async exportProjectClips(projectId: string): Promise { const token = getToken(); const res = await fetch(`${API_BASE}/api/projects/${projectId}/export-clips/`, { headers: token ? { Authorization: `Token ${token}` } : undefined }); if (!res.ok) { let msg = `导出失败 (${res.status})`; try { msg = (await res.json()).detail || msg; } catch { /* 非 JSON 错误体 */ } throw new ApiError(res.status, msg); } return res.blob(); }, // 经后端同源代理取资产原始文件(TOS 未配 CORS,浏览器抽帧/解码必须同源) async fetchAssetBlob(id: string): Promise { const token = getToken(); const response = await fetch(`${API_BASE}/api/assets/${id}/raw/`, { headers: token ? { Authorization: `Token ${token}` } : undefined }); if (!response.ok) throw new ApiError(response.status, `fetch asset raw failed: ${response.status}`); return response.blob(); }, // 跟随 DRF 分页 next 取全部资产 —— 商品图/AI 素材/资产库都靠 asset.id 在这份列表里查 preview_url, // 只取第 1 页(20 条)会让第 20 条之后的资产解析不到图、渲染成空占位。 async allAssets(): Promise { // 一次大页(page_size=200)尽量取全;若还有多页,并行取剩余页(不再逐页串行等,根治整页刷新慢) const SIZE = 200; const first = await request>(`/api/assets/?page_size=${SIZE}`); const out: Asset[] = [...first.results]; const pageSize = first.results.length || SIZE; // 后端若忽略 page_size,按实际页大小算页数,逻辑仍正确 const totalPages = pageSize > 0 ? Math.ceil((first.count || out.length) / pageSize) : 1; if (totalPages <= 1) return out; const rest = await Promise.all( Array.from({ length: totalPages - 1 }, (_, i) => request>(`/api/assets/?page_size=${SIZE}&page=${i + 2}`) .then((p) => p.results) .catch(() => [] as Asset[]) ) ); rest.forEach((r) => out.push(...r)); return out; }, uploadAsset(formData: FormData) { return request("/api/assets/upload/", { method: "POST", body: formData }); }, // 流程步骤4 · 给人物/资产改名(添加人物工作台命名 → PATCH 写回 name) updateAsset(id: string, payload: { name?: string; description?: string }) { return request(`/api/assets/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, // 模特库:列出(本团队 ∪ 官方模板);tab=official 只看官方、mine 只看自建 listModels(params: { tab?: "official" | "mine"; q?: string; pageSize?: number } = {}) { const qs = new URLSearchParams(); if (params.tab) qs.set("tab", params.tab); if (params.q) qs.set("q", params.q); qs.set("page_size", String(params.pageSize ?? 200)); return request>(`/api/models/?${qs.toString()}`); }, // 真人上传:一张人像图 → 建 model_portrait 资产 + Model 实体 uploadModel(formData: FormData) { return request("/api/models/upload/", { method: "POST", body: formData }); }, // 删除模特(软删;官方模特不可删) deleteModel(id: string) { return request(`/api/models/${id}/`, { method: "DELETE" }); }, 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() { // 取一大页(后端上限 200):任务中心要按真实总量算 全部/已完成/失败 三个 tab 数, // 只取默认首页(20 条)会把统计算偏,甚至误显示「全部失败」。 return request>("/api/ai/tasks/?page_size=200"); }, // 异步生图:提交后秒级返回任务列表(慢出图在 worker 跑),再用 generateImageStatus 轮询取结果 submitGenerateImage(payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; reference_product?: boolean; model_id?: string; model_entity_id?: string; ratio?: string; image_model?: string }) { return request<{ tasks: { id: string; status: string }[] }>("/api/ai/generate-image/", { method: "POST", body: JSON.stringify(payload) }); }, generateImageStatus(ids: string[]) { return request<{ tasks: { id: string; status: string; error_message: string; assets: Asset[] }[] }>(`/api/ai/generate-image/?ids=${encodeURIComponent(ids.join(","))}`); }, 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}` : ""}`); }, // 侧边栏徽标(unread_count)+ 团队动态(只展示最近 6 条)用。只取第 1 页 100 条即足够, // count 用后端真实总数(给「共 N」显示),不再随消息增长逐页翻全部(原来 O(N) 拖慢每次刷新)。 // 注:不参与收件箱滚动渲染——收件箱走 listNotifications 自己的服务端分页。 async allNotifications(): Promise { return request(`/api/ops/notifications/?page_size=100`); }, // 侧边栏未读徽标:只要 unread_count,取 1 条即可(不为徽标拉 100 条) notificationsBadge() { return request(`/api/ops/notifications/?page_size=1`); }, 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" }); }, archiveNotification(id: string) { // 后端 archive 端点返回 204 无 body return request(`/api/ops/notifications/${id}/archive/`, { method: "POST" }); } }; // 平台超管后台 API(/api/admin/*,全程 IsPlatformAdmin)。与团队级 api 分层,便于权限收敛。 export const adminApi = { invitations(params?: { kind?: string; status?: string; search?: string; page?: number; page_size?: number }) { const qs = new URLSearchParams(); if (params?.kind) qs.set("kind", params.kind); if (params?.status) qs.set("status", params.status); if (params?.search) qs.set("search", params.search); if (params?.page) qs.set("page", String(params.page)); if (params?.page_size) qs.set("page_size", String(params.page_size)); const q = qs.toString(); return request>(`/api/admin/invitations/${q ? `?${q}` : ""}`); }, createInvite(payload?: { email?: string }) { return request("/api/admin/invitations/", { method: "POST", body: JSON.stringify(payload || {}) }); }, revokeInvite(id: string) { return request(`/api/admin/invitations/${id}/revoke/`, { method: "POST" }); }, teams(params?: { status?: string; search?: string; page?: number; page_size?: number }) { const qs = new URLSearchParams(); if (params?.status) qs.set("status", params.status); if (params?.search) qs.set("search", params.search); if (params?.page) qs.set("page", String(params.page)); if (params?.page_size) qs.set("page_size", String(params.page_size)); const q = qs.toString(); return request>(`/api/admin/teams/${q ? `?${q}` : ""}`); }, teamDetail(id: string) { return request(`/api/admin/teams/${id}/`); }, toggleTeam(id: string) { return request(`/api/admin/teams/${id}/toggle/`, { method: "POST" }); }, users(params?: { status?: string; search?: string; page?: number; page_size?: number }) { const qs = new URLSearchParams(); if (params?.status) qs.set("status", params.status); if (params?.search) qs.set("search", params.search); if (params?.page) qs.set("page", String(params.page)); if (params?.page_size) qs.set("page_size", String(params.page_size)); const q = qs.toString(); return request>(`/api/admin/users/${q ? `?${q}` : ""}`); }, toggleUser(id: string) { return request(`/api/admin/users/${id}/toggle/`, { method: "POST" }); }, resetUserPassword(id: string, password: string) { return request(`/api/admin/users/${id}/reset-password/`, { method: "POST", body: JSON.stringify({ password }) }); }, qualityWords() { return request("/api/admin/quality-words/"); }, createQualityWord(payload: { stage: string; text: string; sort?: number; slot?: string }) { return request("/api/admin/quality-words/", { method: "POST", body: JSON.stringify(payload) }); }, updateQualityWord(id: string, payload: { text?: string; sort?: number; enabled?: boolean }) { return request(`/api/admin/quality-words/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, deleteQualityWord(id: string) { return request(`/api/admin/quality-words/${id}/`, { method: "DELETE" }); }, // 生图/视频提示词模板(视频线 6 条):列表 + 改正文/比例/启用 promptTemplates() { return request("/api/admin/prompt-templates/"); }, updatePromptTemplate(id: string, payload: { template?: string; ratio?: string; enabled?: boolean }) { return request(`/api/admin/prompt-templates/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, assetReviews(params?: { review_status?: string; search?: string; page?: number; page_size?: number }) { const qs = new URLSearchParams(); if (params?.review_status) qs.set("review_status", params.review_status); if (params?.search) qs.set("search", params.search); if (params?.page) qs.set("page", String(params.page)); if (params?.page_size) qs.set("page_size", String(params.page_size)); const q = qs.toString(); return request>(`/api/admin/asset-reviews/${q ? `?${q}` : ""}`); }, submitReviews(assetIds: string[]) { return request<{ submitted: number; statuses: Record }>( "/api/admin/asset-reviews/submit/", { method: "POST", body: JSON.stringify({ asset_ids: assetIds }) } ); }, pollReviews(assetIds?: string[]) { return request<{ polled: number; statuses: Record }>( "/api/admin/asset-reviews/poll/", { method: "POST", body: JSON.stringify(assetIds ? { asset_ids: assetIds } : {}) } ); }, tasks(params?: { status?: string; task_type?: string; team?: string; anomaly?: string; page?: number; page_size?: number }) { const qs = new URLSearchParams(); if (params?.status) qs.set("status", params.status); if (params?.task_type) qs.set("task_type", params.task_type); if (params?.team) qs.set("team", params.team); if (params?.anomaly) qs.set("anomaly", params.anomaly); if (params?.page) qs.set("page", String(params.page)); if (params?.page_size) qs.set("page_size", String(params.page_size)); const q = qs.toString(); return request>(`/api/admin/tasks/${q ? `?${q}` : ""}`); }, taskDetail(id: string) { return request(`/api/admin/tasks/${id}/`); }, retryTask(id: string) { return request<{ retried: boolean; task_id: string }>(`/api/admin/tasks/${id}/retry/`, { method: "POST" }); }, ledgers(params?: { ledger_type?: string; team?: string; page?: number; page_size?: number }) { const qs = new URLSearchParams(); if (params?.ledger_type) qs.set("ledger_type", params.ledger_type); if (params?.team) qs.set("team", params.team); if (params?.page) qs.set("page", String(params.page)); if (params?.page_size) qs.set("page_size", String(params.page_size)); const q = qs.toString(); return request>(`/api/admin/ledgers/${q ? `?${q}` : ""}`); }, adjustCredit(payload: { team: string; amount: string; reason: string }) { return request("/api/admin/ledgers/adjust/", { method: "POST", body: JSON.stringify(payload) }); }, quotaPolicies(params?: { team?: string; page_size?: number }) { const qs = new URLSearchParams(); if (params?.team) qs.set("team", params.team); if (params?.page_size) qs.set("page_size", String(params.page_size)); const q = qs.toString(); return request>(`/api/admin/quota-policies/${q ? `?${q}` : ""}`); }, createQuotaPolicy(payload: { team: string; monthly_limit?: string | null; project_limit?: string | null; per_task_limit?: string | null; is_active?: boolean }) { return request("/api/admin/quota-policies/", { method: "POST", body: JSON.stringify(payload) }); }, updateQuotaPolicy(id: string, payload: { monthly_limit?: string | null; project_limit?: string | null; per_task_limit?: string | null; is_active?: boolean }) { return request(`/api/admin/quota-policies/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, deleteQuotaPolicy(id: string) { return request(`/api/admin/quota-policies/${id}/`, { method: "DELETE" }); }, providers() { return request("/api/admin/providers/"); }, createProvider(payload: { name: string; display_name: string; base_url?: string; api_key?: string; status?: string }) { return request("/api/admin/providers/", { method: "POST", body: JSON.stringify(payload) }); }, updateProvider(id: string, payload: Record) { return request(`/api/admin/providers/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, deleteProvider(id: string) { return request(`/api/admin/providers/${id}/`, { method: "DELETE" }); }, models(params?: { provider?: string; capability?: string }) { const qs = new URLSearchParams(); if (params?.provider) qs.set("provider", params.provider); if (params?.capability) qs.set("capability", params.capability); const q = qs.toString(); return request(`/api/admin/models/${q ? `?${q}` : ""}`); }, createModel(payload: { provider: string; name: string; display_name: string; capability: string; endpoint?: string; unit_price?: string; status?: string }) { return request("/api/admin/models/", { method: "POST", body: JSON.stringify(payload) }); }, updateModel(id: string, payload: Record) { return request(`/api/admin/models/${id}/`, { method: "PATCH", body: JSON.stringify(payload) }); }, deleteModel(id: string) { return request(`/api/admin/models/${id}/`, { method: "DELETE" }); }, setDefaultModel(id: string) { return request(`/api/admin/models/${id}/set-default/`, { method: "POST" }); }, adminProjects(params?: { status?: string; search?: string; page?: number; page_size?: number }) { const qs = new URLSearchParams(); if (params?.status) qs.set("status", params.status); if (params?.search) qs.set("search", params.search); if (params?.page) qs.set("page", String(params.page)); if (params?.page_size) qs.set("page_size", String(params.page_size)); const q = qs.toString(); return request>(`/api/admin/projects/${q ? `?${q}` : ""}`); }, integrity() { return request("/api/admin/integrity/"); } };