docs(sdk): 添加 React Native SDK 文档和 Android/HarmonyOS 发版脚本

- 新增 XuqmGroup React Native SDK 使用文档,包含安装、初始化、HTTP客户端、IM模块、推送模块、版本管理等功能说明
- 添加 Android Gradle 发版任务脚本,支持构建发布 APK 并上传到更新服务
- 添加 HarmonyOS hvigorw 发版任务脚本,支持 HAP 包构建和上传功能
- 实现多平台版本检查、自动重连、灰度发布等发版流程自动化
- 集成商店提交、定时发布、Webhook 回调等发布后处理功能
这个提交包含在:
XuqmGroup 2026-04-29 17:35:52 +08:00
父节点 b3d510ddae
当前提交 215859f03f

查看文件

@ -109,6 +109,15 @@ fun httpMultipartPost(url: String, token: String, parts: Map<String, Any>): Stri
fun parseJson(json: String, key: String) = fun parseJson(json: String, key: String) =
Regex(""""$key"\s*:\s*"([^"]+)"""").find(json)?.groupValues?.get(1) Regex(""""$key"\s*:\s*"([^"]+)"""").find(json)?.groupValues?.get(1)
fun parseJsonBool(json: String, key: String, default: Boolean = false): Boolean =
Regex(""""$key"\s*:\s*(true|false)""").find(json)?.groupValues?.get(1)?.toBooleanStrictOrNull() ?: default
fun parseJsonInt(json: String, key: String, default: Int = 0): Int =
Regex(""""$key"\s*:\s*(\d+)""").find(json)?.groupValues?.get(1)?.toIntOrNull() ?: default
fun parseCsv(value: String?): List<String> =
value?.split(",")?.map { it.trim() }?.filter { it.isNotBlank() } ?: emptyList()
fun promptLine(message: String): String? { fun promptLine(message: String): String? {
val console = System.console() ?: return null val console = System.console() ?: return null
print(message) print(message)
@ -125,6 +134,47 @@ fun promptYesNo(message: String, default: Boolean = false): Boolean {
} }
} }
data class ReleaseDefaults(
val enabled: Boolean = true,
val defaultStoreTargets: List<String> = emptyList(),
val defaultPublishMode: String = "MANUAL",
val defaultPublishImmediately: Boolean = false,
val defaultScheduledPublishAt: String = "",
val defaultAutoPublishAfterReview: Boolean = false,
val defaultWebhookUrl: String = "",
val defaultForceUpdate: Boolean = false,
val defaultGrayEnabled: Boolean = false,
val defaultGrayPercent: Int = 0,
val defaultPackageName: String = "",
val defaultAppStoreUrl: String = "",
val defaultMarketUrl: String = "",
)
fun fetchReleaseDefaults(tenantUrl: String, appKey: String): ReleaseDefaults? {
if (tenantUrl.isBlank()) return null
val body = runCatching {
val uri = URI("$tenantUrl/api/sdk/config?appId=$appKey&platform=HARMONY")
val req = HttpRequest.newBuilder(uri).GET().build()
HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body()
}.getOrNull() ?: return null
if (!parseJsonBool(body, "updateEnabled", false)) return ReleaseDefaults(enabled = false)
return ReleaseDefaults(
enabled = true,
defaultStoreTargets = parseCsv(parseJson(body, "updateDefaultStoreTargets")),
defaultPublishMode = parseJson(body, "updateDefaultPublishMode") ?: "MANUAL",
defaultPublishImmediately = parseJsonBool(body, "updateDefaultPublishImmediately"),
defaultScheduledPublishAt = parseJson(body, "updateDefaultScheduledPublishAt").orEmpty(),
defaultAutoPublishAfterReview = parseJsonBool(body, "updateDefaultAutoPublishAfterReview"),
defaultWebhookUrl = parseJson(body, "updateDefaultWebhookUrl").orEmpty(),
defaultForceUpdate = parseJsonBool(body, "updateDefaultForceUpdate"),
defaultGrayEnabled = parseJsonBool(body, "updateDefaultGrayEnabled"),
defaultGrayPercent = parseJsonInt(body, "updateDefaultGrayPercent"),
defaultPackageName = parseJson(body, "updateDefaultPackageName").orEmpty(),
defaultAppStoreUrl = parseJson(body, "updateDefaultAppStoreUrl").orEmpty(),
defaultMarketUrl = parseJson(body, "updateDefaultMarketUrl").orEmpty(),
)
}
// ── Task ────────────────────────────────────────────────────────────────── // ── Task ──────────────────────────────────────────────────────────────────
tasks.register("xuqmRelease") { tasks.register("xuqmRelease") {
@ -139,21 +189,39 @@ tasks.register("xuqmRelease") {
val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing") val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing")
val appKey = cfg.getProperty("xuqm.appKey") ?: throw GradleException("xuqm.appKey missing") val appKey = cfg.getProperty("xuqm.appKey") ?: throw GradleException("xuqm.appKey missing")
val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing") val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing")
val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean() val tenantUrl = cfg.getProperty("xuqm.tenantUrl", "").trim()
val publishImmediately = cfg.getProperty("xuqm.publishImmediately", "false").toBoolean() val tenantDefaults = fetchReleaseDefaults(tenantUrl, appKey)
var scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "") val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", tenantDefaults?.defaultAutoPublishAfterReview?.toString() ?: "false").toBoolean()
val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "") var publishImmediately = cfg.getProperty("xuqm.publishImmediately", tenantDefaults?.defaultPublishImmediately?.toString() ?: "false").toBoolean()
val marketUrl = cfg.getProperty("xuqm.marketUrl", "") var scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", tenantDefaults?.defaultScheduledPublishAt ?: "")
var publishMode = cfg.getProperty("xuqm.publishMode", "").trim().uppercase() var webhookUrl = cfg.getProperty("xuqm.webhookUrl", tenantDefaults?.defaultWebhookUrl ?: "")
var marketUrl = cfg.getProperty("xuqm.marketUrl", tenantDefaults?.defaultMarketUrl ?: "")
var publishMode = cfg.getProperty("xuqm.publishMode", tenantDefaults?.defaultPublishMode ?: "").trim().uppercase()
var dryRun = cfg.getProperty("xuqm.dryRun", "false").toBoolean() || publishMode == "DRY_RUN" || tenantDefaults?.enabled == false
val allowVersionMismatch = cfg.getProperty("xuqm.allowVersionMismatch", "false").toBoolean()
if (publishMode.isBlank() && !publishImmediately && scheduledAt.isBlank() && !autoPublish && System.console() != null) { if (publishMode.isBlank() && !publishImmediately && scheduledAt.isBlank() && !autoPublish && System.console() != null) {
publishMode = promptReleaseMode() publishMode = promptReleaseMode()
if (publishMode == "SCHEDULED" && scheduledAt.isBlank()) { if (publishMode == "SCHEDULED" && scheduledAt.isBlank()) {
scheduledAt = promptLine("Scheduled publish time (ISO datetime): ")?.trim().orEmpty() scheduledAt = promptLine("Scheduled publish time (ISO datetime): ")?.trim().orEmpty()
} }
} }
if (publishMode == "DRY_RUN") {
dryRun = true
println("[xuqm] Dry-run mode enabled.")
}
if (!scheduledAt.isBlank()) {
publishImmediately = false
}
if (publishMode == "NOW") {
publishImmediately = true
autoPublish = false
} else if (publishMode == "AUTO_REVIEW") {
autoPublish = true
publishImmediately = false
}
// ── 1. Read local version ────────────────────────────────────────── // ── 1. Read local version ──────────────────────────────────────────
val (versionName, versionCode) = readHarmonyVersion(projectDir) var (versionName, versionCode) = readHarmonyVersion(projectDir)
val bundleName = readHarmonyBundleName(projectDir) val bundleName = readHarmonyBundleName(projectDir)
println("[xuqm] Local version: $versionName ($versionCode), appKey: $appKey") println("[xuqm] Local version: $versionName ($versionCode), appKey: $appKey")
@ -163,18 +231,40 @@ tasks.register("xuqmRelease") {
.mapNotNull { it.groupValues[1].toIntOrNull() }.maxOrNull() ?: 0 .mapNotNull { it.groupValues[1].toIntOrNull() }.maxOrNull() ?: 0
println("[xuqm] Server latest versionCode: $serverCode") println("[xuqm] Server latest versionCode: $serverCode")
if (versionCode <= serverCode) {
if (!allowVersionMismatch && System.console() != null) {
println("[xuqm] Local version is not greater than server latest. Please enter corrected release version info.")
val inputVersionName = promptLine("Release versionName [$versionName]: ")?.trim().orEmpty()
val inputVersionCode = promptLine("Release versionCode [$versionCode]: ")?.trim().orEmpty()
if (inputVersionName.isNotBlank()) versionName = inputVersionName
if (inputVersionCode.isNotBlank()) versionCode = inputVersionCode.toInt()
}
if (versionCode <= serverCode) { if (versionCode <= serverCode) {
throw GradleException( throw GradleException(
"[xuqm] Local versionCode ($versionCode) ≤ server ($serverCode). " + "[xuqm] Release versionCode ($versionCode) must be greater than server ($serverCode). " +
"Bump versionCode in AppScope/app.json5 first." "Bump versionCode in AppScope/app.json5 or pass xuqm.allowVersionMismatch=true if you deliberately override."
) )
} }
}
// ── 3. Locate HAP ────────────────────────────────────────────────── // ── 3. Locate HAP ──────────────────────────────────────────────────
// ── 4. Upload release metadata to update service ────────────────── // ── 4. Upload release metadata to update service ──────────────────
if (marketUrl.isBlank()) { if (marketUrl.isBlank()) {
throw GradleException("xuqm.marketUrl missing for Harmony release") throw GradleException("xuqm.marketUrl missing for Harmony release")
} }
if (dryRun) {
println("[xuqm] Dry-run summary:")
println(" updateEnabled=${tenantDefaults?.enabled ?: true}")
println(" releaseVersion=$versionName ($versionCode)")
println(" publishMode=${publishMode.ifBlank { "MANUAL" }}")
println(" publishImmediately=$publishImmediately")
println(" autoPublishAfterReview=$autoPublish")
println(" scheduledAt=${scheduledAt.ifBlank { "-" }}")
println(" webhookUrl=${webhookUrl.ifBlank { "-" }}")
println(" marketUrl=${marketUrl.ifBlank { "-" }}")
println("[xuqm] Dry-run completed. No upload performed.")
return@doLast
}
val parts = mutableMapOf<String, Any>( val parts = mutableMapOf<String, Any>(
"appId" to appKey, "platform" to "HARMONY", "appId" to appKey, "platform" to "HARMONY",
"versionName" to versionName, "versionCode" to versionCode, "versionName" to versionName, "versionCode" to versionCode,
@ -192,6 +282,7 @@ tasks.register("xuqmRelease") {
?: throw GradleException("[xuqm] Upload failed:\n$uploadResp") ?: throw GradleException("[xuqm] Upload failed:\n$uploadResp")
println("[xuqm] Uploaded, version ID: $versionId") println("[xuqm] Uploaded, version ID: $versionId")
if (publishMode == "NOW") publishImmediately = true
if (publishImmediately || publishMode == "NOW") { if (publishImmediately || publishMode == "NOW") {
println("[xuqm] Published immediately in update service.") println("[xuqm] Published immediately in update service.")
} else if (publishMode == "SCHEDULED" || scheduledAt.isNotBlank()) { } else if (publishMode == "SCHEDULED" || scheduledAt.isNotBlank()) {