docs(sdk): 添加 Android SDK 文档和 API 设计规范

- 新增 Android SDK 使用文档,包含模块结构、集成方式和快速开始指南
- 添加 SDK API 重设计规范,统一初始化和登录接口设计
- 补充安全设计规范,完善 UserSig 鉴权和敏感数据处理方案
- 创建平台 REST API 规范,定义服务端到服务端的调用接口
- 添加离线推送架构设计,集成各大厂商推送服务与 IM 联动方案
这个提交包含在:
XuqmGroup 2026-04-29 15:46:39 +08:00
父节点 48ddea9f68
当前提交 a1614840e5
共有 10 个文件被更改,包括 121 次插入64 次删除

查看文件

@ -55,7 +55,7 @@ dependencies {
```kotlin ```kotlin
XuqmSDK.initialize( XuqmSDK.initialize(
context = this, context = this,
appId = "ak_your_app_id", appKey = "ak_your_app_key",
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN
) )
``` ```
@ -102,7 +102,7 @@ sample-app 里也提供了一个“环境设置”页面,可以直接在外网
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
|------|------|------| |------|------|------|
| `appId` | String | 应用标识(租户平台获取) | | `appKey` | String | 应用标识(租户平台获取) |
| `logLevel` | LogLevel | 日志等级 | | `logLevel` | LogLevel | 日志等级 |
### TokenStore ### TokenStore

查看文件

@ -1,11 +1,14 @@
import org.gradle.api.publish.PublishingExtension
apply(plugin = "maven-publish") apply(plugin = "maven-publish")
afterEvaluate { afterEvaluate {
(extensions.findByType(com.android.build.gradle.LibraryExtension::class.java))?.let { if (extensions.findByName("android") != null) {
val releaseComponent = components.findByName("release") ?: return@afterEvaluate
extensions.configure<PublishingExtension> { extensions.configure<PublishingExtension> {
publications { publications {
register<MavenPublication>("release") { register<MavenPublication>("release") {
from(components["release"]) from(releaseComponent)
groupId = rootProject.group.toString() groupId = rootProject.group.toString()
artifactId = project.name artifactId = project.name
version = rootProject.version.toString() version = rootProject.version.toString()
@ -13,10 +16,10 @@ afterEvaluate {
} }
repositories { repositories {
maven { maven {
url = uri(rootProject.ext["nexusUrl"] as String) url = uri(rootProject.extra["nexusUrl"] as String)
credentials { credentials {
username = rootProject.ext["nexusUser"] as String username = rootProject.extra["nexusUser"] as String
password = rootProject.ext["nexusPassword"] as String password = rootProject.extra["nexusPassword"] as String
} }
} }
} }

查看文件

@ -18,7 +18,7 @@ class XuqmSampleApp : Application() {
AppDependencies.init(this) AppDependencies.init(this)
XuqmSDK.initialize( XuqmSDK.initialize(
context = this, context = this,
appId = "ak_demo_chat", appKey = "ak_demo_chat",
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN, logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN,
) )
appScope.launch { appScope.launch {

查看文件

@ -1,7 +1,7 @@
package com.xuqm.sdk package com.xuqm.sdk
data class XuqmLoginSession( data class XuqmLoginSession(
val appId: String, val appKey: String,
val userId: String, val userId: String,
val userSig: String, val userSig: String,
val nickname: String? = null, val nickname: String? = null,

查看文件

@ -27,10 +27,10 @@ object XuqmSDK {
fun initialize( fun initialize(
context: Context, context: Context,
appId: String, appKey: String,
logLevel: LogLevel = LogLevel.WARN, logLevel: LogLevel = LogLevel.WARN,
) { ) {
config = SDKConfig(appId, logLevel) config = SDKConfig(appKey, logLevel)
appContext = context.applicationContext appContext = context.applicationContext
tokenStore = TokenStore(context.applicationContext) tokenStore = TokenStore(context.applicationContext)
ApiClient.init(config, tokenStore) ApiClient.init(config, tokenStore)
@ -55,7 +55,7 @@ object XuqmSDK {
check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." } check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." }
} }
val appId: String get() = config.appId val appKey: String get() = config.appKey
val currentLoginSession: XuqmLoginSession? val currentLoginSession: XuqmLoginSession?
get() = loginSession get() = loginSession
@ -68,7 +68,7 @@ object XuqmSDK {
): XuqmLoginSession = withContext(Dispatchers.IO) { ): XuqmLoginSession = withContext(Dispatchers.IO) {
requireInit() requireInit()
val session = XuqmLoginSession( val session = XuqmLoginSession(
appId = appId, appKey = appKey,
userId = userId, userId = userId,
userSig = userSig, userSig = userSig,
nickname = nickname, nickname = nickname,

查看文件

@ -1,7 +1,7 @@
package com.xuqm.sdk.core package com.xuqm.sdk.core
data class SDKConfig( data class SDKConfig(
val appId: String, val appKey: String,
val logLevel: LogLevel = LogLevel.WARN, val logLevel: LogLevel = LogLevel.WARN,
) )

查看文件

@ -144,10 +144,10 @@ object ImSDK {
} }
suspend fun editMessage(messageId: String, content: String): ImMessage = suspend fun editMessage(messageId: String, content: String): ImMessage =
withContext(Dispatchers.IO) { api.editMessage(messageId, XuqmSDK.appId, EditMessageRequest(content)).data ?: throw IllegalStateException("edit message failed") } withContext(Dispatchers.IO) { api.editMessage(messageId, XuqmSDK.appKey, EditMessageRequest(content)).data ?: throw IllegalStateException("edit message failed") }
suspend fun revokeMessage(messageId: String): ImMessage = suspend fun revokeMessage(messageId: String): ImMessage =
withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appId).data ?: throw IllegalStateException("revoke message failed") } withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appKey).data ?: throw IllegalStateException("revoke message failed") }
fun sendImageMessage( fun sendImageMessage(
toId: String, toId: String,
@ -407,7 +407,7 @@ object ImSDK {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
api.fetchHistory( api.fetchHistory(
toId, toId,
XuqmSDK.appId, XuqmSDK.appKey,
msgType, msgType,
keyword, keyword,
startTime?.toString(), startTime?.toString(),
@ -432,7 +432,7 @@ object ImSDK {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
api.fetchGroupHistory( api.fetchGroupHistory(
groupId, groupId,
XuqmSDK.appId, XuqmSDK.appKey,
msgType, msgType,
keyword, keyword,
startTime?.toString(), startTime?.toString(),
@ -485,30 +485,30 @@ object ImSDK {
} }
suspend fun listGroups(): List<ImGroup> = suspend fun listGroups(): List<ImGroup> =
withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() } withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appKey).data ?: emptyList() }
suspend fun createGroup(name: String, memberIds: List<String>, groupType: String = "WORK"): ImGroup? = suspend fun createGroup(name: String, memberIds: List<String>, groupType: String = "WORK"): ImGroup? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
api.createGroup(XuqmSDK.appId, CreateGroupRequest(name, memberIds, groupType)).data api.createGroup(XuqmSDK.appKey, CreateGroupRequest(name, memberIds, groupType)).data
} }
suspend fun getGroupInfo(groupId: String): ImGroup? = suspend fun getGroupInfo(groupId: String): ImGroup? =
withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data } withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data }
suspend fun listGroupMembers(groupId: String): List<UserProfile> = suspend fun listGroupMembers(groupId: String): List<UserProfile> =
withContext(Dispatchers.IO) { api.listGroupMembers(groupId, XuqmSDK.appId).data ?: emptyList() } withContext(Dispatchers.IO) { api.listGroupMembers(groupId, XuqmSDK.appKey).data ?: emptyList() }
suspend fun searchGroupMembers(groupId: String, keyword: String, size: Int = 20): List<UserProfile> = suspend fun searchGroupMembers(groupId: String, keyword: String, size: Int = 20): List<UserProfile> =
withContext(Dispatchers.IO) { api.searchGroupMembers(groupId, XuqmSDK.appId, keyword, size).data ?: emptyList() } withContext(Dispatchers.IO) { api.searchGroupMembers(groupId, XuqmSDK.appKey, keyword, size).data ?: emptyList() }
suspend fun listPublicGroups(keyword: String? = null): List<ImGroup> = suspend fun listPublicGroups(keyword: String? = null): List<ImGroup> =
withContext(Dispatchers.IO) { api.listPublicGroups(XuqmSDK.appId, keyword).data ?: emptyList() } withContext(Dispatchers.IO) { api.listPublicGroups(XuqmSDK.appKey, keyword).data ?: emptyList() }
suspend fun searchUsers(keyword: String, size: Int = 20): List<UserProfile> = suspend fun searchUsers(keyword: String, size: Int = 20): List<UserProfile> =
withContext(Dispatchers.IO) { api.searchUsers(XuqmSDK.appId, keyword, size).data ?: emptyList() } withContext(Dispatchers.IO) { api.searchUsers(XuqmSDK.appKey, keyword, size).data ?: emptyList() }
suspend fun searchGroups(keyword: String, size: Int = 20): List<ImGroup> = suspend fun searchGroups(keyword: String, size: Int = 20): List<ImGroup> =
withContext(Dispatchers.IO) { api.searchGroups(XuqmSDK.appId, keyword, size).data ?: emptyList() } withContext(Dispatchers.IO) { api.searchGroups(XuqmSDK.appKey, keyword, size).data ?: emptyList() }
suspend fun searchMessages( suspend fun searchMessages(
keyword: String? = null, keyword: String? = null,
@ -521,7 +521,7 @@ object ImSDK {
): PageResult<ImMessage> = ): PageResult<ImMessage> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
api.searchMessages( api.searchMessages(
XuqmSDK.appId, XuqmSDK.appKey,
keyword, keyword,
chatType, chatType,
msgType, msgType,
@ -554,49 +554,49 @@ object ImSDK {
withContext(Dispatchers.IO) { api.dismissGroup(groupId) } withContext(Dispatchers.IO) { api.dismissGroup(groupId) }
suspend fun sendGroupJoinRequest(groupId: String, remark: String? = null): GroupJoinRequest? = suspend fun sendGroupJoinRequest(groupId: String, remark: String? = null): GroupJoinRequest? =
withContext(Dispatchers.IO) { api.sendGroupJoinRequest(groupId, XuqmSDK.appId, remark).data } withContext(Dispatchers.IO) { api.sendGroupJoinRequest(groupId, XuqmSDK.appKey, remark).data }
suspend fun listGroupJoinRequests(groupId: String): List<GroupJoinRequest> = suspend fun listGroupJoinRequests(groupId: String): List<GroupJoinRequest> =
withContext(Dispatchers.IO) { api.listGroupJoinRequests(groupId, XuqmSDK.appId).data ?: emptyList() } withContext(Dispatchers.IO) { api.listGroupJoinRequests(groupId, XuqmSDK.appKey).data ?: emptyList() }
suspend fun acceptGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? = suspend fun acceptGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
withContext(Dispatchers.IO) { api.acceptGroupJoinRequest(groupId, requestId, XuqmSDK.appId).data } withContext(Dispatchers.IO) { api.acceptGroupJoinRequest(groupId, requestId, XuqmSDK.appKey).data }
suspend fun rejectGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? = suspend fun rejectGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
withContext(Dispatchers.IO) { api.rejectGroupJoinRequest(groupId, requestId, XuqmSDK.appId).data } withContext(Dispatchers.IO) { api.rejectGroupJoinRequest(groupId, requestId, XuqmSDK.appKey).data }
suspend fun listFriends(): List<String> = suspend fun listFriends(): List<String> =
withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appId).data ?: emptyList() } withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appKey).data ?: emptyList() }
suspend fun addFriend(friendId: String) = suspend fun addFriend(friendId: String) =
withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appId, friendId) } withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appKey, friendId) }
suspend fun removeFriend(friendId: String) = suspend fun removeFriend(friendId: String) =
withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appId) } withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appKey) }
suspend fun listFriendRequests(direction: String = "incoming"): List<FriendRequest> = suspend fun listFriendRequests(direction: String = "incoming"): List<FriendRequest> =
withContext(Dispatchers.IO) { api.listFriendRequests(XuqmSDK.appId, direction).data ?: emptyList() } withContext(Dispatchers.IO) { api.listFriendRequests(XuqmSDK.appKey, direction).data ?: emptyList() }
suspend fun sendFriendRequest(friendId: String, remark: String? = null): FriendRequest? = suspend fun sendFriendRequest(friendId: String, remark: String? = null): FriendRequest? =
withContext(Dispatchers.IO) { api.sendFriendRequest(XuqmSDK.appId, friendId, remark).data } withContext(Dispatchers.IO) { api.sendFriendRequest(XuqmSDK.appKey, friendId, remark).data }
suspend fun acceptFriendRequest(requestId: String): FriendRequest? = suspend fun acceptFriendRequest(requestId: String): FriendRequest? =
withContext(Dispatchers.IO) { api.acceptFriendRequest(requestId, XuqmSDK.appId).data } withContext(Dispatchers.IO) { api.acceptFriendRequest(requestId, XuqmSDK.appKey).data }
suspend fun rejectFriendRequest(requestId: String): FriendRequest? = suspend fun rejectFriendRequest(requestId: String): FriendRequest? =
withContext(Dispatchers.IO) { api.rejectFriendRequest(requestId, XuqmSDK.appId).data } withContext(Dispatchers.IO) { api.rejectFriendRequest(requestId, XuqmSDK.appKey).data }
suspend fun listBlacklist(): List<BlacklistEntry> = suspend fun listBlacklist(): List<BlacklistEntry> =
withContext(Dispatchers.IO) { api.listBlacklist(XuqmSDK.appId).data ?: emptyList() } withContext(Dispatchers.IO) { api.listBlacklist(XuqmSDK.appKey).data ?: emptyList() }
suspend fun addToBlacklist(blockedUserId: String): BlacklistEntry? = suspend fun addToBlacklist(blockedUserId: String): BlacklistEntry? =
withContext(Dispatchers.IO) { api.addToBlacklist(XuqmSDK.appId, blockedUserId).data } withContext(Dispatchers.IO) { api.addToBlacklist(XuqmSDK.appKey, blockedUserId).data }
suspend fun removeFromBlacklist(blockedUserId: String) = suspend fun removeFromBlacklist(blockedUserId: String) =
withContext(Dispatchers.IO) { api.removeFromBlacklist(XuqmSDK.appId, blockedUserId) } withContext(Dispatchers.IO) { api.removeFromBlacklist(XuqmSDK.appKey, blockedUserId) }
suspend fun getProfile(userId: String) = suspend fun getProfile(userId: String) =
withContext(Dispatchers.IO) { api.getProfile(userId, XuqmSDK.appId).data } withContext(Dispatchers.IO) { api.getProfile(userId, XuqmSDK.appKey).data }
suspend fun updateProfile( suspend fun updateProfile(
userId: String, userId: String,
@ -604,11 +604,11 @@ object ImSDK {
avatar: String? = null, avatar: String? = null,
gender: String? = null, gender: String? = null,
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
api.updateProfile(userId, XuqmSDK.appId, nickname, avatar, gender).data api.updateProfile(userId, XuqmSDK.appKey, nickname, avatar, gender).data
} }
suspend fun listConversations(): List<ConversationData> = suspend fun listConversations(): List<ConversationData> =
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appId).data ?: emptyList() } withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appKey).data ?: emptyList() }
suspend fun getTotalUnreadCount(): Int = suspend fun getTotalUnreadCount(): Int =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -626,13 +626,13 @@ object ImSDK {
} }
suspend fun markRead(targetId: String, chatType: String = "SINGLE") = suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appId, chatType) } withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appKey, chatType) }
suspend fun setDraft(targetId: String, chatType: String, draft: String) = suspend fun setDraft(targetId: String, chatType: String, draft: String) =
withContext(Dispatchers.IO) { api.setDraft(targetId, XuqmSDK.appId, chatType, draft) } withContext(Dispatchers.IO) { api.setDraft(targetId, XuqmSDK.appKey, chatType, draft) }
suspend fun deleteConversation(targetId: String, chatType: String) = suspend fun deleteConversation(targetId: String, chatType: String) =
withContext(Dispatchers.IO) { api.deleteConversation(targetId, XuqmSDK.appId, chatType) } withContext(Dispatchers.IO) { api.deleteConversation(targetId, XuqmSDK.appKey, chatType) }
fun addListener(listener: ImEventListener) { fun addListener(listener: ImEventListener) {
Log.d(TAG, "addListener listener=${listener.javaClass.name}") Log.d(TAG, "addListener listener=${listener.javaClass.name}")
@ -669,7 +669,7 @@ object ImSDK {
_connectionState.value = ImConnectionState.Connecting _connectionState.value = ImConnectionState.Connecting
currentToken = token currentToken = token
Log.d(TAG, "connectWithToken userId=$currentUserId activeGroups=${activeGroupSubscriptions.size}") Log.d(TAG, "connectWithToken userId=$currentUserId activeGroups=${activeGroupSubscriptions.size}")
client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appId) client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appKey)
client?.addListener(connectionListener) client?.addListener(connectionListener)
listeners.forEach { client?.addListener(it) } listeners.forEach { client?.addListener(it) }
reconnectEnabled = true reconnectEnabled = true
@ -742,7 +742,7 @@ object ImSDK {
): ImMessage { ): ImMessage {
return ImMessage( return ImMessage(
id = messageId, id = messageId,
appId = XuqmSDK.appId, appId = XuqmSDK.appKey,
fromId = currentUserId, fromId = currentUserId,
toId = toId, toId = toId,
chatType = chatType, chatType = chatType,
@ -769,7 +769,7 @@ object ImSDK {
reconnectAttempts += 1 reconnectAttempts += 1
_connectionState.value = ImConnectionState.Connecting _connectionState.value = ImConnectionState.Connecting
Log.d(TAG, "reconnect attempt=$reconnectAttempts") Log.d(TAG, "reconnect attempt=$reconnectAttempts")
client = ImClient(ServiceEndpointRegistry.imWsUrl, currentToken, XuqmSDK.appId) client = ImClient(ServiceEndpointRegistry.imWsUrl, currentToken, XuqmSDK.appKey)
client?.addListener(connectionListener) client?.addListener(connectionListener)
listeners.forEach { client?.addListener(it) } listeners.forEach { client?.addListener(it) }
client?.connect() client?.connect()

查看文件

@ -67,7 +67,7 @@ object PushSDK {
scope.launch { scope.launch {
runCatching { runCatching {
api.setReceivePush( api.setReceivePush(
appId = XuqmSDK.appId, appId = XuqmSDK.appKey,
userId = resolvedUserId, userId = resolvedUserId,
enabled = enabled, enabled = enabled,
) )
@ -95,7 +95,7 @@ object PushSDK {
scope.launch { scope.launch {
runCatching { runCatching {
api.registerDevice( api.registerDevice(
appId = XuqmSDK.appId, appId = XuqmSDK.appKey,
userId = userId, userId = userId,
vendor = vendor.name, vendor = vendor.name,
token = pushToken, token = pushToken,
@ -114,7 +114,7 @@ object PushSDK {
XuqmSDK.requireInit() XuqmSDK.requireInit()
scope.launch { scope.launch {
runCatching { runCatching {
api.unregisterDevice(XuqmSDK.appId, userId) api.unregisterDevice(XuqmSDK.appKey, userId)
registeredUserId.compareAndSet(userId, null) registeredUserId.compareAndSet(userId, null)
store(XuqmSDK.appContext).updateLastUserId(null) store(XuqmSDK.appContext).updateLastUserId(null)
} }

查看文件

@ -10,8 +10,13 @@
* Config: xuqm.properties in the project root (or module root) * Config: xuqm.properties in the project root (or module root)
* --- * ---
* xuqm.serverUrl=https://update.dev.xuqinmin.com * xuqm.serverUrl=https://update.dev.xuqinmin.com
* xuqm.appId=your-app-id * xuqm.appKey=your-app-key
* xuqm.apiToken=your-api-token * xuqm.apiToken=your-api-token
* xuqm.storeTargets=HUAWEI,MI,OPPO # optional
* xuqm.autoPublishAfterReview=false
* xuqm.publishImmediately=false # optional: publish update-service record immediately
* xuqm.scheduledPublishAt= # optional ISO datetime
* xuqm.webhookUrl= # optional webhook for review status changes
* --- * ---
* *
* The task: * The task:
@ -31,7 +36,6 @@ import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse
import java.nio.file.Files
import java.util.Properties import java.util.Properties
import java.util.UUID import java.util.UUID
@ -101,6 +105,35 @@ fun parseJson(json: String, key: String): String? =
fun parseJsonInt(json: String, key: String): Int? = fun parseJsonInt(json: String, key: String): Int? =
Regex("\"$key\"\\s*:\\s*(\\d+)").find(json)?.groupValues?.get(1)?.toIntOrNull() Regex("\"$key\"\\s*:\\s*(\\d+)").find(json)?.groupValues?.get(1)?.toIntOrNull()
fun promptLine(message: String): String? {
val console = System.console() ?: return null
print(message)
return console.readLine()
}
fun promptYesNo(message: String, default: Boolean = false): Boolean {
val suffix = if (default) " [Y/n]: " else " [y/N]: "
val answer = promptLine(message + suffix)?.trim().orEmpty()
return when {
answer.isBlank() -> default
answer.equals("y", ignoreCase = true) || answer.equals("yes", ignoreCase = true) -> true
answer.equals("n", ignoreCase = true) || answer.equals("no", ignoreCase = true) -> false
else -> default
}
}
fun promptReleaseMode(): String {
val answer = promptLine(
"Publish mode? [1=manual, 2=publish now, 3=scheduled, 4=auto after review]: "
)?.trim().orEmpty()
return when (answer) {
"2" -> "NOW"
"3" -> "SCHEDULED"
"4" -> "AUTO_REVIEW"
else -> "MANUAL"
}
}
// ── Task registration ───────────────────────────────────────────────────── // ── Task registration ─────────────────────────────────────────────────────
tasks.register("xuqmRelease") { tasks.register("xuqmRelease") {
@ -113,12 +146,20 @@ tasks.register("xuqmRelease") {
doLast { doLast {
val cfg = loadXuqmConfig(projectDir) val cfg = loadXuqmConfig(projectDir)
val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing") val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing")
val appId = cfg.getProperty("xuqm.appId") ?: throw GradleException("xuqm.appId 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 storeTargets = cfg.getProperty("xuqm.storeTargets", "") // e.g. "HUAWEI,MI,OPPO" val storeTargets = cfg.getProperty("xuqm.storeTargets", "") // e.g. "HUAWEI,MI,OPPO"
val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean() val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean()
val scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "") val publishImmediately = cfg.getProperty("xuqm.publishImmediately", "false").toBoolean()
var scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "")
val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "") val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "")
var publishMode = cfg.getProperty("xuqm.publishMode", "").trim().uppercase()
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()
}
}
// ── 1. Read local version ────────────────────────────────────────── // ── 1. Read local version ──────────────────────────────────────────
val android = project.extensions.findByName("android") val android = project.extensions.findByName("android")
@ -129,10 +170,10 @@ tasks.register("xuqmRelease") {
val versionCode = (defaultConfig::class.java.getMethod("getVersionCode").invoke(defaultConfig) as? Int) val versionCode = (defaultConfig::class.java.getMethod("getVersionCode").invoke(defaultConfig) as? Int)
?: throw GradleException("versionCode not set") ?: throw GradleException("versionCode not set")
val applicationId = defaultConfig::class.java.getMethod("getApplicationId").invoke(defaultConfig) as? String ?: "" val applicationId = defaultConfig::class.java.getMethod("getApplicationId").invoke(defaultConfig) as? String ?: ""
println("[xuqm] Local version: $versionName ($versionCode), packageName: $applicationId") println("[xuqm] Local version: $versionName ($versionCode), packageName: $applicationId, appKey: $appKey")
// ── 2. Check server latest ───────────────────────────────────────── // ── 2. Check server latest ─────────────────────────────────────────
val listResp = httpGet("$serverUrl/api/v1/updates/app/list?appId=$appId&platform=ANDROID", apiToken) val listResp = httpGet("$serverUrl/api/v1/updates/app/list?appId=$appKey&platform=ANDROID", apiToken)
// Find highest published versionCode // Find highest published versionCode
val serverVersionCode = Regex("\"versionCode\"\\s*:\\s*(\\d+)").findAll(listResp) val serverVersionCode = Regex("\"versionCode\"\\s*:\\s*(\\d+)").findAll(listResp)
.mapNotNull { it.groupValues[1].toIntOrNull() } .mapNotNull { it.groupValues[1].toIntOrNull() }
@ -154,7 +195,7 @@ tasks.register("xuqmRelease") {
// ── 4. Upload to update service ─────────────────────────────────── // ── 4. Upload to update service ───────────────────────────────────
val parts = mutableMapOf<String, Any>( val parts = mutableMapOf<String, Any>(
"appId" to appId, "appId" to appKey,
"platform" to "ANDROID", "platform" to "ANDROID",
"versionName" to versionName, "versionName" to versionName,
"versionCode" to versionCode, "versionCode" to versionCode,
@ -166,6 +207,7 @@ tasks.register("xuqmRelease") {
if (storeTargets.isNotBlank()) parts["storeSubmitTargets"] = "[\"${storeTargets.split(",").joinToString("\",\"")}\"]" if (storeTargets.isNotBlank()) parts["storeSubmitTargets"] = "[\"${storeTargets.split(",").joinToString("\",\"")}\"]"
if (scheduledAt.isNotBlank()) parts["scheduledPublishAt"] = scheduledAt if (scheduledAt.isNotBlank()) parts["scheduledPublishAt"] = scheduledAt
if (webhookUrl.isNotBlank()) parts["webhookUrl"] = webhookUrl if (webhookUrl.isNotBlank()) parts["webhookUrl"] = webhookUrl
if (publishMode == "NOW") parts["publishImmediately"] = "true"
println("[xuqm] Uploading APK...") println("[xuqm] Uploading APK...")
val uploadResp = httpMultipartPost("$serverUrl/api/v1/updates/app/upload", apiToken, parts) val uploadResp = httpMultipartPost("$serverUrl/api/v1/updates/app/upload", apiToken, parts)
@ -187,12 +229,24 @@ tasks.register("xuqmRelease") {
println("[xuqm] Store submission triggered (HTTP ${storeResp.statusCode()})") println("[xuqm] Store submission triggered (HTTP ${storeResp.statusCode()})")
} }
println("[xuqm] Done. Version is in DRAFT state.") if (publishImmediately || publishMode == "NOW") {
if (scheduledAt.isNotBlank()) { println("[xuqm] Published immediately in update service.")
println("[xuqm] Will auto-publish at: $scheduledAt") } else if (publishMode == "SCHEDULED" || scheduledAt.isNotBlank()) {
println("[xuqm] Will auto-publish at: ${scheduledAt.ifBlank { "(use update service scheduled publish config)" }}")
} else if (autoPublish) { } else if (autoPublish) {
println("[xuqm] Will auto-publish after all store reviews pass.") println("[xuqm] Will auto-publish after all store reviews pass.")
} else { } else if (System.console() != null && promptYesNo("[xuqm] Publish now?", false)) {
val client = HttpClient.newHttpClient()
val req = HttpRequest.newBuilder(URI.create("$serverUrl/api/v1/updates/app/$versionId/publish"))
.header("Authorization", "Bearer $apiToken")
.POST(HttpRequest.BodyPublishers.noBody())
.build()
val publishResp = client.send(req, HttpResponse.BodyHandlers.ofString())
println("[xuqm] Publish HTTP ${publishResp.statusCode()}")
}
println("[xuqm] Done. Version is in DRAFT state.")
if (!publishImmediately && publishMode != "NOW" && scheduledAt.isBlank() && !autoPublish) {
println("[xuqm] Publish manually: POST $serverUrl/api/v1/updates/app/$versionId/publish") println("[xuqm] Publish manually: POST $serverUrl/api/v1/updates/app/$versionId/publish")
} }
} }

查看文件

@ -36,7 +36,7 @@ object UpdateSDK {
val versionCode = context.packageManager val versionCode = context.packageManager
.getPackageInfo(context.packageName, 0).longVersionCode.toInt() .getPackageInfo(context.packageName, 0).longVersionCode.toInt()
runCatching { runCatching {
api.checkUpdate(XuqmSDK.appId, "ANDROID", versionCode).data?.let { api.checkUpdate(XuqmSDK.appKey, "ANDROID", versionCode).data?.let {
it.copy(downloadUrl = normalizeDownloadUrl(it.downloadUrl) ?: it.downloadUrl) it.copy(downloadUrl = normalizeDownloadUrl(it.downloadUrl) ?: it.downloadUrl)
} }
}.getOrNull() }.getOrNull()