From 215859f03fb12a6874254f74b4996e9fc1636d4d Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Wed, 29 Apr 2026 17:35:52 +0800 Subject: [PATCH] =?UTF-8?q?docs(sdk):=20=E6=B7=BB=E5=8A=A0=20React=20Nativ?= =?UTF-8?q?e=20SDK=20=E6=96=87=E6=A1=A3=E5=92=8C=20Android/HarmonyOS=20?= =?UTF-8?q?=E5=8F=91=E7=89=88=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 XuqmGroup React Native SDK 使用文档,包含安装、初始化、HTTP客户端、IM模块、推送模块、版本管理等功能说明 - 添加 Android Gradle 发版任务脚本,支持构建发布 APK 并上传到更新服务 - 添加 HarmonyOS hvigorw 发版任务脚本,支持 HAP 包构建和上传功能 - 实现多平台版本检查、自动重连、灰度发布等发版流程自动化 - 集成商店提交、定时发布、Webhook 回调等发布后处理功能 --- xuqm-sdk/scripts/xuqm_release.gradle.kts | 113 ++++++++++++++++++++--- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/xuqm-sdk/scripts/xuqm_release.gradle.kts b/xuqm-sdk/scripts/xuqm_release.gradle.kts index 1a3e135..cfa6e95 100644 --- a/xuqm-sdk/scripts/xuqm_release.gradle.kts +++ b/xuqm-sdk/scripts/xuqm_release.gradle.kts @@ -109,6 +109,15 @@ fun httpMultipartPost(url: String, token: String, parts: Map): Stri fun parseJson(json: String, key: String) = 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 = + value?.split(",")?.map { it.trim() }?.filter { it.isNotBlank() } ?: emptyList() + fun promptLine(message: String): String? { val console = System.console() ?: return null print(message) @@ -125,6 +134,47 @@ fun promptYesNo(message: String, default: Boolean = false): Boolean { } } +data class ReleaseDefaults( + val enabled: Boolean = true, + val defaultStoreTargets: List = 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 ────────────────────────────────────────────────────────────────── tasks.register("xuqmRelease") { @@ -139,21 +189,39 @@ tasks.register("xuqmRelease") { val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing") val appKey = cfg.getProperty("xuqm.appKey") ?: throw GradleException("xuqm.appKey missing") val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing") - val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean() - val publishImmediately = cfg.getProperty("xuqm.publishImmediately", "false").toBoolean() - var scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "") - val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "") - val marketUrl = cfg.getProperty("xuqm.marketUrl", "") - var publishMode = cfg.getProperty("xuqm.publishMode", "").trim().uppercase() + val tenantUrl = cfg.getProperty("xuqm.tenantUrl", "").trim() + val tenantDefaults = fetchReleaseDefaults(tenantUrl, appKey) + val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", tenantDefaults?.defaultAutoPublishAfterReview?.toString() ?: "false").toBoolean() + var publishImmediately = cfg.getProperty("xuqm.publishImmediately", tenantDefaults?.defaultPublishImmediately?.toString() ?: "false").toBoolean() + var scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", tenantDefaults?.defaultScheduledPublishAt ?: "") + 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) { publishMode = promptReleaseMode() if (publishMode == "SCHEDULED" && scheduledAt.isBlank()) { 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 ────────────────────────────────────────── - val (versionName, versionCode) = readHarmonyVersion(projectDir) + var (versionName, versionCode) = readHarmonyVersion(projectDir) val bundleName = readHarmonyBundleName(projectDir) println("[xuqm] Local version: $versionName ($versionCode), appKey: $appKey") @@ -164,10 +232,19 @@ tasks.register("xuqmRelease") { println("[xuqm] Server latest versionCode: $serverCode") if (versionCode <= serverCode) { - throw GradleException( - "[xuqm] Local versionCode ($versionCode) ≤ server ($serverCode). " + - "Bump versionCode in AppScope/app.json5 first." - ) + 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) { + throw GradleException( + "[xuqm] Release versionCode ($versionCode) must be greater than server ($serverCode). " + + "Bump versionCode in AppScope/app.json5 or pass xuqm.allowVersionMismatch=true if you deliberately override." + ) + } } // ── 3. Locate HAP ────────────────────────────────────────────────── @@ -175,6 +252,19 @@ tasks.register("xuqmRelease") { if (marketUrl.isBlank()) { 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( "appId" to appKey, "platform" to "HARMONY", "versionName" to versionName, "versionCode" to versionCode, @@ -192,6 +282,7 @@ tasks.register("xuqmRelease") { ?: throw GradleException("[xuqm] Upload failed:\n$uploadResp") println("[xuqm] Uploaded, version ID: $versionId") + if (publishMode == "NOW") publishImmediately = true if (publishImmediately || publishMode == "NOW") { println("[xuqm] Published immediately in update service.") } else if (publishMode == "SCHEDULED" || scheduledAt.isNotBlank()) {