/** * API 客户端 * 支持双 Token JWT 认证 */ import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios' import type { VideoReviewRequest, VideoReviewResponse, ReviewProgressResponse, ReviewResultResponse, ScriptReviewRequest, ScriptReviewResponse, } from '@/types/review' import type { TaskResponse, TaskListResponse, TaskScriptUploadRequest, TaskVideoUploadRequest, TaskCreateRequest, TaskReviewRequest, TaskStage, ReviewTaskListResponse, AppealRequest, } from '@/types/task' import type { ProjectResponse, ProjectListResponse, ProjectCreateRequest, ProjectUpdateRequest, } from '@/types/project' import type { BriefResponse, BriefCreateRequest, } from '@/types/brief' import type { AgencyListResponse, CreatorListResponse, BrandListResponse, } from '@/types/organization' import type { CreatorDashboard, AgencyDashboard, BrandDashboard, } from '@/types/dashboard' import type { ForbiddenWordCreate, ForbiddenWordResponse, ForbiddenWordListResponse, WhitelistCreate, WhitelistResponse, WhitelistListResponse, CompetitorCreate, CompetitorResponse, CompetitorListResponse, PlatformRuleResponse, PlatformListResponse, RuleValidateRequest, RuleValidateResponse, PlatformRuleParseRequest, PlatformRuleParseResponse, PlatformRuleConfirmRequest, BrandPlatformRuleResponse, BrandPlatformRuleListResponse, } from '@/types/rules' import type { AIConfigUpdate, AIConfigResponse, GetModelsRequest, ModelsListResponse, TestConnectionRequest, ConnectionTestResponse, } from '@/types/ai-config' const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000' const STORAGE_KEY_ACCESS = 'miaosi_access_token' const STORAGE_KEY_REFRESH = 'miaosi_refresh_token' // ==================== 类型定义 ==================== export type UserRole = 'brand' | 'agency' | 'creator' export interface User { id: string email?: string phone?: string name: string avatar?: string role: UserRole is_verified: boolean brand_id?: string agency_id?: string creator_id?: string tenant_id?: string tenant_name?: string } export interface LoginRequest { email?: string phone?: string password?: string email_code?: string } export interface RegisterRequest { email: string phone?: string password: string name: string role: UserRole email_code: string } export interface SendEmailCodeRequest { email: string purpose: 'register' | 'login' | 'reset_password' } export interface SendEmailCodeResponse { message: string expires_in: number } export interface ResetPasswordRequest { email: string email_code: string new_password: string } export interface LoginResponse { access_token: string refresh_token: string token_type: string expires_in: number user: User } export interface RefreshTokenResponse { access_token: string token_type: string expires_in: number } export interface UploadPolicyResponse { x_tos_algorithm: string x_tos_credential: string x_tos_date: string x_tos_signature: string policy: string host: string dir: string expire: number max_size_mb: number } export interface FileUploadedResponse { url: string file_key: string file_name: string file_size: number file_type: string } // ==================== 用户资料类型 ==================== export interface BrandProfileInfo { id: string name: string logo?: string description?: string contact_name?: string contact_phone?: string contact_email?: string } export interface AgencyProfileInfo { id: string name: string logo?: string description?: string contact_name?: string contact_phone?: string contact_email?: string } export interface CreatorProfileInfo { id: string name: string avatar?: string bio?: string douyin_account?: string xiaohongshu_account?: string bilibili_account?: string } export interface ProfileResponse { id: string email?: string phone?: string name: string avatar?: string role: string is_verified: boolean created_at?: string brand?: BrandProfileInfo agency?: AgencyProfileInfo creator?: CreatorProfileInfo } export interface ProfileUpdateRequest { name?: string avatar?: string phone?: string description?: string contact_name?: string contact_phone?: string contact_email?: string bio?: string douyin_account?: string xiaohongshu_account?: string bilibili_account?: string } export interface ChangePasswordRequest { old_password: string new_password: string } // ==================== 消息类型 ==================== export interface MessageItem { id: string type: string title: string content: string is_read: boolean related_task_id?: string related_project_id?: string sender_name?: string created_at?: string } export interface MessageListResponse { items: MessageItem[] total: number page: number page_size: number } // ==================== Token 管理 ==================== function getAccessToken(): string | null { if (typeof window === 'undefined') return null return localStorage.getItem(STORAGE_KEY_ACCESS) } function getRefreshToken(): string | null { if (typeof window === 'undefined') return null return localStorage.getItem(STORAGE_KEY_REFRESH) } function setTokens(accessToken: string, refreshToken: string): void { if (typeof window === 'undefined') return localStorage.setItem(STORAGE_KEY_ACCESS, accessToken) localStorage.setItem(STORAGE_KEY_REFRESH, refreshToken) } function clearTokens(): void { if (typeof window === 'undefined') return localStorage.removeItem(STORAGE_KEY_ACCESS) localStorage.removeItem(STORAGE_KEY_REFRESH) } // ==================== API 客户端 ==================== class ApiClient { private client: AxiosInstance private tenantId: string = 'default' private isRefreshing = false private refreshSubscribers: Array<(token: string) => void> = [] constructor() { this.client = axios.create({ baseURL: `${API_BASE_URL}/api/v1`, timeout: 30000, headers: { 'Content-Type': 'application/json', }, }) this.setupInterceptors() } private setupInterceptors() { // 请求拦截器:添加 Token 和租户 ID this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => { const token = getAccessToken() if (token) { config.headers.Authorization = `Bearer ${token}` } config.headers['X-Tenant-ID'] = this.tenantId return config }) // 响应拦截器:处理 401 错误 this.client.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean } // 如果是 401 错误且不是刷新 Token 的请求 if (error.response?.status === 401 && !originalRequest._retry) { if (this.isRefreshing) { // 如果正在刷新,等待刷新完成后重试 return new Promise((resolve) => { this.refreshSubscribers.push((token: string) => { originalRequest.headers.Authorization = `Bearer ${token}` resolve(this.client(originalRequest)) }) }) } originalRequest._retry = true this.isRefreshing = true try { const refreshToken = getRefreshToken() if (!refreshToken) { throw new Error('No refresh token') } const response = await axios.post( `${API_BASE_URL}/api/v1/auth/refresh`, { refresh_token: refreshToken } ) const newAccessToken = response.data.access_token setTokens(newAccessToken, refreshToken) // 通知所有等待的请求 this.refreshSubscribers.forEach((callback) => callback(newAccessToken)) this.refreshSubscribers = [] // 重试原请求 originalRequest.headers.Authorization = `Bearer ${newAccessToken}` return this.client(originalRequest) } catch (refreshError) { // 刷新失败,清除 Token 并跳转登录 clearTokens() if (typeof window !== 'undefined') { window.location.href = '/login' } return Promise.reject(refreshError) } finally { this.isRefreshing = false } } const message = (error.response?.data as { detail?: string })?.detail || error.message || '请求失败' return Promise.reject(new Error(message)) } ) } setTenantId(tenantId: string) { this.tenantId = tenantId } // ==================== 认证 ==================== /** * 发送邮箱验证码 */ async sendEmailCode(data: SendEmailCodeRequest): Promise { const response = await this.client.post('/auth/send-code', data) return response.data } /** * 重置密码 */ async resetPassword(data: ResetPasswordRequest): Promise<{ message: string }> { const response = await this.client.post<{ message: string }>('/auth/reset-password', data) return response.data } /** * 用户注册 */ async register(data: RegisterRequest): Promise { const response = await this.client.post('/auth/register', data) setTokens(response.data.access_token, response.data.refresh_token) return response.data } /** * 用户登录 */ async login(data: LoginRequest): Promise { const response = await this.client.post('/auth/login', data) setTokens(response.data.access_token, response.data.refresh_token) return response.data } /** * 退出登录 */ async logout(): Promise { try { await this.client.post('/auth/logout') } finally { clearTokens() } } /** * 刷新 Token */ async refreshToken(): Promise { const refreshToken = getRefreshToken() if (!refreshToken) { throw new Error('No refresh token') } const response = await axios.post( `${API_BASE_URL}/api/v1/auth/refresh`, { refresh_token: refreshToken } ) setTokens(response.data.access_token, refreshToken) return response.data } // ==================== 文件上传 ==================== /** * 获取 TOS 上传凭证 */ async getUploadPolicy(fileType: string = 'general'): Promise { const response = await this.client.post('/upload/policy', { file_type: fileType, }) return response.data } /** * 文件上传完成回调 */ async fileUploaded(fileKey: string, fileName: string, fileSize: number, fileType: string): Promise { const response = await this.client.post('/upload/complete', { file_key: fileKey, file_name: fileName, file_size: fileSize, file_type: fileType, }) return response.data } /** * 后端代理上传(绕过浏览器直传 TOS 的 CORS/代理问题) */ async proxyUpload(file: File, fileType: string = 'general', onProgress?: (pct: number) => void): Promise { const formData = new FormData() formData.append('file', file) formData.append('file_type', fileType) const response = await this.client.post('/upload/proxy', formData, { timeout: 300000, headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total && onProgress) onProgress(Math.round((e.loaded / e.total) * 100)) }, }) return response.data } /** * 获取私有桶文件的预签名访问 URL */ async getSignedUrl(url: string): Promise { const response = await this.client.get<{ signed_url: string; expire_seconds: number }>( '/upload/sign-url', { params: { url } } ) return response.data.signed_url } /** * 通过后端代理获取文件预览 Blob URL(用于 iframe/img src) */ async getPreviewUrl(fileUrl: string): Promise { const response = await this.client.get('/upload/preview', { params: { url: fileUrl }, responseType: 'blob', }) return URL.createObjectURL(response.data) } /** * 通过后端代理下载文件(避免 TOS CORS 问题) */ async downloadFile(fileUrl: string, filename: string): Promise { const response = await this.client.get('/upload/download', { params: { url: fileUrl }, responseType: 'blob', }) const blob = new Blob([response.data]) const blobUrl = URL.createObjectURL(blob) const a = document.createElement('a') a.href = blobUrl a.download = filename document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(blobUrl) } // ==================== 视频审核 ==================== /** * 提交视频审核 */ async submitVideoReview(data: VideoReviewRequest): Promise { const response = await this.client.post('/videos/review', data) return response.data } /** * 查询审核进度 */ async getReviewProgress(reviewId: string): Promise { const response = await this.client.get( `/videos/review/${reviewId}/progress` ) return response.data } /** * 查询审核结果 */ async getReviewResult(reviewId: string): Promise { const response = await this.client.get( `/videos/review/${reviewId}/result` ) return response.data } // ==================== 审核任务 ==================== /** * 创建任务(代理商操作) */ async createTask(data: TaskCreateRequest): Promise { const response = await this.client.post('/tasks', data) return response.data } /** * 查询任务列表 */ async listTasks(page: number = 1, pageSize: number = 20, stage?: TaskStage, projectId?: string): Promise { const response = await this.client.get('/tasks', { params: { page, page_size: pageSize, stage, project_id: projectId }, }) return response.data } /** * 查询待审核任务列表 */ async listPendingReviews(page: number = 1, pageSize: number = 20): Promise { const response = await this.client.get('/tasks/pending', { params: { page, page_size: pageSize }, }) return response.data } /** * 查询任务详情 */ async getTask(taskId: string): Promise { const response = await this.client.get(`/tasks/${taskId}`) return response.data } /** * 上传/更新任务脚本 */ async uploadTaskScript(taskId: string, payload: TaskScriptUploadRequest): Promise { const response = await this.client.post(`/tasks/${taskId}/script`, payload) return response.data } /** * 上传/更新任务视频 */ async uploadTaskVideo(taskId: string, payload: TaskVideoUploadRequest): Promise { const response = await this.client.post(`/tasks/${taskId}/video`, payload) return response.data } /** * 审核脚本 */ async reviewScript(taskId: string, data: TaskReviewRequest): Promise { const response = await this.client.post(`/tasks/${taskId}/script/review`, data) return response.data } /** * 审核视频 */ async reviewVideo(taskId: string, data: TaskReviewRequest): Promise { const response = await this.client.post(`/tasks/${taskId}/video/review`, data) return response.data } /** * 提交申诉(达人操作) */ async submitAppeal(taskId: string, data: AppealRequest): Promise { const response = await this.client.post(`/tasks/${taskId}/appeal`, data) return response.data } /** * 增加申诉次数(代理商操作) */ async increaseAppealCount(taskId: string): Promise { const response = await this.client.post(`/tasks/${taskId}/appeal-count`) return response.data } // ==================== 项目 ==================== /** * 创建项目(品牌方操作) */ async createProject(data: ProjectCreateRequest): Promise { const response = await this.client.post('/projects', data) return response.data } /** * 查询项目列表 */ async listProjects(page: number = 1, pageSize: number = 20, status?: string): Promise { const response = await this.client.get('/projects', { params: { page, page_size: pageSize, status }, }) return response.data } /** * 查询项目详情 */ async getProject(projectId: string): Promise { const response = await this.client.get(`/projects/${projectId}`) return response.data } /** * 更新项目 */ async updateProject(projectId: string, data: ProjectUpdateRequest): Promise { const response = await this.client.put(`/projects/${projectId}`, data) return response.data } /** * 分配代理商到项目 */ async assignAgencies(projectId: string, agencyIds: string[]): Promise { const response = await this.client.post(`/projects/${projectId}/agencies`, { agency_ids: agencyIds, }) return response.data } /** * 从项目移除代理商 */ async removeAgencyFromProject(projectId: string, agencyId: string): Promise { const response = await this.client.delete(`/projects/${projectId}/agencies/${agencyId}`) return response.data } // ==================== Brief ==================== /** * 获取项目 Brief */ async getBrief(projectId: string): Promise { const response = await this.client.get(`/projects/${projectId}/brief`) return response.data } /** * 创建项目 Brief */ async createBrief(projectId: string, data: BriefCreateRequest): Promise { const response = await this.client.post(`/projects/${projectId}/brief`, data) return response.data } /** * 更新项目 Brief */ async updateBrief(projectId: string, data: BriefCreateRequest): Promise { const response = await this.client.put(`/projects/${projectId}/brief`, data) return response.data } /** * 代理商更新 Brief(agency_attachments + selling_points + blacklist_words) */ async updateBriefByAgency(projectId: string, data: { agency_attachments?: Array<{ id?: string; name: string; url: string; size?: string }> selling_points?: Array<{ content: string; required: boolean }> blacklist_words?: Array<{ word: string; reason: string }> brand_tone?: string other_requirements?: string min_selling_points?: number | null }): Promise { const response = await this.client.patch(`/projects/${projectId}/brief/agency-attachments`, data) return response.data } /** * AI 解析 Brief 文档 */ async parseBrief(projectId: string): Promise<{ product_name: string target_audience: string content_requirements: string selling_points: Array<{ content: string; required: boolean }> blacklist_words: Array<{ word: string; reason: string }> }> { const response = await this.client.post(`/projects/${projectId}/brief/parse`, null, { timeout: 180000, // 3 分钟,文档下载 + AI 解析较慢 }) return response.data } // ==================== 组织关系 ==================== /** * 品牌方:查询代理商列表 */ async listBrandAgencies(): Promise { const response = await this.client.get('/organizations/brand/agencies') return response.data } /** * 品牌方:邀请代理商 */ async inviteAgency(agencyId: string): Promise { await this.client.post('/organizations/brand/agencies', { agency_id: agencyId }) } /** * 品牌方:移除代理商 */ async removeAgency(agencyId: string): Promise { await this.client.delete(`/organizations/brand/agencies/${agencyId}`) } /** * 品牌方:更新代理商权限 */ async updateAgencyPermission(agencyId: string, forcePassEnabled: boolean): Promise { await this.client.put(`/organizations/brand/agencies/${agencyId}/permission`, { force_pass_enabled: forcePassEnabled, }) } /** * 代理商:查询达人列表 */ async listAgencyCreators(): Promise { const response = await this.client.get('/organizations/agency/creators') return response.data } /** * 代理商:邀请达人 */ async inviteCreator(creatorId: string): Promise { await this.client.post('/organizations/agency/creators', { creator_id: creatorId }) } /** * 代理商:移除达人 */ async removeCreator(creatorId: string): Promise { await this.client.delete(`/organizations/agency/creators/${creatorId}`) } /** * 代理商:查询关联品牌方 */ async listAgencyBrands(): Promise { const response = await this.client.get('/organizations/agency/brands') return response.data } /** * 搜索代理商 */ async searchAgencies(keyword: string): Promise { const response = await this.client.get('/organizations/search/agencies', { params: { keyword }, }) return response.data } /** * 搜索达人 */ async searchCreators(keyword: string): Promise { const response = await this.client.get('/organizations/search/creators', { params: { keyword }, }) return response.data } // ==================== 工作台统计 ==================== /** * 达人工作台数据 */ async getCreatorDashboard(): Promise { const response = await this.client.get('/dashboard/creator') return response.data } /** * 代理商工作台数据 */ async getAgencyDashboard(): Promise { const response = await this.client.get('/dashboard/agency') return response.data } /** * 品牌方工作台数据 */ async getBrandDashboard(): Promise { const response = await this.client.get('/dashboard/brand') return response.data } // ==================== 脚本预审 ==================== /** * 脚本预审(AI 审核) */ async reviewScriptContent(data: ScriptReviewRequest): Promise { const response = await this.client.post('/scripts/review', data) return response.data } // ==================== 规则管理 ==================== /** * 查询违禁词列表 */ async listForbiddenWords(category?: string): Promise { const response = await this.client.get('/rules/forbidden-words', { params: category ? { category } : undefined, }) return response.data } /** * 添加违禁词 */ async addForbiddenWord(data: ForbiddenWordCreate): Promise { const response = await this.client.post('/rules/forbidden-words', data) return response.data } /** * 删除违禁词 */ async deleteForbiddenWord(wordId: string): Promise { await this.client.delete(`/rules/forbidden-words/${wordId}`) } /** * 查询白名单 */ async listWhitelist(brandId?: string): Promise { const response = await this.client.get('/rules/whitelist', { params: brandId ? { brand_id: brandId } : undefined, }) return response.data } /** * 添加白名单 */ async addToWhitelist(data: WhitelistCreate): Promise { const response = await this.client.post('/rules/whitelist', data) return response.data } /** * 删除白名单 */ async deleteWhitelistItem(id: string): Promise { await this.client.delete(`/rules/whitelist/${id}`) } /** * 查询竞品列表 */ async listCompetitors(brandId?: string): Promise { const response = await this.client.get('/rules/competitors', { params: brandId ? { brand_id: brandId } : undefined, }) return response.data } /** * 添加竞品 */ async addCompetitor(data: CompetitorCreate): Promise { const response = await this.client.post('/rules/competitors', data) return response.data } /** * 删除竞品 */ async deleteCompetitor(competitorId: string): Promise { await this.client.delete(`/rules/competitors/${competitorId}`) } /** * 查询所有平台规则 */ async listPlatformRules(): Promise { const response = await this.client.get('/rules/platforms') return response.data } /** * 查询指定平台规则 */ async getPlatformRules(platform: string): Promise { const response = await this.client.get(`/rules/platforms/${platform}`) return response.data } /** * 规则冲突检测 */ async validateRules(data: RuleValidateRequest): Promise { const response = await this.client.post('/rules/validate', data) return response.data } /** * 上传文档并 AI 解析平台规则 */ async parsePlatformRule(data: PlatformRuleParseRequest): Promise { const response = await this.client.post('/rules/platform-rules/parse', data, { timeout: 180000, // 3 分钟,视觉模型解析图片 PDF 较慢 }) return response.data } /** * 确认/编辑平台规则 */ async confirmPlatformRule(ruleId: string, data: PlatformRuleConfirmRequest): Promise { const response = await this.client.put(`/rules/platform-rules/${ruleId}/confirm`, data) return response.data } /** * 查询品牌方平台规则列表 */ async listBrandPlatformRules(params?: { brand_id?: string; platform?: string; status?: string }): Promise { const response = await this.client.get('/rules/platform-rules', { params }) return response.data } /** * 删除平台规则 */ async deletePlatformRule(ruleId: string): Promise { await this.client.delete(`/rules/platform-rules/${ruleId}`) } // ==================== AI 配置 ==================== /** * 获取 AI 配置 */ async getAIConfig(): Promise { const response = await this.client.get('/ai-config') return response.data } /** * 更新 AI 配置 */ async updateAIConfig(data: AIConfigUpdate): Promise { const response = await this.client.put('/ai-config', data) return response.data } /** * 获取可用模型列表 */ async getAIModels(data: GetModelsRequest): Promise { const response = await this.client.post('/ai-config/models', data) return response.data } /** * 测试 AI 连接 */ async testAIConnection(data: TestConnectionRequest): Promise { const response = await this.client.post('/ai-config/test', data) return response.data } // ==================== 用户资料 ==================== /** * 获取当前用户资料 */ async getProfile(): Promise { const response = await this.client.get('/profile') return response.data } /** * 更新用户资料 */ async updateProfile(data: ProfileUpdateRequest): Promise { const response = await this.client.put('/profile', data) return response.data } /** * 修改密码 */ async changePassword(data: ChangePasswordRequest): Promise<{ message: string }> { const response = await this.client.put<{ message: string }>('/profile/password', data) return response.data } // ==================== 消息/通知 ==================== /** * 获取消息列表 */ async getMessages(params?: { page?: number; page_size?: number; is_read?: boolean; type?: string }): Promise { const response = await this.client.get('/messages', { params }) return response.data } /** * 获取未读消息数 */ async getUnreadCount(): Promise<{ count: number }> { const response = await this.client.get<{ count: number }>('/messages/unread-count') return response.data } /** * 标记单条消息已读 */ async markMessageAsRead(messageId: string): Promise { await this.client.put(`/messages/${messageId}/read`) } /** * 标记所有消息已读 */ async markAllMessagesAsRead(): Promise { await this.client.put('/messages/read-all') } // ==================== 健康检查 ==================== /** * 健康检查 */ async healthCheck(): Promise<{ status: string; version: string }> { const response = await this.client.get('/health') return response.data } } // 单例导出 export const api = new ApiClient() // 导出 Token 管理函数供其他模块使用 export { getAccessToken, getRefreshToken, setTokens, clearTokens } export default api