Your Name 3a444864ac feat: 腾讯云 COS 迁移至火山引擎 TOS 对象存储
签名算法从 COS HMAC-SHA1 改为 TOS V4 HMAC-SHA256,
更新前后端上传凭证字段、配置项、备份脚本和文档。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 11:02:15 +08:00

970 lines
25 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.

/**
* 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,
} 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<RefreshTokenResponse>(
`${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<SendEmailCodeResponse> {
const response = await this.client.post<SendEmailCodeResponse>('/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<LoginResponse> {
const response = await this.client.post<LoginResponse>('/auth/register', data)
setTokens(response.data.access_token, response.data.refresh_token)
return response.data
}
/**
* 用户登录
*/
async login(data: LoginRequest): Promise<LoginResponse> {
const response = await this.client.post<LoginResponse>('/auth/login', data)
setTokens(response.data.access_token, response.data.refresh_token)
return response.data
}
/**
* 退出登录
*/
async logout(): Promise<void> {
try {
await this.client.post('/auth/logout')
} finally {
clearTokens()
}
}
/**
* 刷新 Token
*/
async refreshToken(): Promise<RefreshTokenResponse> {
const refreshToken = getRefreshToken()
if (!refreshToken) {
throw new Error('No refresh token')
}
const response = await axios.post<RefreshTokenResponse>(
`${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<UploadPolicyResponse> {
const response = await this.client.post<UploadPolicyResponse>('/upload/policy', {
file_type: fileType,
})
return response.data
}
/**
* 文件上传完成回调
*/
async fileUploaded(fileKey: string, fileName: string, fileSize: number, fileType: string): Promise<FileUploadedResponse> {
const response = await this.client.post<FileUploadedResponse>('/upload/complete', {
file_key: fileKey,
file_name: fileName,
file_size: fileSize,
file_type: fileType,
})
return response.data
}
// ==================== 视频审核 ====================
/**
* 提交视频审核
*/
async submitVideoReview(data: VideoReviewRequest): Promise<VideoReviewResponse> {
const response = await this.client.post<VideoReviewResponse>('/videos/review', data)
return response.data
}
/**
* 查询审核进度
*/
async getReviewProgress(reviewId: string): Promise<ReviewProgressResponse> {
const response = await this.client.get<ReviewProgressResponse>(
`/videos/review/${reviewId}/progress`
)
return response.data
}
/**
* 查询审核结果
*/
async getReviewResult(reviewId: string): Promise<ReviewResultResponse> {
const response = await this.client.get<ReviewResultResponse>(
`/videos/review/${reviewId}/result`
)
return response.data
}
// ==================== 审核任务 ====================
/**
* 创建任务(代理商操作)
*/
async createTask(data: TaskCreateRequest): Promise<TaskResponse> {
const response = await this.client.post<TaskResponse>('/tasks', data)
return response.data
}
/**
* 查询任务列表
*/
async listTasks(page: number = 1, pageSize: number = 20, stage?: TaskStage): Promise<TaskListResponse> {
const response = await this.client.get<TaskListResponse>('/tasks', {
params: { page, page_size: pageSize, stage },
})
return response.data
}
/**
* 查询待审核任务列表
*/
async listPendingReviews(page: number = 1, pageSize: number = 20): Promise<ReviewTaskListResponse> {
const response = await this.client.get<ReviewTaskListResponse>('/tasks/pending', {
params: { page, page_size: pageSize },
})
return response.data
}
/**
* 查询任务详情
*/
async getTask(taskId: string): Promise<TaskResponse> {
const response = await this.client.get<TaskResponse>(`/tasks/${taskId}`)
return response.data
}
/**
* 上传/更新任务脚本
*/
async uploadTaskScript(taskId: string, payload: TaskScriptUploadRequest): Promise<TaskResponse> {
const response = await this.client.post<TaskResponse>(`/tasks/${taskId}/script`, payload)
return response.data
}
/**
* 上传/更新任务视频
*/
async uploadTaskVideo(taskId: string, payload: TaskVideoUploadRequest): Promise<TaskResponse> {
const response = await this.client.post<TaskResponse>(`/tasks/${taskId}/video`, payload)
return response.data
}
/**
* 审核脚本
*/
async reviewScript(taskId: string, data: TaskReviewRequest): Promise<TaskResponse> {
const response = await this.client.post<TaskResponse>(`/tasks/${taskId}/script/review`, data)
return response.data
}
/**
* 审核视频
*/
async reviewVideo(taskId: string, data: TaskReviewRequest): Promise<TaskResponse> {
const response = await this.client.post<TaskResponse>(`/tasks/${taskId}/video/review`, data)
return response.data
}
/**
* 提交申诉(达人操作)
*/
async submitAppeal(taskId: string, data: AppealRequest): Promise<TaskResponse> {
const response = await this.client.post<TaskResponse>(`/tasks/${taskId}/appeal`, data)
return response.data
}
/**
* 增加申诉次数(代理商操作)
*/
async increaseAppealCount(taskId: string): Promise<TaskResponse> {
const response = await this.client.post<TaskResponse>(`/tasks/${taskId}/appeal-count`)
return response.data
}
// ==================== 项目 ====================
/**
* 创建项目(品牌方操作)
*/
async createProject(data: ProjectCreateRequest): Promise<ProjectResponse> {
const response = await this.client.post<ProjectResponse>('/projects', data)
return response.data
}
/**
* 查询项目列表
*/
async listProjects(page: number = 1, pageSize: number = 20, status?: string): Promise<ProjectListResponse> {
const response = await this.client.get<ProjectListResponse>('/projects', {
params: { page, page_size: pageSize, status },
})
return response.data
}
/**
* 查询项目详情
*/
async getProject(projectId: string): Promise<ProjectResponse> {
const response = await this.client.get<ProjectResponse>(`/projects/${projectId}`)
return response.data
}
/**
* 更新项目
*/
async updateProject(projectId: string, data: ProjectUpdateRequest): Promise<ProjectResponse> {
const response = await this.client.put<ProjectResponse>(`/projects/${projectId}`, data)
return response.data
}
/**
* 分配代理商到项目
*/
async assignAgencies(projectId: string, agencyIds: string[]): Promise<ProjectResponse> {
const response = await this.client.post<ProjectResponse>(`/projects/${projectId}/agencies`, {
agency_ids: agencyIds,
})
return response.data
}
/**
* 从项目移除代理商
*/
async removeAgencyFromProject(projectId: string, agencyId: string): Promise<ProjectResponse> {
const response = await this.client.delete<ProjectResponse>(`/projects/${projectId}/agencies/${agencyId}`)
return response.data
}
// ==================== Brief ====================
/**
* 获取项目 Brief
*/
async getBrief(projectId: string): Promise<BriefResponse> {
const response = await this.client.get<BriefResponse>(`/projects/${projectId}/brief`)
return response.data
}
/**
* 创建项目 Brief
*/
async createBrief(projectId: string, data: BriefCreateRequest): Promise<BriefResponse> {
const response = await this.client.post<BriefResponse>(`/projects/${projectId}/brief`, data)
return response.data
}
/**
* 更新项目 Brief
*/
async updateBrief(projectId: string, data: BriefCreateRequest): Promise<BriefResponse> {
const response = await this.client.put<BriefResponse>(`/projects/${projectId}/brief`, data)
return response.data
}
// ==================== 组织关系 ====================
/**
* 品牌方:查询代理商列表
*/
async listBrandAgencies(): Promise<AgencyListResponse> {
const response = await this.client.get<AgencyListResponse>('/organizations/brand/agencies')
return response.data
}
/**
* 品牌方:邀请代理商
*/
async inviteAgency(agencyId: string): Promise<void> {
await this.client.post('/organizations/brand/agencies', { agency_id: agencyId })
}
/**
* 品牌方:移除代理商
*/
async removeAgency(agencyId: string): Promise<void> {
await this.client.delete(`/organizations/brand/agencies/${agencyId}`)
}
/**
* 品牌方:更新代理商权限
*/
async updateAgencyPermission(agencyId: string, forcePassEnabled: boolean): Promise<void> {
await this.client.put(`/organizations/brand/agencies/${agencyId}/permission`, {
force_pass_enabled: forcePassEnabled,
})
}
/**
* 代理商:查询达人列表
*/
async listAgencyCreators(): Promise<CreatorListResponse> {
const response = await this.client.get<CreatorListResponse>('/organizations/agency/creators')
return response.data
}
/**
* 代理商:邀请达人
*/
async inviteCreator(creatorId: string): Promise<void> {
await this.client.post('/organizations/agency/creators', { creator_id: creatorId })
}
/**
* 代理商:移除达人
*/
async removeCreator(creatorId: string): Promise<void> {
await this.client.delete(`/organizations/agency/creators/${creatorId}`)
}
/**
* 代理商:查询关联品牌方
*/
async listAgencyBrands(): Promise<BrandListResponse> {
const response = await this.client.get<BrandListResponse>('/organizations/agency/brands')
return response.data
}
/**
* 搜索代理商
*/
async searchAgencies(keyword: string): Promise<AgencyListResponse> {
const response = await this.client.get<AgencyListResponse>('/organizations/search/agencies', {
params: { keyword },
})
return response.data
}
/**
* 搜索达人
*/
async searchCreators(keyword: string): Promise<CreatorListResponse> {
const response = await this.client.get<CreatorListResponse>('/organizations/search/creators', {
params: { keyword },
})
return response.data
}
// ==================== 工作台统计 ====================
/**
* 达人工作台数据
*/
async getCreatorDashboard(): Promise<CreatorDashboard> {
const response = await this.client.get<CreatorDashboard>('/dashboard/creator')
return response.data
}
/**
* 代理商工作台数据
*/
async getAgencyDashboard(): Promise<AgencyDashboard> {
const response = await this.client.get<AgencyDashboard>('/dashboard/agency')
return response.data
}
/**
* 品牌方工作台数据
*/
async getBrandDashboard(): Promise<BrandDashboard> {
const response = await this.client.get<BrandDashboard>('/dashboard/brand')
return response.data
}
// ==================== 脚本预审 ====================
/**
* 脚本预审AI 审核)
*/
async reviewScriptContent(data: ScriptReviewRequest): Promise<ScriptReviewResponse> {
const response = await this.client.post<ScriptReviewResponse>('/scripts/review', data)
return response.data
}
// ==================== 规则管理 ====================
/**
* 查询违禁词列表
*/
async listForbiddenWords(category?: string): Promise<ForbiddenWordListResponse> {
const response = await this.client.get<ForbiddenWordListResponse>('/rules/forbidden-words', {
params: category ? { category } : undefined,
})
return response.data
}
/**
* 添加违禁词
*/
async addForbiddenWord(data: ForbiddenWordCreate): Promise<ForbiddenWordResponse> {
const response = await this.client.post<ForbiddenWordResponse>('/rules/forbidden-words', data)
return response.data
}
/**
* 删除违禁词
*/
async deleteForbiddenWord(wordId: string): Promise<void> {
await this.client.delete(`/rules/forbidden-words/${wordId}`)
}
/**
* 查询白名单
*/
async listWhitelist(brandId?: string): Promise<WhitelistListResponse> {
const response = await this.client.get<WhitelistListResponse>('/rules/whitelist', {
params: brandId ? { brand_id: brandId } : undefined,
})
return response.data
}
/**
* 添加白名单
*/
async addToWhitelist(data: WhitelistCreate): Promise<WhitelistResponse> {
const response = await this.client.post<WhitelistResponse>('/rules/whitelist', data)
return response.data
}
/**
* 查询竞品列表
*/
async listCompetitors(brandId?: string): Promise<CompetitorListResponse> {
const response = await this.client.get<CompetitorListResponse>('/rules/competitors', {
params: brandId ? { brand_id: brandId } : undefined,
})
return response.data
}
/**
* 添加竞品
*/
async addCompetitor(data: CompetitorCreate): Promise<CompetitorResponse> {
const response = await this.client.post<CompetitorResponse>('/rules/competitors', data)
return response.data
}
/**
* 删除竞品
*/
async deleteCompetitor(competitorId: string): Promise<void> {
await this.client.delete(`/rules/competitors/${competitorId}`)
}
/**
* 查询所有平台规则
*/
async listPlatformRules(): Promise<PlatformListResponse> {
const response = await this.client.get<PlatformListResponse>('/rules/platforms')
return response.data
}
/**
* 查询指定平台规则
*/
async getPlatformRules(platform: string): Promise<PlatformRuleResponse> {
const response = await this.client.get<PlatformRuleResponse>(`/rules/platforms/${platform}`)
return response.data
}
/**
* 规则冲突检测
*/
async validateRules(data: RuleValidateRequest): Promise<RuleValidateResponse> {
const response = await this.client.post<RuleValidateResponse>('/rules/validate', data)
return response.data
}
// ==================== AI 配置 ====================
/**
* 获取 AI 配置
*/
async getAIConfig(): Promise<AIConfigResponse> {
const response = await this.client.get<AIConfigResponse>('/ai-config')
return response.data
}
/**
* 更新 AI 配置
*/
async updateAIConfig(data: AIConfigUpdate): Promise<AIConfigResponse> {
const response = await this.client.put<AIConfigResponse>('/ai-config', data)
return response.data
}
/**
* 获取可用模型列表
*/
async getAIModels(data: GetModelsRequest): Promise<ModelsListResponse> {
const response = await this.client.post<ModelsListResponse>('/ai-config/models', data)
return response.data
}
/**
* 测试 AI 连接
*/
async testAIConnection(data: TestConnectionRequest): Promise<ConnectionTestResponse> {
const response = await this.client.post<ConnectionTestResponse>('/ai-config/test', data)
return response.data
}
// ==================== 用户资料 ====================
/**
* 获取当前用户资料
*/
async getProfile(): Promise<ProfileResponse> {
const response = await this.client.get<ProfileResponse>('/profile')
return response.data
}
/**
* 更新用户资料
*/
async updateProfile(data: ProfileUpdateRequest): Promise<ProfileResponse> {
const response = await this.client.put<ProfileResponse>('/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<MessageListResponse> {
const response = await this.client.get<MessageListResponse>('/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<void> {
await this.client.put(`/messages/${messageId}/read`)
}
/**
* 标记所有消息已读
*/
async markAllMessagesAsRead(): Promise<void> {
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