2026-04-24 16:16:54 +08:00
|
|
|
import axios from 'axios'
|
2026-04-30 09:49:05 +08:00
|
|
|
import { isJwtExpired } from '@/utils/jwt'
|
2026-04-24 16:16:54 +08:00
|
|
|
|
|
|
|
|
const updateClient = axios.create({
|
2026-04-30 09:49:05 +08:00
|
|
|
baseURL: import.meta.env.VITE_UPDATE_API_BASE_URL ?? '',
|
2026-04-24 16:16:54 +08:00
|
|
|
timeout: 30000,
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-28 16:08:07 +08:00
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:16:54 +08:00
|
|
|
updateClient.interceptors.request.use((config) => {
|
2026-04-29 17:35:52 +08:00
|
|
|
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')
|
2026-04-30 09:49:05 +08:00
|
|
|
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('登录已失效,请重新登录'))
|
|
|
|
|
}
|
2026-04-29 17:35:52 +08:00
|
|
|
}
|
2026-04-24 16:16:54 +08:00
|
|
|
return config
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
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)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-29 19:08:13 +08:00
|
|
|
export type StoreType = 'HUAWEI' | 'MI' | 'OPPO' | 'VIVO' | 'HONOR' | 'APP_STORE' | 'GOOGLE_PLAY' | 'HARMONY_APP' | 'REVIEW_WEBHOOK'
|
2026-04-29 00:36:41 +08:00
|
|
|
export type StoreReviewState = 'PENDING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED'
|
2026-04-29 19:08:13 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
export interface OperationLog {
|
|
|
|
|
id: string
|
|
|
|
|
appId: string
|
|
|
|
|
resourceType: string
|
|
|
|
|
resourceId: string
|
|
|
|
|
action: string
|
|
|
|
|
operator?: string
|
|
|
|
|
reason?: string
|
|
|
|
|
detailJson?: string
|
|
|
|
|
createdAt: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 19:08:13 +08:00
|
|
|
export interface GrayMember {
|
|
|
|
|
userId: string
|
|
|
|
|
name?: string
|
|
|
|
|
groupName?: string
|
|
|
|
|
extraJson?: string
|
|
|
|
|
updatedAt?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GrayMemberGroup {
|
|
|
|
|
groupName: string
|
|
|
|
|
members: GrayMember[]
|
|
|
|
|
}
|
2026-04-29 00:36:41 +08:00
|
|
|
|
|
|
|
|
export interface StoreConfig {
|
|
|
|
|
id: string
|
|
|
|
|
appId: string
|
|
|
|
|
storeType: StoreType
|
|
|
|
|
configJson?: string
|
|
|
|
|
enabled: boolean
|
|
|
|
|
updatedAt: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:16:54 +08:00
|
|
|
export interface AppVersion {
|
|
|
|
|
id: string
|
|
|
|
|
appId: string
|
2026-04-29 15:46:40 +08:00
|
|
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
2026-04-24 16:16:54 +08:00
|
|
|
versionName: string
|
|
|
|
|
versionCode: number
|
2026-04-29 00:36:41 +08:00
|
|
|
packageName?: string
|
2026-04-24 16:16:54 +08:00
|
|
|
downloadUrl?: string
|
|
|
|
|
changeLog?: string
|
|
|
|
|
forceUpdate: boolean
|
|
|
|
|
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
|
|
|
|
|
grayEnabled: boolean
|
|
|
|
|
grayPercent: number
|
|
|
|
|
appStoreUrl?: string
|
|
|
|
|
marketUrl?: string
|
2026-04-29 00:36:41 +08:00
|
|
|
scheduledPublishAt?: string
|
|
|
|
|
autoPublishAfterReview: boolean
|
|
|
|
|
webhookUrl?: string
|
|
|
|
|
storeSubmitTargets?: string
|
|
|
|
|
storeReviewStatus?: string
|
2026-04-29 19:08:13 +08:00
|
|
|
storeSubmitMode?: PublishMode
|
|
|
|
|
storeSubmitScheduledAt?: string
|
|
|
|
|
grayMode?: GrayMode
|
|
|
|
|
grayMemberIds?: string
|
2026-04-24 16:16:54 +08:00
|
|
|
createdAt: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 12:33:26 +08:00
|
|
|
export interface AppPackageInspectResult {
|
2026-04-29 15:46:40 +08:00
|
|
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
2026-04-29 12:33:26 +08:00
|
|
|
packageName?: string
|
|
|
|
|
versionName?: string
|
|
|
|
|
versionCode?: number
|
|
|
|
|
fileName?: string
|
|
|
|
|
detected: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:16:54 +08:00
|
|
|
export interface RnBundle {
|
|
|
|
|
id: string
|
|
|
|
|
appId: string
|
|
|
|
|
moduleId: string
|
2026-04-29 15:46:40 +08:00
|
|
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
2026-04-24 16:16:54 +08:00
|
|
|
version: string
|
|
|
|
|
md5: string
|
|
|
|
|
minCommonVersion?: string
|
2026-04-29 15:46:40 +08:00
|
|
|
packageName?: string
|
2026-04-24 16:16:54 +08:00
|
|
|
note?: string
|
|
|
|
|
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
|
2026-04-29 19:08:13 +08:00
|
|
|
publishMode?: PublishMode
|
|
|
|
|
scheduledPublishAt?: string
|
2026-04-24 16:16:54 +08:00
|
|
|
grayEnabled: boolean
|
|
|
|
|
grayPercent: number
|
2026-04-29 19:08:13 +08:00
|
|
|
grayMode?: GrayMode
|
|
|
|
|
grayMemberIds?: string
|
2026-04-24 16:16:54 +08:00
|
|
|
createdAt: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 12:33:26 +08:00
|
|
|
export interface RnBundleInspectResult {
|
|
|
|
|
moduleId?: string
|
2026-04-29 15:46:40 +08:00
|
|
|
platform?: 'ANDROID' | 'IOS' | 'HARMONY'
|
2026-04-29 12:33:26 +08:00
|
|
|
version?: string
|
|
|
|
|
minCommonVersion?: string
|
2026-04-29 15:46:40 +08:00
|
|
|
packageName?: string
|
2026-04-29 12:33:26 +08:00
|
|
|
fileName?: string
|
|
|
|
|
detected: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:07 +08:00
|
|
|
export interface UnifiedAppUploadItem {
|
|
|
|
|
fileKey: string
|
2026-04-29 15:46:40 +08:00
|
|
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
2026-04-28 21:05:07 +08:00
|
|
|
versionName: string
|
|
|
|
|
versionCode: number
|
|
|
|
|
changeLog?: string
|
|
|
|
|
forceUpdate: boolean
|
2026-04-29 15:46:40 +08:00
|
|
|
packageName?: string
|
2026-04-28 21:05:07 +08:00
|
|
|
appStoreUrl?: string
|
|
|
|
|
marketUrl?: string
|
2026-04-29 15:46:40 +08:00
|
|
|
publishImmediately: boolean
|
2026-04-28 21:05:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UnifiedRnUploadItem {
|
|
|
|
|
fileKey: string
|
|
|
|
|
moduleId: string
|
2026-04-29 15:46:40 +08:00
|
|
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
2026-04-28 21:05:07 +08:00
|
|
|
version: string
|
|
|
|
|
minCommonVersion?: string
|
2026-04-29 15:46:40 +08:00
|
|
|
packageName?: string
|
2026-04-28 21:05:07 +08:00
|
|
|
note?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UnifiedReleaseManifest {
|
|
|
|
|
appVersions: UnifiedAppUploadItem[]
|
|
|
|
|
rnBundles: UnifiedRnUploadItem[]
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:16:54 +08:00
|
|
|
export const updateAdminApi = {
|
2026-04-29 15:46:40 +08:00
|
|
|
listAppVersions(appId: string, platform: 'ANDROID' | 'IOS' | 'HARMONY') {
|
2026-04-24 16:16:54 +08:00
|
|
|
return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', {
|
|
|
|
|
params: { appId, platform },
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-29 19:08:13 +08:00
|
|
|
publishAppVersion(id: string, body?: { publishImmediately?: boolean; scheduledPublishAt?: string; forceUpdate?: boolean }) {
|
|
|
|
|
return updateClient.post(`/api/v1/updates/app/${id}/publish`, body ?? {})
|
2026-04-24 16:16:54 +08:00
|
|
|
},
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
unpublishAppVersion(id: string, reason: string) {
|
|
|
|
|
return updateClient.post(`/api/v1/updates/app/${id}/unpublish`, { reason })
|
2026-04-24 16:16:54 +08:00
|
|
|
},
|
|
|
|
|
|
2026-04-29 19:08:13 +08:00
|
|
|
grayAppVersion(id: string, body: {
|
|
|
|
|
enabled: boolean
|
|
|
|
|
grayMode: GrayMode
|
|
|
|
|
percent?: number
|
|
|
|
|
memberIds?: string[]
|
|
|
|
|
selectionSource?: GraySelectionSource
|
|
|
|
|
}) {
|
|
|
|
|
return updateClient.post(`/api/v1/updates/app/${id}/gray`, body)
|
2026-04-24 16:16:54 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
uploadAppVersion(formData: FormData) {
|
2026-04-29 17:35:52 +08:00
|
|
|
return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData)
|
2026-04-24 16:16:54 +08:00
|
|
|
},
|
|
|
|
|
|
2026-04-29 17:35:52 +08:00
|
|
|
inspectAppPackage(apkUrl: string) {
|
|
|
|
|
return updateClient.get<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', {
|
|
|
|
|
params: { apkUrl },
|
2026-04-29 12:33:26 +08:00
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-24 16:16:54 +08:00
|
|
|
listRnBundles(appId: string, moduleId?: string, platform?: string) {
|
|
|
|
|
return updateClient.get<{ data: RnBundle[] }>('/api/v1/rn/list', {
|
|
|
|
|
params: { appId, ...(moduleId && { moduleId }), ...(platform && { platform }) },
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-29 19:08:13 +08:00
|
|
|
publishRnBundle(id: string, body?: { publishImmediately?: boolean; scheduledPublishAt?: string }) {
|
|
|
|
|
return updateClient.post(`/api/v1/rn/${id}/publish`, body ?? {})
|
2026-04-24 16:16:54 +08:00
|
|
|
},
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
unpublishRnBundle(id: string, reason: string) {
|
|
|
|
|
return updateClient.post(`/api/v1/rn/${id}/unpublish`, { reason })
|
2026-04-24 16:16:54 +08:00
|
|
|
},
|
|
|
|
|
|
2026-04-29 19:08:13 +08:00
|
|
|
grayRnBundle(id: string, body: {
|
|
|
|
|
enabled: boolean
|
|
|
|
|
grayMode: GrayMode
|
|
|
|
|
percent?: number
|
|
|
|
|
memberIds?: string[]
|
|
|
|
|
selectionSource?: GraySelectionSource
|
|
|
|
|
}) {
|
|
|
|
|
return updateClient.post(`/api/v1/rn/${id}/gray`, body)
|
2026-04-24 16:16:54 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
uploadRnBundle(formData: FormData) {
|
2026-04-29 17:35:52 +08:00
|
|
|
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData)
|
2026-04-24 16:16:54 +08:00
|
|
|
},
|
2026-04-28 21:05:07 +08:00
|
|
|
|
2026-04-29 12:33:26 +08:00
|
|
|
inspectRnBundle(formData: FormData) {
|
2026-04-29 17:35:52 +08:00
|
|
|
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData)
|
2026-04-29 12:33:26 +08:00
|
|
|
},
|
|
|
|
|
|
2026-04-28 21:05:07 +08:00
|
|
|
uploadUnifiedRelease(formData: FormData) {
|
2026-04-29 17:35:52 +08:00
|
|
|
return updateClient.post('/api/v1/updates/unified/upload', formData)
|
2026-04-28 21:05:07 +08:00
|
|
|
},
|
2026-04-29 00:36:41 +08:00
|
|
|
|
|
|
|
|
// ── 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 } })
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-29 19:08:13 +08:00
|
|
|
executeSubmitToStores(
|
|
|
|
|
versionId: string,
|
|
|
|
|
storeTypes: StoreType[],
|
|
|
|
|
submitMode: PublishMode = 'MANUAL',
|
|
|
|
|
scheduledPublishAt?: string,
|
|
|
|
|
autoPublishAfterReview = false,
|
|
|
|
|
) {
|
2026-04-29 00:36:41 +08:00
|
|
|
return updateClient.post<{ data: AppVersion }>(
|
|
|
|
|
`/api/v1/updates/store/app/${versionId}/execute-submit`,
|
2026-04-29 19:08:13 +08:00
|
|
|
{ storeTypes, submitMode, scheduledPublishAt, autoPublishAfterReview },
|
2026-04-29 00:36:41 +08:00
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateStoreReview(versionId: string, storeType: StoreType, state: StoreReviewState) {
|
|
|
|
|
return updateClient.post<{ data: AppVersion }>(
|
|
|
|
|
`/api/v1/updates/store/app/${versionId}/review`,
|
|
|
|
|
{ storeType, state },
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-04-29 19:08:13 +08:00
|
|
|
|
|
|
|
|
getPublishConfig(appId: string) {
|
|
|
|
|
return updateClient.get<{ data: PublishConfig }>('/api/v1/updates/publish/config', { params: { appId } })
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
savePublishConfig(appId: string, config: Record<string, unknown>) {
|
|
|
|
|
return updateClient.put<{ data: PublishConfig }>('/api/v1/updates/publish/config', config, { params: { appId } })
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
listOperationLogs(appId: string, limit = 100) {
|
|
|
|
|
return updateClient.get<{ data: OperationLog[] }>('/api/v1/updates/ops/logs', {
|
|
|
|
|
params: { appId, limit },
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-29 19:08:13 +08:00
|
|
|
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 },
|
|
|
|
|
})
|
|
|
|
|
},
|
2026-04-24 16:16:54 +08:00
|
|
|
}
|