feat(harmony): add xuqm_release hvigorw task, expand IM SDK, ignore .hvigor cache
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
9e5fa3da03
当前提交
de1c7e77e7
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@ build/
|
|||||||
*.iml
|
*.iml
|
||||||
.idea/
|
.idea/
|
||||||
*.log
|
*.log
|
||||||
|
.hvigor/
|
||||||
|
|||||||
@ -1,6 +1,20 @@
|
|||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"signingConfigs": [],
|
"signingConfigs": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"type": "HarmonyOS",
|
||||||
|
"material": {
|
||||||
|
"certpath": "/Users/xuqinmin/.ohos/config/default_XuqmGroup-HarmonySDK_xvtPiZRWbWuJ1guqtszanEOmoD8f2kda5ume6x7cDEg=.cer",
|
||||||
|
"keyAlias": "debugKey",
|
||||||
|
"keyPassword": "0000001BAEFFFD913AED357759CBA7F92502E226B07F444BBB101F06934FC8D4A84C1B5561BC0253532FCC",
|
||||||
|
"profile": "/Users/xuqinmin/.ohos/config/default_XuqmGroup-HarmonySDK_xvtPiZRWbWuJ1guqtszanEOmoD8f2kda5ume6x7cDEg=.p7b",
|
||||||
|
"signAlg": "SHA256withECDSA",
|
||||||
|
"storeFile": "/Users/xuqinmin/.ohos/config/default_XuqmGroup-HarmonySDK_xvtPiZRWbWuJ1guqtszanEOmoD8f2kda5ume6x7cDEg=.p12",
|
||||||
|
"storePassword": "0000001B822DC06852C049293C0D7A3A5ED4C040F995F0DE8E832EA7C75D5C0B9A063C501AE3B14CE6B900"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"products": [
|
"products": [
|
||||||
{
|
{
|
||||||
"name": "default",
|
"name": "default",
|
||||||
@ -12,12 +26,17 @@
|
|||||||
"caseSensitiveCheck": true,
|
"caseSensitiveCheck": true,
|
||||||
"useNormalizedOHMUrl": true
|
"useNormalizedOHMUrl": true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"targetSdkVersion": "6.0.2(22)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"buildModeSet": [
|
"buildModeSet": [
|
||||||
{ "name": "debug" },
|
{
|
||||||
{ "name": "release" }
|
"name": "debug"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"modules": [
|
"modules": [
|
||||||
@ -25,14 +44,24 @@
|
|||||||
"name": "xuqmSdk",
|
"name": "xuqmSdk",
|
||||||
"srcPath": "./xuqm-sdk",
|
"srcPath": "./xuqm-sdk",
|
||||||
"targets": [
|
"targets": [
|
||||||
{ "name": "default", "applyToProducts": ["default"] }
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "entry",
|
"name": "entry",
|
||||||
"srcPath": "./entry",
|
"srcPath": "./entry",
|
||||||
"targets": [
|
"targets": [
|
||||||
{ "name": "default", "applyToProducts": ["default"] }
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
"maskedByOverrideDependencyMap": false
|
"maskedByOverrideDependencyMap": false
|
||||||
},
|
},
|
||||||
"xuqm-sdk": {
|
"xuqm-sdk": {
|
||||||
"name": "xuqm-sdk",
|
"name": "xuqmSdk",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"dynamicDependencies": {},
|
"dynamicDependencies": {},
|
||||||
|
|||||||
@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* XuqmGroup Update Service — HarmonyOS hvigorw Release Task
|
||||||
|
*
|
||||||
|
* HarmonyOS projects use hvigorw (Huawei's Gradle wrapper).
|
||||||
|
* This script registers a task compatible with both hvigorw and standard Gradle.
|
||||||
|
*
|
||||||
|
* Copy to your HarmonyOS module directory and apply:
|
||||||
|
* apply(from = "xuqm_release.gradle.kts") // in build.gradle.kts
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* ./hvigorw xuqmRelease --mode project
|
||||||
|
*
|
||||||
|
* Config: xuqm.properties in the module or project root
|
||||||
|
* ---
|
||||||
|
* xuqm.serverUrl=https://update.dev.xuqinmin.com
|
||||||
|
* xuqm.appId=your-app-id
|
||||||
|
* xuqm.apiToken=your-api-token
|
||||||
|
* xuqm.storeTargets=HUAWEI # optional: HUAWEI,HONOR,...
|
||||||
|
* xuqm.autoPublishAfterReview=false
|
||||||
|
* xuqm.scheduledPublishAt= # optional ISO datetime
|
||||||
|
* xuqm.webhookUrl= # optional
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* Version is read from AppScope/app.json5 (standard HarmonyOS project layout).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.gradle.api.GradleException
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpClient
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
import java.util.Properties
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
// ── Config ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun loadXuqmCfg(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")
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Version from app.json5 ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
data class HarmonyVersion(val name: String, val code: Int)
|
||||||
|
|
||||||
|
fun readHarmonyVersion(projectDir: File): HarmonyVersion {
|
||||||
|
val json5 = listOf(
|
||||||
|
File(projectDir, "AppScope/app.json5"),
|
||||||
|
File(projectDir.parentFile, "AppScope/app.json5"),
|
||||||
|
).firstOrNull { it.exists() } ?: throw GradleException("AppScope/app.json5 not found")
|
||||||
|
|
||||||
|
val content = json5.readText()
|
||||||
|
val name = Regex(""""versionName"\s*:\s*"([^"]+)"""").find(content)?.groupValues?.get(1)
|
||||||
|
?: throw GradleException("versionName not found in app.json5")
|
||||||
|
val code = Regex(""""versionCode"\s*:\s*(\d+)""").find(content)?.groupValues?.get(1)?.toInt()
|
||||||
|
?: throw GradleException("versionCode not found in app.json5")
|
||||||
|
return HarmonyVersion(name, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HTTP helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun httpGet(url: String, token: String): String {
|
||||||
|
val client = HttpClient.newHttpClient()
|
||||||
|
val req = HttpRequest.newBuilder(URI.create(url)).header("Authorization", "Bearer $token").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 client = HttpClient.newHttpClient()
|
||||||
|
val req = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.header("Authorization", "Bearer $token")
|
||||||
|
.header("Content-Type", "multipart/form-data; boundary=$boundary")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofByteArray(baos.toByteArray()))
|
||||||
|
.build()
|
||||||
|
return client.send(req, HttpResponse.BodyHandlers.ofString()).body()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseJson(json: String, key: String) =
|
||||||
|
Regex(""""$key"\s*:\s*"([^"]+)"""").find(json)?.groupValues?.get(1)
|
||||||
|
|
||||||
|
// ── Task ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
tasks.register("xuqmRelease") {
|
||||||
|
group = "xuqm"
|
||||||
|
description = "Build HAP and upload to XuqmGroup Update Service"
|
||||||
|
|
||||||
|
// hvigorw uses 'assembleApp' by default; adjust if your task is named differently
|
||||||
|
dependsOn(tasks.findByName("assembleApp") ?: tasks.findByName("default") ?: return@register)
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val cfg = loadXuqmCfg(projectDir)
|
||||||
|
val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing")
|
||||||
|
val appId = cfg.getProperty("xuqm.appId") ?: throw GradleException("xuqm.appId missing")
|
||||||
|
val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing")
|
||||||
|
val storeTargets = cfg.getProperty("xuqm.storeTargets", "")
|
||||||
|
val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean()
|
||||||
|
val scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "")
|
||||||
|
val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "")
|
||||||
|
|
||||||
|
// ── 1. Read local version ──────────────────────────────────────────
|
||||||
|
val (versionName, versionCode) = readHarmonyVersion(projectDir)
|
||||||
|
println("[xuqm] Local version: $versionName ($versionCode)")
|
||||||
|
|
||||||
|
// ── 2. Check server ────────────────────────────────────────────────
|
||||||
|
val listResp = httpGet("$serverUrl/api/v1/updates/app/list?appId=$appId&platform=ANDROID", apiToken)
|
||||||
|
val serverCode = Regex(""""versionCode"\s*:\s*(\d+)""").findAll(listResp)
|
||||||
|
.mapNotNull { it.groupValues[1].toIntOrNull() }.maxOrNull() ?: 0
|
||||||
|
println("[xuqm] Server latest versionCode: $serverCode")
|
||||||
|
|
||||||
|
if (versionCode <= serverCode) {
|
||||||
|
throw GradleException(
|
||||||
|
"[xuqm] Local versionCode ($versionCode) ≤ server ($serverCode). " +
|
||||||
|
"Bump versionCode in AppScope/app.json5 first."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. Locate HAP ──────────────────────────────────────────────────
|
||||||
|
val hapDir = File(projectDir, "entry/build/default/outputs/default")
|
||||||
|
val hapFile = hapDir.listFiles { f -> f.extension == "hap" }?.firstOrNull()
|
||||||
|
?: throw GradleException("HAP not found in ${hapDir.absolutePath}")
|
||||||
|
println("[xuqm] HAP: ${hapFile.absolutePath}")
|
||||||
|
|
||||||
|
// ── 4. Upload ──────────────────────────────────────────────────────
|
||||||
|
val parts = mutableMapOf<String, Any>(
|
||||||
|
"appId" to appId, "platform" to "ANDROID",
|
||||||
|
"versionName" to versionName, "versionCode" to versionCode,
|
||||||
|
"forceUpdate" to "false", "autoPublishAfterReview" to autoPublish.toString(),
|
||||||
|
"apkFile" to hapFile,
|
||||||
|
)
|
||||||
|
if (storeTargets.isNotBlank()) parts["storeSubmitTargets"] = "[\"${storeTargets.split(",").joinToString("\",\"")}\"]"
|
||||||
|
if (scheduledAt.isNotBlank()) parts["scheduledPublishAt"] = scheduledAt
|
||||||
|
if (webhookUrl.isNotBlank()) parts["webhookUrl"] = webhookUrl
|
||||||
|
|
||||||
|
println("[xuqm] Uploading HAP...")
|
||||||
|
val uploadResp = httpMultipartPost("$serverUrl/api/v1/updates/app/upload", apiToken, parts)
|
||||||
|
val versionId = parseJson(uploadResp, "id")
|
||||||
|
?: throw GradleException("[xuqm] Upload failed:\n$uploadResp")
|
||||||
|
println("[xuqm] Uploaded, version ID: $versionId")
|
||||||
|
|
||||||
|
// ── 5. Trigger store submission ────────────────────────────────────
|
||||||
|
if (storeTargets.isNotBlank()) {
|
||||||
|
println("[xuqm] Triggering store submission: $storeTargets")
|
||||||
|
val body = """{"storeTypes":[${storeTargets.split(",").joinToString(",") { "\"$it\"" }}]}"""
|
||||||
|
val client = HttpClient.newHttpClient()
|
||||||
|
val req = HttpRequest.newBuilder(URI.create("$serverUrl/api/v1/updates/store/app/$versionId/execute-submit"))
|
||||||
|
.header("Authorization", "Bearer $apiToken")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(body)).build()
|
||||||
|
val storeResp = client.send(req, HttpResponse.BodyHandlers.ofString())
|
||||||
|
println("[xuqm] Store submission HTTP ${storeResp.statusCode()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
println("[xuqm] Done.")
|
||||||
|
if (scheduledAt.isNotBlank()) {
|
||||||
|
println("[xuqm] Will publish at: $scheduledAt")
|
||||||
|
} else if (autoPublish) {
|
||||||
|
println("[xuqm] Will auto-publish after store reviews pass.")
|
||||||
|
} else {
|
||||||
|
println("[xuqm] Publish manually: POST $serverUrl/api/v1/updates/app/$versionId/publish")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -323,6 +323,20 @@ export class ImClient {
|
|||||||
return HttpClient.get<ImGroup>('/api/im/groups/' + encodeURIComponent(groupId), this.buildAppQuery())
|
return HttpClient.get<ImGroup>('/api/im/groups/' + encodeURIComponent(groupId), this.buildAppQuery())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listGroupMembers(groupId: string): Promise<UserProfile[]> {
|
||||||
|
return HttpClient.get<UserProfile[]>(
|
||||||
|
'/api/im/groups/' + encodeURIComponent(groupId) + '/members',
|
||||||
|
this.buildAppQuery(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchGroupMembers(groupId: string, keyword: string, size: number = 20): Promise<UserProfile[]> {
|
||||||
|
return HttpClient.get<UserProfile[]>(
|
||||||
|
'/api/im/groups/' + encodeURIComponent(groupId) + '/members/search',
|
||||||
|
this.buildSearchQuery(keyword, size),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async searchUsers(keyword: string, size: number = 20): Promise<UserProfile[]> {
|
async searchUsers(keyword: string, size: number = 20): Promise<UserProfile[]> {
|
||||||
return HttpClient.get<UserProfile[]>(
|
return HttpClient.get<UserProfile[]>(
|
||||||
'/api/im/admin/users/search',
|
'/api/im/admin/users/search',
|
||||||
@ -360,6 +374,14 @@ export class ImClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async revokeMessage(messageId: string): Promise<ImMessage> {
|
||||||
|
return HttpClient.post<ImMessage>(
|
||||||
|
'/api/im/messages/' + encodeURIComponent(messageId) + '/revoke',
|
||||||
|
this.buildAppBody(),
|
||||||
|
this.buildAppQuery(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async sendFriendRequest(toUserId: string, remark: string | null = null): Promise<FriendRequest> {
|
async sendFriendRequest(toUserId: string, remark: string | null = null): Promise<FriendRequest> {
|
||||||
const params = new FriendRequestBody()
|
const params = new FriendRequestBody()
|
||||||
params.appId = SDKContext.getConfig().appKey
|
params.appId = SDKContext.getConfig().appKey
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户