- 实现SDKContext用于配置管理和数据持久化存储 - 定义完整的类型系统包括消息、用户、群组等接口 - 集成更新SDK支持原生应用和RN热更新检查 - 提供统一的XuqmSDK入口类和模块导出 - 编写详细的开发文档和使用示例
263 行
10 KiB
Plaintext
263 行
10 KiB
Plaintext
/**
|
|
* XuqmGroup Update Service — Harmony RN Bundle Release Task
|
|
*
|
|
* Copy this file to your Harmony project and apply it:
|
|
* apply(from = "xuqm_rn_release.gradle.kts")
|
|
*
|
|
* Then run:
|
|
* ./hvigorw xuqmRnRelease
|
|
*
|
|
* Config: xuqm.properties in the project root (or module root)
|
|
* ---
|
|
* xuqm.serverUrl=https://update.dev.xuqinmin.com
|
|
* xuqm.appKey=your-app-key
|
|
* xuqm.apiToken=your-api-token
|
|
* xuqm.moduleId=main
|
|
* xuqm.bundleFile=/path/to/index.bundle
|
|
* xuqm.resourceDir=/path/to/resources # optional, but recommended
|
|
* xuqm.bundleVersion=100
|
|
* xuqm.minCommonVersion=1.0.0 # optional
|
|
* xuqm.packageName=com.example.app # optional
|
|
* xuqm.note=optional note
|
|
* ---
|
|
*
|
|
* The task:
|
|
* 1. Reads local RN release metadata from xuqm.properties
|
|
* 2. Packages bundle + resources + rn-manifest.json into a zip
|
|
* 3. Uploads the zip to update-service so the server can auto-detect metadata
|
|
*/
|
|
|
|
import org.gradle.api.DefaultTask
|
|
import org.gradle.api.GradleException
|
|
import org.gradle.api.tasks.TaskAction
|
|
import java.io.File
|
|
import java.io.FileOutputStream
|
|
import java.net.URI
|
|
import java.net.http.HttpClient
|
|
import java.net.http.HttpRequest
|
|
import java.net.http.HttpResponse
|
|
import java.nio.charset.StandardCharsets
|
|
import java.nio.file.Files
|
|
import java.nio.file.Path
|
|
import java.util.Properties
|
|
import java.util.UUID
|
|
import java.util.zip.ZipEntry
|
|
import java.util.zip.ZipOutputStream
|
|
|
|
fun loadXuqmConfig(projectDir: File): Properties {
|
|
val props = Properties()
|
|
listOf(
|
|
File(projectDir, "xuqm.properties"),
|
|
File(projectDir.parentFile, "xuqm.properties"),
|
|
).firstOrNull { it.exists() }?.inputStream()?.use(props::load)
|
|
?: throw GradleException("xuqm.properties not found in module or project root")
|
|
return props
|
|
}
|
|
|
|
fun httpGet(url: String, token: String): String {
|
|
val client = HttpClient.newHttpClient()
|
|
val builder = HttpRequest.newBuilder(URI.create(url))
|
|
if (token.isNotBlank()) {
|
|
builder.header("Authorization", "Bearer $token")
|
|
}
|
|
val req = builder.GET().build()
|
|
return client.send(req, HttpResponse.BodyHandlers.ofString()).body()
|
|
}
|
|
|
|
fun httpMultipartPost(url: String, token: String, parts: Map<String, Any>): String {
|
|
val boundary = UUID.randomUUID().toString()
|
|
val baos = java.io.ByteArrayOutputStream()
|
|
|
|
fun writeln(s: String) = baos.write(("$s\r\n").toByteArray())
|
|
|
|
for ((name, value) in parts) {
|
|
writeln("--$boundary")
|
|
when (value) {
|
|
is File -> {
|
|
writeln("Content-Disposition: form-data; name=\"$name\"; filename=\"${value.name}\"")
|
|
writeln("Content-Type: application/octet-stream")
|
|
writeln("")
|
|
baos.write(value.readBytes())
|
|
writeln("")
|
|
}
|
|
else -> {
|
|
writeln("Content-Disposition: form-data; name=\"$name\"")
|
|
writeln("")
|
|
writeln(value.toString())
|
|
}
|
|
}
|
|
}
|
|
writeln("--$boundary--")
|
|
|
|
val body = baos.toByteArray()
|
|
val client = HttpClient.newHttpClient()
|
|
val builder = HttpRequest.newBuilder(URI.create(url))
|
|
.header("Content-Type", "multipart/form-data; boundary=$boundary")
|
|
if (token.isNotBlank()) {
|
|
builder.header("Authorization", "Bearer $token")
|
|
}
|
|
val req = builder.POST(HttpRequest.BodyPublishers.ofByteArray(body)).build()
|
|
return client.send(req, HttpResponse.BodyHandlers.ofString()).body()
|
|
}
|
|
|
|
fun parseJson(json: String, key: String): String? =
|
|
Regex("\"$key\"\\s*:\\s*\"([^\"]+)\"").find(json)?.groupValues?.get(1)
|
|
|
|
fun parseJsonInt(json: String, key: String): Int? =
|
|
Regex("\"$key\"\\s*:\\s*(\\d+)").find(json)?.groupValues?.get(1)?.toIntOrNull()
|
|
|
|
fun parseCsv(value: String?): List<String> =
|
|
value?.split(",")?.map { it.trim() }?.filter { it.isNotBlank() } ?: emptyList()
|
|
|
|
fun promptLine(message: String): String? {
|
|
val console = System.console() ?: return null
|
|
print(message)
|
|
return console.readLine()
|
|
}
|
|
|
|
fun escapeJson(value: String?): String {
|
|
if (value == null) return ""
|
|
return buildString(value.length + 8) {
|
|
value.forEach { ch ->
|
|
when (ch) {
|
|
'\\' -> append("\\\\")
|
|
'"' -> append("\\\"")
|
|
'\b' -> append("\\b")
|
|
'\u000C' -> append("\\f")
|
|
'\n' -> append("\\n")
|
|
'\r' -> append("\\r")
|
|
'\t' -> append("\\t")
|
|
else -> append(ch)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun writeZipEntry(zip: ZipOutputStream, entryName: String, content: ByteArray) {
|
|
val entry = ZipEntry(entryName)
|
|
zip.putNextEntry(entry)
|
|
zip.write(content)
|
|
zip.closeEntry()
|
|
}
|
|
|
|
fun addFileToZip(zip: ZipOutputStream, file: File, entryName: String) {
|
|
val entry = ZipEntry(entryName)
|
|
zip.putNextEntry(entry)
|
|
file.inputStream().use { input ->
|
|
input.copyTo(zip)
|
|
}
|
|
zip.closeEntry()
|
|
}
|
|
|
|
fun addDirectoryToZip(zip: ZipOutputStream, dir: File, prefix: String) {
|
|
if (!dir.exists()) return
|
|
dir.walkTopDown()
|
|
.filter { it.isFile }
|
|
.forEach { file ->
|
|
val relative = file.relativeTo(dir).path.replace(File.separatorChar, '/')
|
|
addFileToZip(zip, file, "$prefix/$relative")
|
|
}
|
|
}
|
|
|
|
fun packRnReleaseZip(bundleFile: File, resourceDir: File?, manifestJson: String): File {
|
|
val zipPath = Files.createTempFile("xuqm-rn-release-", ".zip").toFile()
|
|
ZipOutputStream(FileOutputStream(zipPath)).use { zip ->
|
|
writeZipEntry(zip, "rn-manifest.json", manifestJson.toByteArray(StandardCharsets.UTF_8))
|
|
addFileToZip(zip, bundleFile, "bundle/${bundleFile.name}")
|
|
if (resourceDir != null && resourceDir.exists()) {
|
|
addDirectoryToZip(zip, resourceDir, "resources")
|
|
}
|
|
}
|
|
return zipPath
|
|
}
|
|
|
|
fun findHighestVersion(listResp: String): Int {
|
|
return Regex("\"version\"\\s*:\\s*\"?(\\d+)\"?").findAll(listResp)
|
|
.mapNotNull { it.groupValues[1].toIntOrNull() }
|
|
.maxOrNull() ?: 0
|
|
}
|
|
|
|
tasks.register("xuqmRnRelease") {
|
|
group = "xuqm"
|
|
description = "Package RN bundle + resources into zip and upload to XuqmGroup Update Service"
|
|
|
|
doLast {
|
|
val cfg = loadXuqmConfig(projectDir)
|
|
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 moduleId = cfg.getProperty("xuqm.moduleId") ?: throw GradleException("xuqm.moduleId missing")
|
|
val bundleFilePath = cfg.getProperty("xuqm.bundleFile") ?: throw GradleException("xuqm.bundleFile missing")
|
|
val bundleVersion = cfg.getProperty("xuqm.bundleVersion")
|
|
?.trim()
|
|
?.toIntOrNull()
|
|
?: throw GradleException("xuqm.bundleVersion missing or invalid")
|
|
val resourceDirPath = cfg.getProperty("xuqm.resourceDir", "").trim()
|
|
val minCommonVersion = cfg.getProperty("xuqm.minCommonVersion", "").trim()
|
|
val packageName = cfg.getProperty("xuqm.packageName", "").trim()
|
|
val note = cfg.getProperty("xuqm.note", "").trim()
|
|
|
|
val bundleFile = File(bundleFilePath)
|
|
if (!bundleFile.exists()) {
|
|
throw GradleException("Bundle file not found: ${bundleFile.absolutePath}")
|
|
}
|
|
val resourceDir = if (resourceDirPath.isNotBlank()) File(resourceDirPath) else null
|
|
if (resourceDir != null && !resourceDir.exists()) {
|
|
throw GradleException("Resource directory not found: ${resourceDir.absolutePath}")
|
|
}
|
|
|
|
val listResp = httpGet("$serverUrl/api/v1/rn/list?appId=$appKey&moduleId=$moduleId&platform=HARMONY", apiToken)
|
|
val serverVersion = findHighestVersion(listResp)
|
|
println("[xuqm] Server latest bundleVersion: $serverVersion")
|
|
|
|
var releaseVersion = bundleVersion
|
|
if (releaseVersion <= serverVersion && System.console() != null) {
|
|
println("[xuqm] Local bundleVersion is not greater than server latest. Please enter corrected release version info.")
|
|
val inputVersion = promptLine("Release bundleVersion [$bundleVersion]: ")?.trim().orEmpty()
|
|
if (inputVersion.isNotBlank()) {
|
|
releaseVersion = inputVersion.toInt()
|
|
}
|
|
}
|
|
if (releaseVersion <= serverVersion) {
|
|
throw GradleException(
|
|
"Release bundleVersion ($releaseVersion) must be greater than server ($serverVersion). " +
|
|
"Bump the local RN release metadata or override intentionally via script config."
|
|
)
|
|
}
|
|
|
|
val manifestJson = buildString {
|
|
append("{")
|
|
append("\"moduleId\":\"").append(escapeJson(moduleId)).append("\",")
|
|
append("\"platform\":\"HARMONY\",")
|
|
append("\"version\":\"").append(releaseVersion).append("\",")
|
|
append("\"bundleVersion\":").append(releaseVersion).append(",")
|
|
append("\"minCommonVersion\":\"").append(escapeJson(minCommonVersion)).append("\",")
|
|
append("\"packageName\":\"").append(escapeJson(packageName)).append("\",")
|
|
append("\"note\":\"").append(escapeJson(note)).append("\"")
|
|
append("}")
|
|
}
|
|
val zipFile = packRnReleaseZip(bundleFile, resourceDir, manifestJson)
|
|
|
|
println("[xuqm] RN bundle zip: ${zipFile.absolutePath}")
|
|
println("[xuqm] bundleVersion=$releaseVersion, moduleId=$moduleId, minCommonVersion=${minCommonVersion.ifBlank { "-" }}, packageName=${packageName.ifBlank { "-" }}")
|
|
|
|
val parts = mutableMapOf<String, Any>(
|
|
"appId" to appKey,
|
|
"moduleId" to moduleId,
|
|
"platform" to "HARMONY",
|
|
"version" to releaseVersion.toString(),
|
|
"bundle" to zipFile,
|
|
)
|
|
if (minCommonVersion.isNotBlank()) parts["minCommonVersion"] = minCommonVersion
|
|
if (packageName.isNotBlank()) parts["packageName"] = packageName
|
|
if (note.isNotBlank()) parts["note"] = note
|
|
|
|
println("[xuqm] Uploading RN release zip...")
|
|
val uploadResp = httpMultipartPost("$serverUrl/api/v1/rn/upload", apiToken, parts)
|
|
val versionId = parseJson(uploadResp, "id")
|
|
?: throw GradleException("[xuqm] Upload failed:\n$uploadResp")
|
|
println("[xuqm] Uploaded, version ID: $versionId")
|
|
println("[xuqm] Release zip contains rn-manifest.json + bundle + resources")
|
|
}
|
|
}
|