Files
yingqing/core/frontend/src/api.ts
T

1123 lines
57 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
AdminIntegrity,
AdminLedger,
AdminModel,
AdminProjectRow,
AdminProvider,
AdminQualityWord,
AdminQuotaPolicy,
AdminReviewAsset,
AdminTask,
AdminTaskDetail,
AdminTeam,
AdminTeamDetail,
AdminUser,
AITask,
Asset,
AuthPayload,
BillingConfigInfo,
BillingSummary,
BillingTrend,
FreeAssetGroup,
FreeAssetItem,
FreeVideoRef,
FreeVideoTask,
FreeVideoUploadResult,
Ledger,
LoginSession,
Invitation,
ImageConversation,
ImageConversationTrash,
ImageConversationTask,
ModelConfig,
ModelEntity,
Notification,
NotificationList,
Paginated,
Product,
Project,
RechargeResult,
ScriptTemplate,
ScriptVersion,
Team,
TeamMember,
User,
UserPreference,
VoiceoverInfo
} from "./types";
import type { PresentationFormat, VideoStructure } from "./script-setup";
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;
const AUTH_MISSING_MESSAGES = new Set([
"身份认证信息未提供。",
"Authentication credentials were not provided."
]);
const FRIENDLY_AUTH_MISSING_MESSAGE = "登录状态异常,请刷新页面后重新登录;若仍无法操作,请联系管理员。";
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<T>(path: string, options: RequestInit = {}): Promise<T> {
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<string, unknown>;
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();
}
// 后端默认的“身份认证信息未提供”只描述技术原因,用户无法判断该怎么处理。
// 仅翻译这一种“根本没有携带凭证”的响应;无效 token 等其他认证错误仍保留原始提示,方便区分。
if (response.status === 401 && AUTH_MISSING_MESSAGES.has(message)) {
message = FRIENDLY_AUTH_MISSING_MESSAGE;
}
// 团队接口统一用此 403 表示当前账号不具备操作资格;只翻译精确原文,避免掩盖其他 403 原因。
if (response.status === 403 && message === "permission denied") {
message = "当前账号权限不足";
}
// nginx / 网关在请求进 Django 前拦大文件,响应体是 HTML「413 Request Entity Too Large」。
if (response.status === 413) {
message = "文件太大,服务器没接收。视频请压到 200MB 以内再传";
}
throw new ApiError(response.status, message);
}
if (response.status === 204) return undefined as T;
return response.json() as Promise<T>;
}
export const api = {
register(payload: { username: string; password: string; email?: string; team_name?: string; invite_code: string }) {
return request<AuthPayload>("/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<AuthPayload>("/api/auth/login/", { method: "POST", body: JSON.stringify(payload) });
},
me() {
return request<{ user: User; team: Team; role?: string }>("/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<User>("/api/auth/me/avatar/", { method: "POST", body: formData });
},
resetAvatar() {
return request<User>("/api/auth/me/avatar/", { method: "DELETE" });
},
deleteAsset(id: string) {
return request<void>(`/api/assets/${id}/`, { method: "DELETE" });
},
// R108 资产垃圾桶(与商品垃圾桶同构):已软删资产列表 / 恢复回资产库 / 彻底删除(不可恢复)
assetsTrash() {
return request<Paginated<Asset>>("/api/assets/trash/");
},
restoreAsset(id: string) {
return request<Asset>(`/api/assets/${id}/restore/`, { method: "POST" });
},
purgeAsset(id: string) {
return request<void>(`/api/assets/${id}/purge/`, { method: "DELETE" });
},
preferences() {
return request<UserPreference>("/api/auth/me/preferences/");
},
updatePreferences(payload: Partial<UserPreference>) {
return request<UserPreference>("/api/auth/me/preferences/", { method: "PUT", body: JSON.stringify(payload) });
},
loginSessions() {
return request<LoginSession[]>("/api/auth/me/sessions/");
},
revokeSession(id: string) {
// 后端旋转 token 真正吊销被下线设备(单 token 体系),回传新 token 给当前设备保活
return request<{ revoked: number; token?: string }>(`/api/auth/me/sessions/${id}/revoke/`, { method: "POST" });
},
revokeOtherSessions() {
return request<{ token: string }>("/api/auth/me/sessions/revoke-others/", { method: "POST" });
},
logout() {
return request<void>("/api/auth/logout/", { method: "POST" });
},
teamMembers() {
return request<TeamMember[]>("/api/auth/team/members/");
},
// 团队级设置读取(任意成员可读):团队页/消费页取最新月限额,避免依赖登录时的陈旧 /me/ 快照(R72)
teamSettings() {
return request<Team>("/api/auth/team/settings/");
},
// 团队级月限额持久化(超管在团队页设置 · PMC#12);monthly_credit_limit: 数值 / null(清空)
updateTeamSettings(payload: { monthly_credit_limit?: number | string | null }) {
return request<Team>("/api/auth/team/settings/", { method: "PATCH", body: JSON.stringify(payload) });
},
createTeamMember(payload: {
username: string;
password: string;
name?: string;
email?: string;
role?: string;
daily_credit_limit?: number | string;
monthly_credit_limit?: number | string;
total_credit_limit?: number | string;
}) {
return request<TeamMember>("/api/auth/team/members/", { method: "POST", body: JSON.stringify(payload) });
},
updateTeamMember(id: string, payload: { role?: string; daily_credit_limit?: number | string; monthly_credit_limit?: number | string; total_credit_limit?: number | string; name?: string }) {
return request<TeamMember>(`/api/auth/team/members/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
removeTeamMember(id: string) {
return request<void>(`/api/auth/team/members/${id}/`, { method: "DELETE" });
},
resetMemberPassword(id: string, password: string) {
return request<void>(`/api/auth/team/members/${id}/password/`, {
method: "POST",
body: JSON.stringify({ password })
});
},
listInvitations() {
return request<Invitation[]>("/api/auth/team/invitations/");
},
createInvitation(payload: { role?: string; email?: string; monthly_credit_limit?: number | string }) {
return request<Invitation>("/api/auth/team/invitations/", { method: "POST", body: JSON.stringify(payload) });
},
revokeInvitation(id: string) {
return request<Invitation>(`/api/auth/team/invitations/${id}/revoke/`, { method: "POST" });
},
products() {
return request<Paginated<Product>>("/api/products/");
},
product(id: string) {
return request<Product>(`/api/products/${id}/`);
},
createProduct(payload: {
title?: string;
brand?: string;
business_type?: string;
category?: string;
target_audience?: string;
description?: string;
specs?: Record<string, unknown>;
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<Product>("/api/products/", { method: "POST", body: JSON.stringify(payload) });
},
updateProduct(id: string, payload: Partial<Product>) {
return request<Product>(`/api/products/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
uploadProductImage(productId: string, formData: FormData) {
return request<Product>(`/api/products/${productId}/images/`, { method: "POST", body: formData });
},
deleteProductImage(productId: string, imageId: string) {
return request<Product>(`/api/products/${productId}/images/${imageId}/`, { method: "DELETE" });
},
deleteProduct(id: string) {
// 软删 = 进垃圾桶(后端 status→archived,可恢复)
return request<void>(`/api/products/${id}/`, { method: "DELETE" });
},
// 商品详情结构化素材(角色/商品图/图片成品/项目包)
productMaterials(id: string) {
return request<import("./types").ProductMaterials>(`/api/products/${id}/materials/`);
},
productsTrash() {
return request<Paginated<Product>>("/api/products/trash/");
},
restoreProduct(id: string) {
return request<Product>(`/api/products/${id}/restore/`, { method: "POST" });
},
purgeProduct(id: string) {
// 彻底删除 = 二级软删除(从垃圾桶隐藏,DB 记录保留)
return request<void>(`/api/products/${id}/purge/`, { method: "DELETE" });
},
projects() {
return request<Paginated<Project>>("/api/projects/");
},
project(id: string) {
return request<Project>(`/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<string, unknown> }) {
return request<Project>("/api/projects/", { method: "POST", body: JSON.stringify(payload) });
},
// 整体替换 metadata —— 调用方务必先展开现有 project.metadata 再合并,别把别的 key 冲掉
updateProject(id: string, payload: { name?: string; metadata?: Record<string, unknown> }) {
return request<Project>(`/api/projects/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
deleteProject(id: string) {
return request<void>(`/api/projects/${id}/`, { method: "DELETE" });
},
projectsTrash() {
return request<Paginated<Project>>("/api/projects/trash/");
},
restoreProject(id: string) {
return request<Project>(`/api/projects/${id}/restore/`, { method: "POST" });
},
purgeProject(id: string) {
return request<void>(`/api/projects/${id}/purge/`, { method: "DELETE" });
},
// 上传脚本(docx)取正文:docx 是 zip,交后端拆,前端不引解析库。txt 前端自己 file.text() 即可。
extractScriptFile(projectId: string, formData: FormData) {
return request<{ name: string; chars: number; text: string }>(
`/api/projects/${projectId}/extract-script-file/`,
{ method: "POST", body: formData }
);
},
// 1.11 上传视频提炼:后端抽帧 → 多模态读帧 → 中文分镜稿。与上传脚本同形状(只回文本不落库),
// 差别是真调模型,慢(整条 30~60 秒)且计一次费,故前端要给等待反馈。
extractScriptVideo(projectId: string, formData: FormData) {
return request<{ name: string; chars: number; text: string; frames: number; duration: number }>(
`/api/projects/${projectId}/extract-script-video/`,
{ method: "POST", body: formData }
);
},
// ── 套路模板(5.1 存 / 5.2 换商品重跑)· 团队隔离 ──
// 存模板只抽套路不抽文案:后端从这版脚本抽镜数/每镜作用/节奏/商品露出/转化写法。
saveProjectAsTemplate(projectId: string, payload: { name: string; script_version_id?: string }) {
return request<ScriptTemplate>(`/api/projects/${projectId}/save-as-template/`, {
method: "POST",
body: JSON.stringify(payload),
});
},
listScriptTemplates() {
return request<Paginated<ScriptTemplate>>("/api/script-templates/");
},
renameScriptTemplate(id: string, name: string) {
return request<ScriptTemplate>(`/api/script-templates/${id}/`, { method: "PATCH", body: JSON.stringify({ name }) });
},
deleteScriptTemplate(id: string) {
return request<void>(`/api/script-templates/${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[];
persona?: string;
base_version_id?: string;
aspect_ratio?: string;
total_duration?: number;
// 二期:表现形式 × 视频结构 —— 后端按这两个挑套路提示词,不传则回落 口播 × 痛点解决
presentation_format?: PresentationFormat;
video_structure?: VideoStructure;
target_index?: number;
// 哪个入口来的:辅助生成 / 上传脚本 / 上传视频提炼。三者都是 mode=auto,靠它区分并记进脚本「来源」
source?: "ai" | "manual" | "video";
},
onEvent: (evt: { type: string; [k: string]: unknown }) => void,
signal?: AbortSignal
): Promise<void> {
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<ScriptVersion>(`/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?: unknown;
error_message: 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<ScriptVersion>(`/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<ScriptVersion>(`/api/projects/${projectId}/add-script-segment/`, { method: "POST", body: JSON.stringify(payload) });
},
deleteScriptSegment(projectId: string, payload: { segment_id: string }) {
return request<ScriptVersion>(`/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<ScriptVersion>(`/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<Project>(`/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; reference_asset_id?: 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<string, string> }>(`/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<Project>(`/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<Project>(`/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<Project>(`/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<import("./types").ExportPoll>(`/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<Project>(`/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<Project>(`/api/projects/${projectId}/upload-bgm/`, { method: "POST", body: form });
},
saveTimeline(projectId: string, payload: import("./types").TimelineSavePayload) {
return request<Project>(`/api/projects/${projectId}/save-timeline/`, { method: "POST", body: JSON.stringify(payload) });
},
assets() {
return request<Paginated<Asset>>("/api/assets/");
},
// 服务端分页+过滤的资产列表(各页按需懒加载,不再前端取全量再切片)。
assetsPage(params: {
tab?: string; category?: string; source?: string; asset_type?: string;
product?: string; origin_task?: string; q?: string; ordering?: string; page?: number; pageSize?: number;
// in_library: "all" 看全部(含未入库,任务中心用)/ "false" 只看未入库 / 省略=只看已入库
in_library?: string;
meta?: Record<string, string>;
} = {}) {
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<Paginated<Asset>>(`/api/assets/?${qs.toString()}`);
},
// 图片趴(模特上身图/平台套图/自由创作)按生成批次成组分页:每批一张卡(封面+张数),点开看整批图
assetBatches(params: {
tab?: string; source?: string; product?: string; q?: string; ordering?: string; page?: number; pageSize?: number;
meta?: Record<string, string>;
} = {}) {
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<Paginated<import("./types").AssetBatch>>(`/api/assets/batches/?${qs.toString()}`);
},
// 资产库各 tab 计数(tab 徽标用,不必取全量)
assetSummary() {
return request<Record<string, number>>("/api/assets/summary/");
},
// 视频成品:按项目素材包分组(每项目一包,含片段列表)
videoPacks() {
return request<import("./types").VideoPack[]>("/api/assets/video-packs/");
},
// 手动兜底:把单个送审类素材(角色/三视图/分镜)送火山审核(用户点灰盾「待审核」时)。
// forReference:素材要拿去当生成参考(自由创作 @引用三库),此时不看类目白名单一律送审。
submitAssetReview(id: string, forReference = false) {
return request<{ review_status: string; review_error: string }>(`/api/assets/${id}/submit-review/`, {
method: "POST",
body: JSON.stringify({ for_reference: forReference })
});
},
// 置 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 键取值),供下拉「只列真有的」
async assetFacets(tab?: string, metaKeys: string[] = []) {
const qs = new URLSearchParams();
if (tab) qs.set("tab", tab);
if (metaKeys.length) qs.set("meta_keys", metaKeys.join(","));
const f = await request<{ sources?: string[]; kinds?: string[]; metadata?: Record<string, string[]>; products?: { id: string; title: string }[] }>(`/api/assets/facets/?${qs.toString()}`);
// 旧后端(未部署 products 字段)返回体缺键会让页面 undefined.find 白屏,逐键兜底
return { sources: f.sources ?? [], kinds: f.kinds ?? [], metadata: f.metadata ?? {}, products: f.products ?? [] };
},
// 导出全部:把项目所有已采用视频片段打成一个 zip 下载(后端逐段从 TOS 拉取打包)
async exportProjectClips(projectId: string): Promise<Blob> {
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<Blob> {
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<Asset[]> {
// 一次大页(page_size=200)尽量取全;若还有多页,并行取剩余页(不再逐页串行等,根治整页刷新慢)
const SIZE = 200;
const first = await request<Paginated<Asset>>(`/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<Paginated<Asset>>(`/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<Asset>("/api/assets/upload/", { method: "POST", body: formData });
},
// 流程步骤4 · 给模特形象/资产改名(添加模特工作台命名 → PATCH 写回 name
updateAsset(id: string, payload: { name?: string; description?: string }) {
return request<Asset>(`/api/assets/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
// 模特库:列出(本团队 官方模板);tab=official 只看官方、mine 只看自建
listModels(params: { tab?: "official" | "mine"; q?: string; portraitAsset?: string; pageSize?: number } = {}) {
const qs = new URLSearchParams();
if (params.tab) qs.set("tab", params.tab);
if (params.q) qs.set("q", params.q);
if (params.portraitAsset) qs.set("portrait_asset", params.portraitAsset);
qs.set("page_size", String(params.pageSize ?? 200));
return request<Paginated<ModelEntity>>(`/api/models/?${qs.toString()}`);
},
// 真人上传:一张人像图 → 建 model_portrait 资产 + Model 实体
uploadModel(formData: FormData) {
return request<ModelEntity>("/api/models/upload/", { method: "POST", body: formData });
},
// /models 详情:上传并替换当前模特形象图,不创建新模特。
uploadModelPortrait(id: string, formData: FormData) {
return request<ModelEntity>(`/api/models/${id}/upload-portrait/`, { method: "POST", body: formData });
},
// 图片创作「加入模特库」:把一张已生成的图复用为模特形象图建 Model 条目(不重传、不二次扣费;幂等)
enrollModelFromAsset(assetId: string, name?: string) {
return request<ModelEntity>("/api/models/enroll-asset/", { method: "POST", body: JSON.stringify({ asset_id: assetId, name }) });
},
// /models 模特详情编辑:只更新模特实体资料,不回写项目角色或底层图片资产。
updateLibraryModel(id: string, payload: { name?: string; description?: string }) {
return request<ModelEntity>(`/api/models/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
modelTriviewQuote(id: string) {
return request<{ points: string }>(`/api/models/${id}/triview-quote/`);
},
generateModelTriview(id: string) {
return request<{ task_id: string; status: string; estimated_cost: string; reused: boolean }>(
`/api/models/${id}/generate-triview/`,
{ method: "POST" },
);
},
modelTriviewStatus(id: string, taskId: string) {
const query = new URLSearchParams({ task_id: taskId });
return request<{
task_id: string;
status: string;
estimated_cost: string;
actual_cost: string;
error?: unknown;
error_message: string;
model: ModelEntity | null;
}>(`/api/models/${id}/triview-status/?${query.toString()}`);
},
// 删除模特(软删;官方模特不可删)
deleteModel(id: string) {
return request<void>(`/api/models/${id}/`, { method: "DELETE" });
},
modelsTrash() {
return request<Paginated<ModelEntity>>("/api/models/trash/");
},
restoreModel(id: string) {
return request<ModelEntity>(`/api/models/${id}/restore/`, { method: "POST" });
},
purgeModel(id: string) {
return request<void>(`/api/models/${id}/purge/`, { method: "DELETE" });
},
billingSummary() {
return request<BillingSummary>("/api/billing/summary/");
},
ledgers(page = 1, pageSize = 10, filters?: { ledger_type?: string; user?: string }) {
// 类型/成员筛选传参给后端做全量过滤(count/总页数随筛选变化 · R70)
const q = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
if (filters?.ledger_type) q.set("ledger_type", filters.ledger_type);
if (filters?.user) q.set("user", filters.user);
return request<{ count: number; page: number; page_size: number; results: Ledger[] }>(`/api/billing/ledgers/?${q.toString()}`);
},
billingTrend(range?: "day" | "week" | "month") {
return request<BillingTrend>(`/api/billing/trend/${range ? `?range=${range}` : ""}`);
},
modelConfigs() {
return request<Paginated<ModelConfig>>("/api/ai/models/");
},
aiTasks() {
// 生图工作室的任务中心:只看生图任务(模特上身图/平台套图/图片创作 = person_image /
// product_image),不掺脚本/实体抽取/故事板等流水线内部任务。取一大页(后端上限 200)
// 以便 全部/已完成/失败 三个 tab 数按真实总量算。
return request<Paginated<AITask>>("/api/ai/tasks/?page_size=200&task_type=person_image,product_image");
},
// R100:工作台生成记录(模特上身图/平台套图)——从后端持久任务拉取,与任务中心同源,
// 刷新/换浏览器不丢;按 mode(+product)过滤,前端按 batch_id 归批。
workbenchTasks(mode: "model" | "cover" | "image", productId?: string) {
const qs = new URLSearchParams({ mode });
if (productId) qs.set("product", productId);
return request<{ tasks: import("./types").WorkbenchTask[] }>(`/api/ai/tasks/workbench/?${qs.toString()}`);
},
// 工作台图片批次:全成功时沿用资产垃圾桶;含失败/取消任务时进入“图片异常批次”并支持整批恢复。
deleteWorkbenchImageBatch(taskId: string) {
return request<{ storage: "asset" | "exception_batch" }>(`/api/ai/tasks/${taskId}/workbench-batch/`, { method: "DELETE" });
},
imageExceptionBatchesTrash() {
return request<{ results: import("./types").ImageExceptionBatchTrash[] }>("/api/ai/tasks/workbench-exception-batches-trash/");
},
restoreWorkbenchImageBatch(taskId: string) {
return request<{ restored_task_count: number }>(`/api/ai/tasks/${taskId}/restore-workbench-batch/`, { method: "POST" });
},
purgeWorkbenchImageBatch(taskId: string) {
return request<void>(`/api/ai/tasks/${taskId}/purge-workbench-batch/`, { method: "DELETE" });
},
// YYX#row22:未读生成任务汇总(导航胶囊总数 + 按商品分组角标)
aiTasksUnread() {
return request<{ total: number; by_product: Record<string, number> }>("/api/ai/tasks/unread/");
},
// YYX#row22:标记已读 → 清零未读胶囊。不传参 = 全部已读;可按 product_id / batch_id / ids 局部清。
markAiTasksRead(payload?: { product_id?: string; batch_id?: string; ids?: string[] }) {
return request<{ updated: number }>("/api/ai/tasks/mark-read/", { method: "POST", body: JSON.stringify(payload || {}) });
},
// 异步生图:提交后秒级返回任务列表(慢出图在 worker 跑),再用 generateImageStatus 轮询取结果。
// 带 conversation_id 则归属该对话;不带则后端自动开一条新对话并回传其 id。
// 带 batch_id(重跑/补图)则任务归回原批次;响应回传本批 batch_id 供前端存进批次卡。
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; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string; retry_of_task_id?: string }) {
return request<{ conversation_id: string; batch_id?: string; tasks: { id: string; status: string }[] }>("/api/ai/generate-image/", { method: "POST", body: JSON.stringify(payload) });
},
// 图片创作对话 CRUD —— 左栏会话列表 / 新对话 / 切换 / 重命名 / 删除
listConversations(mode: "image" | "model" | "cover" = "image", productId?: string) {
const params = new URLSearchParams({ mode, page_size: "100" });
if (productId) params.set("product_id", productId);
else params.set("scope", "unbound");
return request<Paginated<ImageConversation>>(`/api/ai/image-conversations/?${params.toString()}`);
},
conversationsTrash(mode: "image" | "model" | "cover" = "image") {
return request<Paginated<ImageConversationTrash>>(`/api/ai/image-conversations/trash/?mode=${mode}&page_size=100`);
},
createConversation(payload: { mode?: "image" | "model" | "cover"; title?: string; product?: string | null }) {
return request<ImageConversation>("/api/ai/image-conversations/", { method: "POST", body: JSON.stringify(payload) });
},
renameConversation(id: string, title: string) {
return request<ImageConversation>(`/api/ai/image-conversations/${id}/`, { method: "PATCH", body: JSON.stringify({ title }) });
},
deleteConversation(id: string) {
return request<void>(`/api/ai/image-conversations/${id}/`, { method: "DELETE" });
},
restoreConversation(id: string) {
return request<ImageConversation>(`/api/ai/image-conversations/${id}/restore/`, { method: "POST" });
},
purgeConversation(id: string) {
return request<void>(`/api/ai/image-conversations/${id}/purge/`, { method: "DELETE" });
},
// 切换对话时回填:该对话历次生成的任务 + 成图
conversationTasks(id: string) {
return request<{ conversation_id: string; tasks: ImageConversationTask[] }>(`/api/ai/image-conversations/${id}/tasks/`);
},
generateImageStatus(ids: string[]) {
return request<{ tasks: { id: string; status: string; error?: unknown; error_message: string; assets: Asset[] }[] }>(`/api/ai/generate-image/?ids=${encodeURIComponent(ids.join(","))}`);
},
// —— 自由创作·视频生成 ——
// 提交秒回(火山 create 同步、慢轮询交给 worker 兜底 + 前端主动 poll);校验失败 400 带中文 detail。
submitFreeVideo(payload: {
prompt: string;
mode: "universal" | "keyframe";
model?: string;
aspect_ratio?: string;
resolution?: string;
duration?: number;
seed?: number;
generate_audio?: boolean;
references?: FreeVideoRef[];
}) {
return request<{ task: FreeVideoTask }>("/api/ai/free-video/", { method: "POST", body: JSON.stringify(payload) });
},
freeVideoTasks(offset = 0, pageSize = 20) {
return request<{ results: FreeVideoTask[]; total: number; has_more: boolean }>(
`/api/ai/free-video/?offset=${offset}&page_size=${pageSize}`
);
},
freeVideoTrash(offset = 0, pageSize = 20) {
return request<{ results: FreeVideoTask[]; total: number; has_more: boolean }>(
`/api/ai/free-video/trash/?offset=${offset}&page_size=${pageSize}`
);
},
// web 进程内单次轮询+终态化(幂等):前端渐进轮询打这里,本地无 worker 也能收尾
pollFreeVideo(id: string) {
return request<{ task: FreeVideoTask }>(`/api/ai/free-video/${id}/poll/`, { method: "POST" });
},
toggleFreeVideoFavorite(id: string) {
return request<{ is_favorited: boolean }>(`/api/ai/free-video/${id}/favorite/`, { method: "POST" });
},
deleteFreeVideo(id: string) {
return request<void>(`/api/ai/free-video/${id}/`, { method: "DELETE" });
},
restoreFreeVideo(id: string) {
return request<{ task: FreeVideoTask }>(`/api/ai/free-video/${id}/restore/`, { method: "POST" });
},
purgeFreeVideo(id: string) {
return request<void>(`/api/ai/free-video/${id}/purge/`, { method: "DELETE" });
},
uploadFreeVideoRef(formData: FormData) {
return request<FreeVideoUploadResult>("/api/ai/free-video/upload/", { method: "POST", body: formData });
},
// 自由创作·人物素材库(火山 Assets API asset:// 引用登记)
freeAssetGroups() {
return request<{ results: FreeAssetGroup[] }>("/api/assets/free-groups/");
},
createFreeAssetGroup(payload: { name: string; description?: string }) {
return request<{ group: FreeAssetGroup }>("/api/assets/free-groups/", { method: "POST", body: JSON.stringify(payload) });
},
freeAssetGroup(id: string) {
return request<{ group: FreeAssetGroup; assets: FreeAssetItem[] }>(`/api/assets/free-groups/${id}/`);
},
updateFreeAssetGroup(id: string, payload: { name?: string; description?: string }) {
return request<{ group: FreeAssetGroup }>(`/api/assets/free-groups/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
deleteFreeAssetGroup(id: string) {
return request<void>(`/api/assets/free-groups/${id}/`, { method: "DELETE" });
},
uploadFreeAsset(groupId: string, formData: FormData) {
return request<{ asset: FreeAssetItem }>(`/api/assets/free-groups/${groupId}/assets/`, { method: "POST", body: formData });
},
quickUploadFreeAsset(formData: FormData) {
return request<{ group: FreeAssetGroup; asset: FreeAssetItem }>("/api/assets/free-assets/quick-upload/", { method: "POST", body: formData });
},
renameFreeAsset(id: string, name: string) {
return request<{ asset: FreeAssetItem }>(`/api/assets/free-assets/${id}/`, { method: "PATCH", body: JSON.stringify({ name }) });
},
deleteFreeAsset(id: string) {
return request<void>(`/api/assets/free-assets/${id}/`, { method: "DELETE" });
},
pollFreeAsset(id: string) {
return request<{ asset: FreeAssetItem }>(`/api/assets/free-assets/${id}/poll/`, { method: "POST" });
},
recharge(payload: { amount: number | string; bonus_points?: number | string; channel?: string }) {
// amount 是真实支付 ¥,到账积分 = ¥ × points_per_yuan + bonus_points(赠送积分)
return request<RechargeResult>("/api/billing/recharge/", { method: "POST", body: JSON.stringify(payload) });
},
// 计费公共配置(积分汇率/视频毛利/预留 buffer):所有前端预估统一读这里,与后端计价引擎同源
billingConfig() {
return request<BillingConfigInfo>("/api/billing/config/");
},
// 收件箱按页拉取 —— 滚动加载逐页向后端要(tab/搜索 走服务端,计数随响应回来)
listNotifications(params?: { type?: string; unread?: boolean; search?: string; page?: number; pageSize?: number; excludeTypes?: string[] }) {
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));
for (const type of params?.excludeTypes || []) {
if (type) query.append("exclude_type", type);
}
const qs = query.toString();
return request<NotificationList>(`/api/ops/notifications/${qs ? `?${qs}` : ""}`);
},
// 侧边栏徽标(unread_count)+ 团队动态(只展示最近 6 条)用。只取第 1 页 100 条即足够,
// count 用后端真实总数(给「共 N」显示),不再随消息增长逐页翻全部(原来 O(N) 拖慢每次刷新)。
// 注:不参与收件箱滚动渲染——收件箱走 listNotifications 自己的服务端分页。
async allNotifications(): Promise<NotificationList> {
return request<NotificationList>(`/api/ops/notifications/?page_size=100`);
},
// 侧边栏未读徽标:只要 unread_count,取 1 条即可(不为徽标拉 100 条)
notificationsBadge() {
return request<NotificationList>(`/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<Notification>(`/api/ops/notifications/${id}/mark-read/`, { method: "POST" });
},
archiveNotification(id: string) {
// 后端 archive 端点返回 204 无 body
return request<void>(`/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<Paginated<Invitation>>(`/api/admin/invitations/${q ? `?${q}` : ""}`);
},
createInvite(payload?: { email?: string }) {
return request<Invitation>("/api/admin/invitations/", { method: "POST", body: JSON.stringify(payload || {}) });
},
revokeInvite(id: string) {
return request<Invitation>(`/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<Paginated<AdminTeam>>(`/api/admin/teams/${q ? `?${q}` : ""}`);
},
teamDetail(id: string) {
return request<AdminTeamDetail>(`/api/admin/teams/${id}/`);
},
setTeamPricing(id: string, priceMultiplier: string) {
return request<AdminTeam>(`/api/admin/teams/${id}/pricing/`, { method: "POST", body: JSON.stringify({ price_multiplier: priceMultiplier }) });
},
toggleTeam(id: string) {
return request<AdminTeam>(`/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<Paginated<AdminUser>>(`/api/admin/users/${q ? `?${q}` : ""}`);
},
toggleUser(id: string) {
return request<AdminUser>(`/api/admin/users/${id}/toggle/`, { method: "POST" });
},
resetUserPassword(id: string, password: string) {
return request<void>(`/api/admin/users/${id}/reset-password/`, { method: "POST", body: JSON.stringify({ password }) });
},
qualityWords() {
return request<AdminQualityWord[]>("/api/admin/quality-words/");
},
createQualityWord(payload: { stage: string; text: string; sort?: number; slot?: string }) {
return request<AdminQualityWord>("/api/admin/quality-words/", { method: "POST", body: JSON.stringify(payload) });
},
updateQualityWord(id: string, payload: { text?: string; sort?: number; enabled?: boolean }) {
return request<AdminQualityWord>(`/api/admin/quality-words/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
deleteQualityWord(id: string) {
return request<void>(`/api/admin/quality-words/${id}/`, { method: "DELETE" });
},
// 生图/视频提示词模板(视频线 6 条):列表 + 改正文/比例/启用
promptTemplates() {
return request<import("./types").AdminPromptTemplate[]>("/api/admin/prompt-templates/");
},
updatePromptTemplate(id: string, payload: { template?: string; ratio?: string; enabled?: boolean }) {
return request<import("./types").AdminPromptTemplate>(`/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<Paginated<AdminReviewAsset>>(`/api/admin/asset-reviews/${q ? `?${q}` : ""}`);
},
submitReviews(assetIds: string[]) {
return request<{ submitted: number; statuses: Record<string, string> }>(
"/api/admin/asset-reviews/submit/",
{ method: "POST", body: JSON.stringify({ asset_ids: assetIds }) }
);
},
pollReviews(assetIds?: string[]) {
return request<{ polled: number; statuses: Record<string, string> }>(
"/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<Paginated<AdminTask>>(`/api/admin/tasks/${q ? `?${q}` : ""}`);
},
taskDetail(id: string) {
return request<AdminTaskDetail>(`/api/admin/tasks/${id}/`);
},
retryTask(id: string) {
return request<{ retried: boolean; task_id: string }>(`/api/admin/tasks/${id}/retry/`, { method: "POST" });
},
pollTasks(taskIds?: string[]) {
return request<{ polled: number; statuses: Record<string, string> }>(
"/api/admin/tasks/poll/",
{ method: "POST", body: JSON.stringify(taskIds ? { task_ids: taskIds } : {}) }
);
},
reapTask(id: string) {
return request<AdminTask>(`/api/admin/tasks/${id}/reap/`, { 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<Paginated<AdminLedger>>(`/api/admin/ledgers/${q ? `?${q}` : ""}`);
},
billingConfig() {
return request<BillingConfigInfo & { updated_at: string }>("/api/admin/billing-config/");
},
updateBillingConfig(payload: { points_per_yuan?: string; video_margin_multiplier?: string; video_reserve_buffer?: string }) {
return request<BillingConfigInfo & { updated_at: string }>("/api/admin/billing-config/", { method: "PATCH", body: JSON.stringify(payload) });
},
adjustCredit(payload: { team: string; amount: string; reason: string }) {
return request<AdminLedger>("/api/admin/ledgers/adjust/", { method: "POST", body: JSON.stringify(payload) });
},
quotaPolicies(params?: { team?: string; page?: number; 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<Paginated<AdminQuotaPolicy>>(`/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<AdminQuotaPolicy>("/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<AdminQuotaPolicy>(`/api/admin/quota-policies/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
deleteQuotaPolicy(id: string) {
return request<void>(`/api/admin/quota-policies/${id}/`, { method: "DELETE" });
},
providers() {
return request<AdminProvider[]>("/api/admin/providers/");
},
createProvider(payload: { name: string; display_name: string; base_url?: string; api_key?: string; status?: string }) {
return request<AdminProvider>("/api/admin/providers/", { method: "POST", body: JSON.stringify(payload) });
},
updateProvider(id: string, payload: Record<string, unknown>) {
return request<AdminProvider>(`/api/admin/providers/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
deleteProvider(id: string) {
return request<void>(`/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<AdminModel[]>(`/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<AdminModel>("/api/admin/models/", { method: "POST", body: JSON.stringify(payload) });
},
updateModel(id: string, payload: Record<string, unknown>) {
return request<AdminModel>(`/api/admin/models/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
},
deleteModel(id: string) {
return request<void>(`/api/admin/models/${id}/`, { method: "DELETE" });
},
setDefaultModel(id: string) {
return request<AdminModel>(`/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<Paginated<AdminProjectRow>>(`/api/admin/projects/${q ? `?${q}` : ""}`);
},
integrity() {
return request<AdminIntegrity>("/api/admin/integrity/");
}
};