import axios from 'axios' import { isJwtExpired } from '@/utils/jwt' const updateClient = axios.create({ baseURL: import.meta.env.VITE_UPDATE_API_BASE_URL ?? '', timeout: 30000, }) if (import.meta.env.DEV) { updateClient.interceptors.request.use((config) => { console.debug('[tenant-platform][UPDATE] request', { method: config.method?.toUpperCase(), url: config.baseURL ? `${config.baseURL}${config.url ?? ''}` : config.url, params: config.params, }) return config }) updateClient.interceptors.response.use((res) => { console.debug('[tenant-platform][UPDATE] response', { status: res.status, url: res.config.url, }) return res }) } updateClient.interceptors.request.use((config) => { const url = config.url ?? '' const skipAuth = ( url.startsWith('/api/v1/updates/app/check') || url.startsWith('/api/v1/updates/app/inspect') || url.startsWith('/api/v1/rn/update/check') || url.startsWith('/api/v1/rn/inspect') || url.startsWith('/api/v1/rn/files/') ) if (!skipAuth) { const token = localStorage.getItem('token') if (token && !isJwtExpired(token)) { config.headers.Authorization = `Bearer ${token}` } else if (token && isJwtExpired(token)) { localStorage.removeItem('token') if (typeof window !== 'undefined' && window.location.pathname !== '/login') { window.location.href = `/login?reason=${encodeURIComponent('登录已失效,请重新登录')}` } return Promise.reject(new Error('登录已失效,请重新登录')) } } return config }) updateClient.interceptors.response.use( (response) => response, (error) => { const status = error?.response?.status if (status === 401) { localStorage.removeItem('token') if (typeof window !== 'undefined' && window.location.pathname !== '/login') { window.location.href = `/login?reason=${encodeURIComponent('登录已失效,请重新登录')}` } return Promise.reject(error) } if (status === 403) { return Promise.reject(error) } return Promise.reject(error) }, ) export type StoreType = 'HUAWEI' | 'MI' | 'OPPO' | 'VIVO' | 'HONOR' | 'APP_STORE' | 'GOOGLE_PLAY' | 'HARMONY_APP' | 'REVIEW_WEBHOOK' export type StoreReviewState = 'PENDING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED' export type PublishMode = 'MANUAL' | 'NOW' | 'SCHEDULED' | 'AUTO_REVIEW' export type GrayMode = 'PERCENT' | 'MEMBERS' export type GraySelectionSource = 'LOCAL' | 'CALLBACK' export interface PublishConfig { id: string appId: string configJson?: string updatedAt: string } export interface OperationLog { id: string appId: string resourceType: string resourceId: string action: string operator?: string reason?: string detailJson?: string createdAt: string } export interface GrayMember { userId: string name?: string groupName?: string extraJson?: string updatedAt?: string } export interface GrayMemberGroup { groupName: string members: GrayMember[] } export interface StoreConfig { id: string appId: string storeType: StoreType configJson?: string enabled: boolean updatedAt: string } export interface AppVersion { id: string appId: string platform: 'ANDROID' | 'IOS' | 'HARMONY' versionName: string versionCode: number packageName?: string downloadUrl?: string changeLog?: string forceUpdate: boolean publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' grayEnabled: boolean grayPercent: number appStoreUrl?: string marketUrl?: string scheduledPublishAt?: string autoPublishAfterReview: boolean webhookUrl?: string storeSubmitTargets?: string storeReviewStatus?: string storeSubmitMode?: PublishMode storeSubmitScheduledAt?: string grayMode?: GrayMode grayMemberIds?: string createdAt: string } export interface AppPackageInspectResult { platform: 'ANDROID' | 'IOS' | 'HARMONY' packageName?: string versionName?: string versionCode?: number fileName?: string detected: boolean } export interface RnBundle { id: string appId: string moduleId: string platform: 'ANDROID' | 'IOS' | 'HARMONY' version: string md5: string minCommonVersion?: string packageName?: string note?: string publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' publishMode?: PublishMode scheduledPublishAt?: string grayEnabled: boolean grayPercent: number grayMode?: GrayMode grayMemberIds?: string createdAt: string } export interface RnBundleInspectResult { moduleId?: string platform?: 'ANDROID' | 'IOS' | 'HARMONY' version?: string minCommonVersion?: string packageName?: string fileName?: string detected: boolean } export interface UnifiedAppUploadItem { fileKey: string platform: 'ANDROID' | 'IOS' | 'HARMONY' versionName: string versionCode: number changeLog?: string forceUpdate: boolean packageName?: string appStoreUrl?: string marketUrl?: string publishImmediately: boolean } export interface UnifiedRnUploadItem { fileKey: string moduleId: string platform: 'ANDROID' | 'IOS' | 'HARMONY' version: string minCommonVersion?: string packageName?: string note?: string } export interface UnifiedReleaseManifest { appVersions: UnifiedAppUploadItem[] rnBundles: UnifiedRnUploadItem[] } export const updateAdminApi = { listAppVersions(appId: string, platform: 'ANDROID' | 'IOS' | 'HARMONY') { return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', { params: { appId, platform }, }) }, publishAppVersion(id: string, body?: { publishImmediately?: boolean; scheduledPublishAt?: string; forceUpdate?: boolean }) { return updateClient.post(`/api/v1/updates/app/${id}/publish`, body ?? {}) }, unpublishAppVersion(id: string, reason: string) { return updateClient.post(`/api/v1/updates/app/${id}/unpublish`, { reason }) }, grayAppVersion(id: string, body: { enabled: boolean grayMode: GrayMode percent?: number memberIds?: string[] selectionSource?: GraySelectionSource }) { return updateClient.post(`/api/v1/updates/app/${id}/gray`, body) }, uploadAppVersion(formData: FormData) { return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData) }, inspectAppPackage(apkUrl: string) { return updateClient.get<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', { params: { apkUrl }, }) }, listRnBundles(appId: string, moduleId?: string, platform?: string) { return updateClient.get<{ data: RnBundle[] }>('/api/v1/rn/list', { params: { appId, ...(moduleId && { moduleId }), ...(platform && { platform }) }, }) }, publishRnBundle(id: string, body?: { publishImmediately?: boolean; scheduledPublishAt?: string }) { return updateClient.post(`/api/v1/rn/${id}/publish`, body ?? {}) }, unpublishRnBundle(id: string, reason: string) { return updateClient.post(`/api/v1/rn/${id}/unpublish`, { reason }) }, grayRnBundle(id: string, body: { enabled: boolean grayMode: GrayMode percent?: number memberIds?: string[] selectionSource?: GraySelectionSource }) { return updateClient.post(`/api/v1/rn/${id}/gray`, body) }, uploadRnBundle(formData: FormData) { return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData) }, inspectRnBundle(formData: FormData) { return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData) }, uploadUnifiedRelease(formData: FormData) { return updateClient.post('/api/v1/updates/unified/upload', formData) }, // ── Store config ──────────────────────────────────────────────────────── getStoreConfigs(appId: string) { return updateClient.get<{ data: StoreConfig[] }>('/api/v1/updates/store/configs', { params: { appId } }) }, saveStoreConfig(appId: string, storeType: StoreType, configJson: string, enabled: boolean) { return updateClient.put<{ data: StoreConfig }>( `/api/v1/updates/store/configs/${storeType}`, { configJson, enabled }, { params: { appId } }, ) }, deleteStoreConfig(appId: string, storeType: StoreType) { return updateClient.delete(`/api/v1/updates/store/configs/${storeType}`, { params: { appId } }) }, executeSubmitToStores( versionId: string, storeTypes: StoreType[], submitMode: PublishMode = 'MANUAL', scheduledPublishAt?: string, autoPublishAfterReview = false, ) { return updateClient.post<{ data: AppVersion }>( `/api/v1/updates/store/app/${versionId}/execute-submit`, { storeTypes, submitMode, scheduledPublishAt, autoPublishAfterReview }, ) }, updateStoreReview(versionId: string, storeType: StoreType, state: StoreReviewState) { return updateClient.post<{ data: AppVersion }>( `/api/v1/updates/store/app/${versionId}/review`, { storeType, state }, ) }, getPublishConfig(appId: string) { return updateClient.get<{ data: PublishConfig }>('/api/v1/updates/publish/config', { params: { appId } }) }, savePublishConfig(appId: string, config: Record) { return updateClient.put<{ data: PublishConfig }>('/api/v1/updates/publish/config', config, { params: { appId } }) }, listOperationLogs(appId: string, limit = 100) { return updateClient.get<{ data: OperationLog[] }>('/api/v1/updates/ops/logs', { params: { appId, limit }, }) }, listGrayMembers(appId: string, keyword?: string, groupName?: string) { return updateClient.get<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members', { params: { appId, ...(keyword && { keyword }), ...(groupName && { groupName }) }, }) }, syncGrayMembers(appId: string) { return updateClient.post<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members/sync', null, { params: { appId }, }) }, }