import axios, { type AxiosProgressEvent } from 'axios' import { isJwtExpired } from '@/utils/jwt' const updateClient = axios.create({ baseURL: import.meta.env.VITE_UPDATE_API_BASE_URL ?? '', timeout: 30000, }) type UploadProgressHandler = (percent: number) => void function uploadProgressConfig(onProgress?: UploadProgressHandler) { if (!onProgress) return {} return { onUploadProgress: (event: AxiosProgressEvent) => { const total = event.total ?? 0 if (!total) return onProgress(Math.min(100, Math.round((event.loaded * 100) / total))) }, } } 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/updates/public/') || url.startsWith('/api/v1/rn/release-set/check') || 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' | 'SUBMITTING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED' export type PublishMode = 'MANUAL' | 'NOW' | 'SCHEDULED' | 'AUTO_REVIEW' export type GrayMode = 'PERCENT' | 'MEMBERS' export interface PublishConfig { id: string appKey: string configJson?: string updatedAt: string allowAnonymousUpdateCheck?: boolean } export interface OperationLog { id: string appKey: string resourceType: string resourceId: string action: string operator?: string reason?: string detailJson?: string createdAt: string } export interface GrayMember { userId: string name: string source: 'SYNC' | 'MANUAL' active: boolean tags: string[] } export interface GrayTag { tagName: string memberCount: number } export interface StoreConfig { id: string appKey: string storeType: StoreType configJson?: string enabled: boolean updatedAt: string } export type StoreMonitoringType = 'APP_STORE' | 'HARMONY_APP' export type StoreMonitoringPlatform = 'IOS' | 'HARMONY' export type StoreVersionState = | 'DISCOVERED' | 'PREPARING' | 'SUBMITTED' | 'WAITING_FOR_REVIEW' | 'IN_REVIEW' | 'REJECTED' | 'APPROVED_PENDING_RELEASE' | 'RELEASE_SCHEDULED' | 'RELEASE_REQUESTED' | 'PROCESSING_DISTRIBUTION' | 'PARTIALLY_AVAILABLE' | 'AVAILABLE' | 'WITHDRAWN' | 'REMOVED' | 'UNKNOWN' | 'SYNC_FAILED' export interface StoreMonitoringBinding { id: string appKey: string platform: StoreMonitoringPlatform storeType: StoreMonitoringType packageIdentifier: string officialAppId?: string marketUrl?: string targetRegion: 'CHN' enabled: boolean monitorCredentialConfigured: boolean lastSyncStatus?: string lastSyncError?: string lastSyncedAt?: string } export interface StoreMonitoringBindingInput { packageIdentifier: string marketUrl?: string enabled: boolean credentialDisplayName?: string credential?: Record } export interface StoreMonitoringVersion { id: string officialVersionId: string versionName: string buildVersion: string source: 'XUQM_PLATFORM' | 'EXTERNAL_STORE' state: StoreVersionState rawState?: string chinaAvailable: boolean releaseSequence?: number stateOccurredAt?: string lastSyncedAt?: string } export interface StoreMonitoringEvent { id: string eventType: string source: 'WEBHOOK' | 'POLL' | 'MANUAL' | 'IMPORT' previousState?: StoreVersionState currentState?: StoreVersionState rawState?: string occurredAt?: string receivedAt?: string } export interface StoreUpdateDraft { storeVersionId: string appVersionId: string platform: StoreMonitoringPlatform versionName: string buildVersion: string releaseSequence: number publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' storeUrl?: string } export interface StoreNotificationPolicy { emailEnabled: boolean emailRecipients: string[] webhookEnabled: boolean webhookUrl?: string webhookSecretConfigured: boolean eventTypes: string[] updatedAt?: string } export interface StoreNotificationPolicyInput { emailEnabled: boolean emailRecipients: string[] webhookEnabled: boolean webhookUrl?: string webhookSecret?: string eventTypes: string[] } export interface AppVersion { id: string appKey: 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 StoreRemoteStateDto { storeType: StoreType reviewState: 'ONLINE' | 'UNDER_REVIEW' | 'UNDER_REVIEW_XIAOMI' | 'REJECTED' | 'NOT_FOUND' | 'UNKNOWN' | 'QUERY_FAILED' onlineVersionName?: string onlineVersionCode?: string reviewVersionName?: string reviewVersionCode?: string currentSubmissionLive: boolean nonCurrentRelease: boolean canSubmit: boolean blockReason?: string } export interface PreflightSubmitResultDto { versionId: string versionName: string versionCode: number stores: StoreRemoteStateDto[] } export interface AppPackageInspectResult { platform: 'ANDROID' | 'IOS' | 'HARMONY' packageName?: string versionName?: string versionCode?: number fileName?: string detected: boolean } export interface RnBundle { id: string appKey: string packageName: string moduleId: string type: 'common' | 'app' | 'buz' platform: 'ANDROID' | 'IOS' version: string appVersionRange: string builtAgainstNativeBaselineId: string buildId: string bundleFormat: string commonVersionRange?: string minNativeApiLevel: number bundleSha256: string archiveSha256: string keyId: string signature: string note?: string publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' publishMode?: PublishMode scheduledPublishAt?: string grayEnabled: boolean grayPercent: number grayMode?: GrayMode grayMemberIds?: string createdAt: string } 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 UnifiedReleaseManifest { appVersions: UnifiedAppUploadItem[] } export interface DownloadPageHistoryItem { versionName: string versionCode: number downloadUrl?: string available: boolean changeLog?: string publishedAt: string current: boolean } export interface DownloadPageAppInfo { versionName: string versionCode: number downloadUrl?: string available?: boolean changeLog?: string storeUrl?: string storeLinks?: Record publishedAt: string history?: DownloadPageHistoryItem[] } export interface DownloadPageInfo { android: DownloadPageAppInfo | null ios: DownloadPageAppInfo | null harmony: DownloadPageAppInfo | null } export function fetchDownloadPageInfo(appKey: string) { return updateClient.get<{ data: DownloadPageInfo }>('/api/v1/updates/public/download-info', { params: { appKey }, }) } export const updateAdminApi = { listAppVersions(appKey: string, platform: 'ANDROID' | 'IOS' | 'HARMONY') { return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', { params: { appKey, 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 }) }, patchChangeLog(id: string, changeLog: string) { return updateClient.patch<{ data: AppVersion }>(`/api/v1/updates/app/${id}/changelog`, { changeLog }) }, grayAppVersion(id: string, body: { enabled: boolean grayMode: GrayMode percent?: number groupNames?: string[] extraMemberIds?: string[] }) { return updateClient.post(`/api/v1/updates/app/${id}/gray`, body) }, uploadAppVersion(formData: FormData, onProgress?: UploadProgressHandler) { return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData, uploadProgressConfig(onProgress)) }, inspectAppPackage(apkUrl: string) { return updateClient.get<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', { params: { apkUrl }, }) }, listRnBundles(appKey: string, moduleId?: string, platform?: string) { return updateClient.get<{ data: RnBundle[] }>('/api/v1/rn/list', { params: { appKey, ...(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 groupNames?: string[] extraMemberIds?: string[] }) { return updateClient.post(`/api/v1/rn/${id}/gray`, body) }, uploadRnBundle(formData: FormData, onProgress?: UploadProgressHandler) { return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, uploadProgressConfig(onProgress)) }, uploadUnifiedRelease(formData: FormData, onProgress?: UploadProgressHandler) { return updateClient.post('/api/v1/updates/unified/upload', formData, uploadProgressConfig(onProgress)) }, // ── Store config ──────────────────────────────────────────────────────── getStoreConfigs(appKey: string) { return updateClient.get<{ data: StoreConfig[] }>('/api/v1/updates/store/configs', { params: { appKey } }) }, saveStoreConfig(appKey: string, storeType: StoreType, configJson: string, enabled: boolean) { return updateClient.put<{ data: StoreConfig }>( `/api/v1/updates/store/configs/${storeType}`, { configJson, enabled }, { params: { appKey } }, ) }, deleteStoreConfig(appKey: string, storeType: StoreType) { return updateClient.delete(`/api/v1/updates/store/configs/${storeType}`, { params: { appKey } }) }, // ── iOS / Harmony official store read-only monitoring ────────────────── listStoreMonitoringBindings(appKey: string) { return updateClient.get<{ data: StoreMonitoringBinding[] }>( '/api/v1/updates/store-monitoring/bindings', { params: { appKey } }, ) }, saveStoreMonitoringBinding( appKey: string, storeType: StoreMonitoringType, body: StoreMonitoringBindingInput, ) { return updateClient.put<{ data: StoreMonitoringBinding }>( `/api/v1/updates/store-monitoring/bindings/${storeType}`, body, { params: { appKey } }, ) }, synchronizeStoreMonitoring(appKey: string, storeType: StoreMonitoringType) { return updateClient.post<{ data: StoreMonitoringVersion[] }>( `/api/v1/updates/store-monitoring/bindings/${storeType}/sync`, null, { params: { appKey } }, ) }, listStoreMonitoringVersions(appKey: string, bindingId: string) { return updateClient.get<{ data: StoreMonitoringVersion[] }>( `/api/v1/updates/store-monitoring/bindings/${bindingId}/versions`, { params: { appKey } }, ) }, listStoreMonitoringEvents(appKey: string, bindingId: string) { return updateClient.get<{ data: StoreMonitoringEvent[] }>( `/api/v1/updates/store-monitoring/bindings/${bindingId}/events`, { params: { appKey } }, ) }, createStoreUpdateDraft(appKey: string, bindingId: string, storeVersionId: string) { return updateClient.post<{ data: StoreUpdateDraft }>( `/api/v1/updates/store-monitoring/bindings/${bindingId}/versions/${storeVersionId}/update-draft`, null, { params: { appKey } }, ) }, getStoreNotificationPolicy(appKey: string) { return updateClient.get<{ data: StoreNotificationPolicy }>( '/api/v1/updates/store-monitoring/notification-policy', { params: { appKey } }, ) }, saveStoreNotificationPolicy(appKey: string, body: StoreNotificationPolicyInput) { return updateClient.put<{ data: StoreNotificationPolicy }>( '/api/v1/updates/store-monitoring/notification-policy', body, { params: { appKey } }, ) }, 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, reason?: string) { return updateClient.post<{ data: AppVersion }>( `/api/v1/updates/store/app/${versionId}/review`, { storeType, state, ...(reason ? { reason } : {}) }, ) }, cancelStoreReview(versionId: string, storeTypes?: StoreType[]) { return updateClient.post<{ data: null }>( `/api/v1/updates/store/app/${versionId}/cancel-review`, storeTypes ? { storeTypes } : {}, ) }, preflightStoreSubmission(versionId: string) { return updateClient.post<{ data: PreflightSubmitResultDto }>( `/api/v1/updates/store/app/${versionId}/preflight-submit`, {}, ) }, refreshStoreReviewStatus(versionId: string) { return updateClient.post<{ data: PreflightSubmitResultDto }>( `/api/v1/updates/store/app/${versionId}/refresh-review-status`, {}, ) }, deleteAppVersion(versionId: string) { return updateClient.delete<{ data: null }>(`/api/v1/updates/app/${versionId}`) }, updatePublishSchedule( versionId: string, publishType: 'IMMEDIATE' | 'SCHEDULED', scheduledAt?: string, ) { return updateClient.put<{ data: AppVersion }>( `/api/v1/updates/store/app/${versionId}/publish-schedule`, { publishType, scheduledAt }, ) }, getPublishConfig(appKey: string) { return updateClient.get<{ data: PublishConfig }>('/api/v1/updates/publish/config', { params: { appKey } }) }, savePublishConfig(appKey: string, config: Record) { return updateClient.put<{ data: PublishConfig }>('/api/v1/updates/publish/config', config, { params: { appKey } }) }, listOperationLogs(appKey: string, limit = 100) { return updateClient.get<{ data: OperationLog[] }>('/api/v1/updates/ops/logs', { params: { appKey, limit }, }) }, listGrayMembers(appKey: string) { return updateClient.get<{ data: GrayMember[] }>('/api/v1/updates/gray/members', { params: { appKey }, }) }, addGrayMembers(appKey: string, userIds: string[], name?: string) { return updateClient.post('/api/v1/updates/gray/members', { appKey, userIds, name }) }, deleteGrayMember(appKey: string, userId: string) { return updateClient.delete(`/api/v1/updates/gray/members/${userId}`, { params: { appKey } }) }, syncGrayMembers(appKey: string, members: { userId: string; name?: string }[]) { return updateClient.post<{ data: { added: number; updated: number; removed: number } }>( '/api/v1/updates/gray/members/sync', { appKey, members }) }, importGrayMembersFromIm(appKey: string) { return updateClient.post<{ data: { added: number; updated: number; removed: number } }>( '/api/v1/updates/gray/members/import-im', null, { params: { appKey } }) }, listGrayTags(appKey: string) { return updateClient.get<{ data: GrayTag[] }>('/api/v1/updates/gray/tags', { params: { appKey } }) }, createGrayTag(appKey: string, tagName: string, userIds: string[]) { return updateClient.post('/api/v1/updates/gray/tags', { appKey, tagName, userIds }) }, deleteGrayTag(appKey: string, tagName: string) { return updateClient.delete(`/api/v1/updates/gray/tags/${tagName}`, { params: { appKey } }) }, addMembersToTag(appKey: string, tagName: string, userIds: string[]) { return updateClient.post(`/api/v1/updates/gray/tags/${tagName}/members`, { appKey, userIds }) }, removeMembersFromTag(appKey: string, tagName: string, userIds: string[]) { return updateClient.delete(`/api/v1/updates/gray/tags/${tagName}/members`, { data: { appKey, userIds } }) }, // ── Statistics ───────────────────────────────────────────────────────── getVersionDistribution(appKey: string, platform: string, days?: number) { return updateClient.get<{ data: { versionCode: number; versionName: string; deviceCount: number }[] }>( '/api/v1/updates/stats/version-distribution', { params: { appKey, platform, days: days ?? 30 } }, ) }, getDeviceDistribution(appKey: string, platform: string, days?: number) { return updateClient.get<{ data: { models: { model: string; deviceCount: number }[] osVersions: { osVersion: string; deviceCount: number }[] vendors: { vendor: string; deviceCount: number }[] } }>('/api/v1/updates/stats/device-distribution', { params: { appKey, platform, days: days ?? 30 } }) }, getDailyTrend(appKey: string, platform: string, days?: number) { return updateClient.get<{ data: { day: string; deviceCount: number }[] }>( '/api/v1/updates/stats/daily-trend', { params: { appKey, platform, days: days ?? 7 } }, ) }, }