feat(update): manage terminal RN plugin packages

这个提交包含在:
XuqmGroup 2026-07-27 00:37:16 +08:00
父节点 985c7f78d7
当前提交 7c4915bdb6
共有 3 个文件被更改,包括 64 次插入174 次删除

查看文件

@ -55,64 +55,34 @@ await UpdateSDK.openStore(appUpdate.appStoreUrl, appUpdate.marketUrl)
--- ---
## 4. RN Bundle 热更新 ## 4. RN 插件更新
### 4.1 注册插件 插件更新以服务端返回的 release-set 为最小事务单位。一个 release-set 只包含目标
app/buz 及其 Common 依赖;SDK 会统一完成签名、宿主身份、整包版本、原生基线、
SHA-256 和最终依赖闭包校验。宿主不能分别下载后自行拼接版本。
在插件 Bundle 的入口文件顶部注册插件元数据: 进入 buz 前自动检查并安装
```ts ```ts
// plugin/index.ts await UpdateSDK.checkAndInstallPlugin('home', {
import { UpdateSDK } from '@xuqm/rn-update' onProgress(moduleId, progress) {
import meta from './plugin.json' console.log(moduleId, progress.percent)
},
UpdateSDK.registerPlugin(meta) })
``` ```
`plugin.json` 示例 需要由宿主展示确认弹窗时,检查与安装分开调用
```json ```ts
{ const plan = await UpdateSDK.checkPluginRelease('home')
"moduleId": "home", if (plan && (await showPluginUpdateDialog(plan))) {
"version": "1.2.3" await UpdateSDK.installPluginRelease(plan)
} }
``` ```
### 4.2 检查热更新 `checkPluginRelease` 只检查,不下载、不修改本地状态。安装阶段会再次验证签名和检查时
记录的本地基线;计划已过期时必须重新检查。插件版本、路径和事务状态由原生层统一
```ts 管理,宿主不得另建一套缓存版本表。
const rnUpdate = await UpdateSDK.checkRnUpdate('home')
if (rnUpdate.needsUpdate) {
console.log('最新版本:', rnUpdate.latestVersion)
console.log('下载地址:', rnUpdate.downloadUrl)
console.log('更新说明:', rnUpdate.note)
console.log('最低依赖版本:', rnUpdate.minCommonVersion)
}
```
### 4.3 下载并缓存 Bundle
```ts
// 下载 Bundle 源码
const source = await UpdateSDK.downloadRnBundle(rnUpdate.downloadUrl)
// 缓存到本地
await UpdateSDK.cacheRnBundle('home', rnUpdate.latestVersion, rnUpdate.md5, source)
// 读取已缓存的 Bundle
const cached = await UpdateSDK.getCachedRnBundle('home')
if (cached) {
console.log('缓存版本:', cached.version)
console.log('缓存时间:', cached.downloadedAt)
}
```
### 4.4 获取已注册插件版本
```ts
const version = UpdateSDK.getRegisteredPluginVersion('home')
console.log('当前运行版本:', version)
```
--- ---
@ -160,30 +130,32 @@ await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig_jwt' })
SDK 提供 `xuqm_release.mjs` 脚本,可一键完成 RN Bundle 构建并上传至 XuqmGroup 版本管理服务: SDK 提供 `xuqm_release.mjs` 脚本,可一键完成 RN Bundle 构建并上传至 XuqmGroup 版本管理服务:
```bash ```bash
node scripts/xuqm_release.mjs --platform android --env production XUQM_API_TOKEN=*** node scripts/xuqm_release.mjs --platform android --publish
``` ```
脚本执行流程: 脚本执行流程:
1. 执行 `react-native bundle` 构建 JS Bundle 1. 按模块构建 JS/Hermes Bundle 和 Source Map
2. 自动提取模块 ID、版本号、更新日志 2. 生成根目录唯一的 `rn-manifest.json`
3. 上传 Bundle 到 XuqmGroup 后台 3. 生成 `<moduleId>.<platform>.xuqm.zip`
4. 返回发布结果 4. 上传归档并由服务端校验、签名
5. 按用户选择发布,返回不可变发布记录
常用参数: 常用参数:
| 参数 | 说明 | | 参数 | 说明 |
|------|------| |------|------|
| `--platform` | 平台:`android` / `ios` | | `--platform` | 平台:`android` / `ios` |
| `--env` | 环境:`production` / `staging` | | `--module` | 可重复传入,只构建或发布指定模块 |
| `--moduleId` | 模块 ID`home`| | `--note` | 本次发布说明 |
| `--forceUpdate` | 是否强制更新 | | `--publish` | 上传后立即发布 |
| `--bugcollect` | `platform` 上传 Source Map;`disabled` 仅保留本地产物 |
`package.json` 中添加快捷命令: `package.json` 中添加快捷命令:
```json ```json
{ {
"scripts": { "scripts": {
"xuqm:release": "node scripts/xuqm_release.mjs --platform android --env production" "xuqm:release": "node scripts/xuqm_release.mjs --platform android --publish"
} }
} }
``` ```

查看文件

@ -43,8 +43,7 @@ updateClient.interceptors.request.use((config) => {
url.startsWith('/api/v1/updates/app/check') || url.startsWith('/api/v1/updates/app/check') ||
url.startsWith('/api/v1/updates/app/inspect') || url.startsWith('/api/v1/updates/app/inspect') ||
url.startsWith('/api/v1/updates/public/') || url.startsWith('/api/v1/updates/public/') ||
url.startsWith('/api/v1/rn/update/check') || url.startsWith('/api/v1/rn/release-set/check') ||
url.startsWith('/api/v1/rn/inspect') ||
url.startsWith('/api/v1/rn/files/') url.startsWith('/api/v1/rn/files/')
) )
if (!skipAuth) { if (!skipAuth) {
@ -186,12 +185,21 @@ export interface AppPackageInspectResult {
export interface RnBundle { export interface RnBundle {
id: string id: string
appKey: string appKey: string
packageName: string
moduleId: string moduleId: string
platform: 'ANDROID' | 'IOS' | 'HARMONY' type: 'common' | 'app' | 'buz'
platform: 'ANDROID' | 'IOS'
version: string version: string
md5: string appVersionRange: string
minCommonVersion?: string builtAgainstNativeBaselineId: string
packageName?: string buildId: string
bundleFormat: string
commonVersionRange?: string
minNativeApiLevel: number
bundleSha256: string
archiveSha256: string
keyId: string
signature: string
note?: string note?: string
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
publishMode?: PublishMode publishMode?: PublishMode
@ -203,16 +211,6 @@ export interface RnBundle {
createdAt: 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 { export interface UnifiedAppUploadItem {
fileKey: string fileKey: string
platform: 'ANDROID' | 'IOS' | 'HARMONY' platform: 'ANDROID' | 'IOS' | 'HARMONY'
@ -226,19 +224,8 @@ export interface UnifiedAppUploadItem {
publishImmediately: boolean publishImmediately: boolean
} }
export interface UnifiedRnUploadItem {
fileKey: string
moduleId: string
platform: 'ANDROID' | 'IOS' | 'HARMONY'
version: string
minCommonVersion?: string
packageName?: string
note?: string
}
export interface UnifiedReleaseManifest { export interface UnifiedReleaseManifest {
appVersions: UnifiedAppUploadItem[] appVersions: UnifiedAppUploadItem[]
rnBundles: UnifiedRnUploadItem[]
} }
export interface DownloadPageHistoryItem { export interface DownloadPageHistoryItem {
@ -342,10 +329,6 @@ export const updateAdminApi = {
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, uploadProgressConfig(onProgress)) return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, uploadProgressConfig(onProgress))
}, },
inspectRnBundle(formData: FormData, onProgress?: UploadProgressHandler) {
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData, uploadProgressConfig(onProgress))
},
uploadUnifiedRelease(formData: FormData, onProgress?: UploadProgressHandler) { uploadUnifiedRelease(formData: FormData, onProgress?: UploadProgressHandler) {
return updateClient.post('/api/v1/updates/unified/upload', formData, uploadProgressConfig(onProgress)) return updateClient.post('/api/v1/updates/unified/upload', formData, uploadProgressConfig(onProgress))
}, },

查看文件

@ -178,8 +178,18 @@
<div class="table-wrap"> <div class="table-wrap">
<el-table :data="rnBundles" v-loading="loadingRn" border stripe> <el-table :data="rnBundles" v-loading="loadingRn" border stripe>
<el-table-column prop="moduleId" label="模块ID" width="140" /> <el-table-column prop="moduleId" label="模块ID" width="140" />
<el-table-column prop="type" label="类型" width="90" />
<el-table-column prop="version" label="版本" width="100" /> <el-table-column prop="version" label="版本" width="100" />
<el-table-column prop="platform" label="平台" width="90" /> <el-table-column prop="platform" label="平台" width="90" />
<el-table-column prop="appVersionRange" label="整包版本范围" width="170" />
<el-table-column prop="commonVersionRange" label="Common 版本范围" width="180">
<template #default="{row}">{{ row.commonVersionRange || '—' }}</template>
</el-table-column>
<el-table-column prop="builtAgainstNativeBaselineId" label="原生基线" width="220" show-overflow-tooltip />
<el-table-column prop="buildId" label="构建 ID" width="150" show-overflow-tooltip />
<el-table-column prop="bundleFormat" label="Bundle 格式" width="140" />
<el-table-column prop="minNativeApiLevel" label="最低原生 API" width="130" />
<el-table-column prop="archiveSha256" label="归档 SHA-256" width="220" show-overflow-tooltip />
<el-table-column label="状态" width="140"> <el-table-column label="状态" width="140">
<template #default="{row}"> <template #default="{row}">
<el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag> <el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag>
@ -191,7 +201,6 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="minCommonVersion" label="最低 Common 版本" width="160" />
<el-table-column prop="note" label="说明" show-overflow-tooltip /> <el-table-column prop="note" label="说明" show-overflow-tooltip />
<el-table-column prop="createdAt" label="上传时间" width="160"> <el-table-column prop="createdAt" label="上传时间" width="160">
<template #default="{row}">{{ formatTime(row.createdAt) }}</template> <template #default="{row}">{{ formatTime(row.createdAt) }}</template>
@ -924,44 +933,25 @@
:auto-upload="false" :auto-upload="false"
:limit="1" :limit="1"
:on-change="onRnBundleChange" :on-change="onRnBundleChange"
accept=".bundle,.js" accept=".xuqm.zip"
> >
<el-icon class="el-icon--upload"><UploadFilled /></el-icon> <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text"> <div class="el-upload__text">
RN Bundle 拖到这里 <em>点击选择文件</em> CLI 生成的 .xuqm.zip 发布包拖到这里 <em>点击选择文件</em>
</div> </div>
<template #tip> <template #tip>
<div class="el-upload__tip"> <div class="el-upload__tip">
选择或拖入文件后会自动识别模块平台版本和 Common 版本 发布身份只读取压缩包根目录的 rn-manifest.json页面不能修改
</div> </div>
</template> </template>
</el-upload> </el-upload>
</el-form-item> </el-form-item>
<el-form-item v-if="rnInspectUploadProgress > 0" label="识别进度">
<div class="upload-progress-block">
<el-progress :percentage="rnInspectUploadProgress" :status="rnInspectUploadProgress === 100 ? 'success' : undefined" />
<span class="upload-progress-text">{{ rnInspectUploadProgress }}%</span>
</div>
</el-form-item>
<el-alert <el-alert
type="info" type="info"
:closable="false" :closable="false"
show-icon show-icon
title="推荐文件名格式moduleId__ANDROID__1.0.0__1.0.0__com.example.app.bundle,系统会按命名自动识别模块、平台、版本、最低 Common 版本和包名。" title="只接受由发布 CLI 生成的 .xuqm.zip。服务端会校验清单、Bundle SHA-256、归档 SHA-256、应用归属并签署不可变发布身份。"
/> />
<el-form-item label="模块ID"><el-input v-model="rnUploadForm.moduleId" placeholder="可由文件名自动识别" /></el-form-item>
<el-form-item label="平台">
<el-select v-model="rnUploadForm.platform">
<el-option value="ANDROID" label="Android" />
<el-option value="IOS" label="iOS" />
<el-option value="HARMONY" label="Harmony" />
</el-select>
</el-form-item>
<el-form-item label="版本"><el-input v-model="rnUploadForm.version" placeholder="可由文件名自动识别" /></el-form-item>
<el-form-item label="最低 Common 版本"><el-input v-model="rnUploadForm.minCommonVersion" placeholder="可由文件名自动识别" /></el-form-item>
<el-form-item label="包名 / Bundle">
<el-input v-model="rnUploadForm.packageName" placeholder="可选,建议与应用包名一致" />
</el-form-item>
<el-form-item label="说明"><el-input v-model="rnUploadForm.note" type="textarea" :rows="2" /></el-form-item> <el-form-item label="说明"><el-input v-model="rnUploadForm.note" type="textarea" :rows="2" /></el-form-item>
<el-form-item v-if="rnBundleUploadProgress > 0" label="提交进度"> <el-form-item v-if="rnBundleUploadProgress > 0" label="提交进度">
<div class="upload-progress-block"> <div class="upload-progress-block">
@ -981,7 +971,7 @@
<div class="drag-overlay-content"> <div class="drag-overlay-content">
<el-icon size="64"><UploadFilled /></el-icon> <el-icon size="64"><UploadFilled /></el-icon>
<p class="drag-overlay-title">释放文件以上传</p> <p class="drag-overlay-title">释放文件以上传</p>
<p class="drag-overlay-hint">支持 .apk.bundle.js</p> <p class="drag-overlay-hint">支持 .apk.xuqm.zip</p>
</div> </div>
</div> </div>
</template> </template>
@ -1005,7 +995,6 @@ import {
type GrayTag, type GrayTag,
type PublishMode, type PublishMode,
type RnBundle, type RnBundle,
type RnBundleInspectResult,
type StoreConfig, type StoreConfig,
type StoreType, type StoreType,
} from '@/api/update' } from '@/api/update'
@ -1132,12 +1121,12 @@ async function handleDrop(e: DragEvent) {
openUploadAppDialog() openUploadAppDialog()
await nextTick() await nextTick()
await onAppPackageChange({ raw: file }) await onAppPackageChange({ raw: file })
} else if (ext === '.bundle' || ext === '.js') { } else if (file.name.toLowerCase().endsWith('.xuqm.zip')) {
showUploadRn.value = true showUploadRn.value = true
await nextTick() await nextTick()
await onRnBundleChange({ raw: file }) await onRnBundleChange({ raw: file })
} else { } else {
ElMessage.warning(`不支持的文件类型:${file.name},请拖入 .apk、.bundle 或 .js 文件`) ElMessage.warning(`不支持的文件类型:${file.name},请拖入 .apk 或 .xuqm.zip 文件`)
} }
} }
@ -1167,7 +1156,6 @@ const appPackageInspecting = ref(false)
const appPackageUploadProgress = ref(0) const appPackageUploadProgress = ref(0)
const appVersionUploadProgress = ref(0) const appVersionUploadProgress = ref(0)
const appUploadRef = ref<any>(null) const appUploadRef = ref<any>(null)
const rnInspectUploadProgress = ref(0)
const rnBundleUploadProgress = ref(0) const rnBundleUploadProgress = ref(0)
const operationLogs = ref<{ const operationLogs = ref<{
id: string id: string
@ -2155,81 +2143,28 @@ async function submitAppUpload() {
const showUploadRn = ref(false) const showUploadRn = ref(false)
const uploadingRn = ref(false) const uploadingRn = ref(false)
const rnUploadForm = ref({ const rnUploadForm = ref({
moduleId: '',
platform: 'ANDROID' as 'ANDROID' | 'IOS' | 'HARMONY',
version: '',
minCommonVersion: '',
packageName: '',
note: '', note: '',
file: null as File | null, file: null as File | null,
}) })
function parseRnBundleName(fileName: string): RnBundleInspectResult | null {
const baseName = fileName.replace(/\.[^.]+$/, '')
const parts = baseName.split('__')
if (parts.length >= 4) {
return {
moduleId: parts[0],
platform: parts[1].toUpperCase() as 'ANDROID' | 'IOS' | 'HARMONY',
version: parts[2],
minCommonVersion: parts[3],
packageName: parts[4],
fileName,
detected: true,
}
}
return null
}
async function onRnBundleChange(uploadFile: { raw?: File } | null) { async function onRnBundleChange(uploadFile: { raw?: File } | null) {
const file = uploadFile?.raw ?? null const file = uploadFile?.raw ?? null
rnUploadForm.value.file = file rnUploadForm.value.file = file
if (!file) return if (!file) return
if (!file.name.toLowerCase().endsWith('.xuqm.zip')) {
const local = parseRnBundleName(file.name) rnUploadForm.value.file = null
if (local) { ElMessage.warning('RN 插件只接受发布 CLI 生成的 .xuqm.zip')
if (local.moduleId) rnUploadForm.value.moduleId = local.moduleId
if (local.platform) rnUploadForm.value.platform = local.platform
if (local.version) rnUploadForm.value.version = local.version
if (local.minCommonVersion) rnUploadForm.value.minCommonVersion = local.minCommonVersion
if (local.packageName) rnUploadForm.value.packageName = local.packageName
return
}
const formData = new FormData()
formData.append('bundle', file)
rnInspectUploadProgress.value = 0
try {
const res = await updateAdminApi.inspectRnBundle(formData, (percent) => {
rnInspectUploadProgress.value = percent
})
rnInspectUploadProgress.value = 100
const inspected = res.data.data as RnBundleInspectResult
if (inspected.moduleId) rnUploadForm.value.moduleId = inspected.moduleId
if (inspected.platform) rnUploadForm.value.platform = inspected.platform
if (inspected.version) rnUploadForm.value.version = inspected.version
if (inspected.minCommonVersion) rnUploadForm.value.minCommonVersion = inspected.minCommonVersion
if (inspected.packageName) rnUploadForm.value.packageName = inspected.packageName
} catch {
ElMessage.warning('已选择文件,但未能从文件名识别出 RN Bundle 元数据,请补全后上传')
} finally {
rnInspectUploadProgress.value = 0
} }
} }
async function submitRnUpload() { async function submitRnUpload() {
const f = rnUploadForm.value const f = rnUploadForm.value
if (!f.moduleId || !f.version || !f.file) return ElMessage.warning('请填写模块ID、版本和 Bundle 文件') if (!f.file) return ElMessage.warning('请选择 CLI 生成的 .xuqm.zip 发布包')
uploadingRn.value = true uploadingRn.value = true
rnBundleUploadProgress.value = 0 rnBundleUploadProgress.value = 0
try { try {
const fd = new FormData() const fd = new FormData()
fd.append('appKey', appKey.value) fd.append('appKey', appKey.value)
fd.append('moduleId', f.moduleId)
fd.append('platform', f.platform)
fd.append('version', f.version)
if (f.minCommonVersion) fd.append('minCommonVersion', f.minCommonVersion)
if (f.packageName) fd.append('packageName', f.packageName)
if (f.note) fd.append('note', f.note) if (f.note) fd.append('note', f.note)
fd.append('bundle', f.file) fd.append('bundle', f.file)
await updateAdminApi.uploadRnBundle(fd, (percent) => { await updateAdminApi.uploadRnBundle(fd, (percent) => {