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
// plugin/index.ts
import { UpdateSDK } from '@xuqm/rn-update'
import meta from './plugin.json'
UpdateSDK.registerPlugin(meta)
await UpdateSDK.checkAndInstallPlugin('home', {
onProgress(moduleId, progress) {
console.log(moduleId, progress.percent)
},
})
```
`plugin.json` 示例
需要由宿主展示确认弹窗时,检查与安装分开调用
```json
{
"moduleId": "home",
"version": "1.2.3"
```ts
const plan = await UpdateSDK.checkPluginRelease('home')
if (plan && (await showPluginUpdateDialog(plan))) {
await UpdateSDK.installPluginRelease(plan)
}
```
### 4.2 检查热更新
```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)
```
`checkPluginRelease` 只检查,不下载、不修改本地状态。安装阶段会再次验证签名和检查时
记录的本地基线;计划已过期时必须重新检查。插件版本、路径和事务状态由原生层统一
管理,宿主不得另建一套缓存版本表。
---
@ -160,30 +130,32 @@ await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig_jwt' })
SDK 提供 `xuqm_release.mjs` 脚本,可一键完成 RN Bundle 构建并上传至 XuqmGroup 版本管理服务:
```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
2. 自动提取模块 ID、版本号、更新日志
3. 上传 Bundle 到 XuqmGroup 后台
4. 返回发布结果
1. 按模块构建 JS/Hermes Bundle 和 Source Map
2. 生成根目录唯一的 `rn-manifest.json`
3. 生成 `<moduleId>.<platform>.xuqm.zip`
4. 上传归档并由服务端校验、签名
5. 按用户选择发布,返回不可变发布记录
常用参数:
| 参数 | 说明 |
|------|------|
| `--platform` | 平台:`android` / `ios` |
| `--env` | 环境:`production` / `staging` |
| `--moduleId` | 模块 ID`home`|
| `--forceUpdate` | 是否强制更新 |
| `--module` | 可重复传入,只构建或发布指定模块 |
| `--note` | 本次发布说明 |
| `--publish` | 上传后立即发布 |
| `--bugcollect` | `platform` 上传 Source Map;`disabled` 仅保留本地产物 |
`package.json` 中添加快捷命令:
```json
{
"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/inspect') ||
url.startsWith('/api/v1/updates/public/') ||
url.startsWith('/api/v1/rn/update/check') ||
url.startsWith('/api/v1/rn/inspect') ||
url.startsWith('/api/v1/rn/release-set/check') ||
url.startsWith('/api/v1/rn/files/')
)
if (!skipAuth) {
@ -186,12 +185,21 @@ export interface AppPackageInspectResult {
export interface RnBundle {
id: string
appKey: string
packageName: string
moduleId: string
platform: 'ANDROID' | 'IOS' | 'HARMONY'
type: 'common' | 'app' | 'buz'
platform: 'ANDROID' | 'IOS'
version: string
md5: string
minCommonVersion?: string
packageName?: 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
@ -203,16 +211,6 @@ export interface RnBundle {
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'
@ -226,19 +224,8 @@ export interface UnifiedAppUploadItem {
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 interface DownloadPageHistoryItem {
@ -342,10 +329,6 @@ export const updateAdminApi = {
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) {
return updateClient.post('/api/v1/updates/unified/upload', formData, uploadProgressConfig(onProgress))
},

查看文件

@ -178,8 +178,18 @@
<div class="table-wrap">
<el-table :data="rnBundles" v-loading="loadingRn" border stripe>
<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="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">
<template #default="{row}">
<el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag>
@ -191,7 +201,6 @@
</el-tag>
</template>
</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="createdAt" label="上传时间" width="160">
<template #default="{row}">{{ formatTime(row.createdAt) }}</template>
@ -924,44 +933,25 @@
:auto-upload="false"
:limit="1"
:on-change="onRnBundleChange"
accept=".bundle,.js"
accept=".xuqm.zip"
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
RN Bundle 拖到这里 <em>点击选择文件</em>
CLI 生成的 .xuqm.zip 发布包拖到这里 <em>点击选择文件</em>
</div>
<template #tip>
<div class="el-upload__tip">
选择或拖入文件后会自动识别模块平台版本和 Common 版本
发布身份只读取压缩包根目录的 rn-manifest.json页面不能修改
</div>
</template>
</el-upload>
</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
type="info"
:closable="false"
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 v-if="rnBundleUploadProgress > 0" label="提交进度">
<div class="upload-progress-block">
@ -981,7 +971,7 @@
<div class="drag-overlay-content">
<el-icon size="64"><UploadFilled /></el-icon>
<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>
</template>
@ -1005,7 +995,6 @@ import {
type GrayTag,
type PublishMode,
type RnBundle,
type RnBundleInspectResult,
type StoreConfig,
type StoreType,
} from '@/api/update'
@ -1132,12 +1121,12 @@ async function handleDrop(e: DragEvent) {
openUploadAppDialog()
await nextTick()
await onAppPackageChange({ raw: file })
} else if (ext === '.bundle' || ext === '.js') {
} else if (file.name.toLowerCase().endsWith('.xuqm.zip')) {
showUploadRn.value = true
await nextTick()
await onRnBundleChange({ raw: file })
} 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 appVersionUploadProgress = ref(0)
const appUploadRef = ref<any>(null)
const rnInspectUploadProgress = ref(0)
const rnBundleUploadProgress = ref(0)
const operationLogs = ref<{
id: string
@ -2155,81 +2143,28 @@ async function submitAppUpload() {
const showUploadRn = ref(false)
const uploadingRn = ref(false)
const rnUploadForm = ref({
moduleId: '',
platform: 'ANDROID' as 'ANDROID' | 'IOS' | 'HARMONY',
version: '',
minCommonVersion: '',
packageName: '',
note: '',
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) {
const file = uploadFile?.raw ?? null
rnUploadForm.value.file = file
if (!file) return
const local = parseRnBundleName(file.name)
if (local) {
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
if (!file.name.toLowerCase().endsWith('.xuqm.zip')) {
rnUploadForm.value.file = null
ElMessage.warning('RN 插件只接受发布 CLI 生成的 .xuqm.zip')
}
}
async function submitRnUpload() {
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
rnBundleUploadProgress.value = 0
try {
const fd = new FormData()
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)
fd.append('bundle', f.file)
await updateAdminApi.uploadRnBundle(fd, (percent) => {