比较提交

...

4 次代码提交

作者 SHA1 备注 提交日期
XuqmGroup
de1c7e77e7 feat(harmony): add xuqm_release hvigorw task, expand IM SDK, ignore .hvigor cache
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:37:18 +08:00
XuqmGroup
9e5fa3da03 feat(chat): 添加聊天界面视图模型和联系人管理功能
- 实现 ChatViewModel 处理消息收发、历史记录加载和状态管理
- 添加消息搜索、草稿保存、引用回复等功能
- 实现多媒体附件发送包括图片、视频、音频和文件
- 添加群组提及用户功能和消息撤回机制
- 实现联系人管理功能包括好友搜索、添加、删除和黑名单管理
- 添加好友请求处理和实时消息监听
- 实现会话列表管理包含未读消息统计和实时更新
- 集成 IM SDK 的连接状态管理和事件监听
- 添加消息状态跟踪和超时处理机制
- 实现数据缓存机制优化用户体验
2026-04-28 22:32:20 +08:00
XuqmGroup
c656bdd202 feat(im): 添加即时通讯SDK核心功能
- 实现IM API接口定义,包括消息、群组、好友、黑名单等功能
- 定义IM消息相关数据模型,包含聊天类型、消息类型、用户资料等
- 实现ImSDK单例类,提供登录、消息发送、群组管理、好友管理等核心功能
- 添加WebSocket连接管理,支持自动重连机制
- 实现历史消息查询、群组操作、用户资料管理等API调用
- 添加会话状态管理,支持置顶、静音、草稿等功能
- 集成文件上传结果,支持多媒体消息发送
- 实现连接状态监听和事件回调机制
2026-04-28 21:05:06 +08:00
XuqmGroup
d7f156f160 feat(chat): 添加聊天界面和文件更新SDK功能
- 实现完整的聊天界面UI组件,支持文本、图片、视频、音频、文件等多种消息类型
- 集成IM消息收发功能,实现消息气泡显示和用户头像占位符
- 添加媒体文件选择和拍摄功能,支持相册图片、视频及相机拍照录像
- 实现语音录制和播放功能,包含按住说话交互和权限处理
- 添加群组提及功能,支持@用户和提及候选列表显示
- 实现消息回复和引用功能,支持消息长按回复操作
- 添加本地消息搜索功能,支持搜索当前会话的历史消息
- 实现文件上传下载功能,集成FileSDK进行文件传输管理
- 添加应用更新检查功能,集成UpdateSDK支持版本更新
- 实现消息状态显示,包括发送、送达、已读等状态标识
- 添加群组已读人数统计,显示消息在群聊中的阅读情况
- 实现草稿保存和恢复功能,支持断点续聊体验
- 添加连接状态横幅,实时显示IM服务连接状态
- 实现滚动加载更多历史消息,优化大量消息的性能表现
- 添加多媒体文件下载保存功能,支持保存到应用专属目录
2026-04-28 20:11:37 +08:00
共有 38 个文件被更改,包括 118789 次插入149 次删除

1
.gitignore vendored
查看文件

@ -8,3 +8,4 @@ build/
*.iml *.iml
.idea/ .idea/
*.log *.log
.hvigor/

1
.hvigor/cache/file-cache.json vendored 普通文件

文件差异因一行或多行过长而隐藏

1
.hvigor/cache/meta.json vendored 普通文件
查看文件

@ -0,0 +1 @@
{"compileSdkVersion":"6.0.2(22)","hvigorVersion":"6.22.4","toolChainsVersion":"6.0.2.130"}

1
.hvigor/cache/task-cache.json vendored 普通文件

文件差异因一行或多行过长而隐藏

查看文件

@ -0,0 +1 @@
{"basePath":"/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-HarmonySDK/.hvigor/dependencyMap/dependencyMap.json5","rootDependency":"./oh-package.json5","dependencyMap":{"xuqmSdk":"./xuqmSdk/oh-package.json5","entry":"./entry/oh-package.json5"},"modules":[{"name":"xuqmSdk","srcPath":"../../../xuqm-sdk"},{"name":"entry","srcPath":"../../../entry"}]}

查看文件

@ -0,0 +1 @@
{"name":"entry","version":"1.0.0","description":"SDK sample app","main":"","author":"","license":"MIT","dependencies":{"@xuqm/harmony-sdk":"file:../xuqm-sdk"}}

查看文件

@ -0,0 +1 @@
{"name":"xuqm-harmony-sdk-workspace","version":"0.1.0","modelVersion":"5.0.0","description":"XuqmGroup HarmonyOS SDK workspace","author":"xuqm","license":"MIT"}

查看文件

@ -0,0 +1 @@
{"name":"@xuqm/harmony-sdk","version":"0.1.0","description":"XuqmGroup HarmonyOS SDK — IM, Push, Version Management","main":"Index.ets","author":"xuqm","license":"MIT","publishConfig":{"registry":"https://ohpm.openharmony.cn/ohpm/"},"dependencies":{}}

查看文件

@ -0,0 +1,69 @@
{
"HVIGOR_OHOS_PLUGIN": {
"MODULES": [
{
"MODULE_NAME": "77aabe6c19463543339f337db9c84e4d10fd2f56ea0aedaf85a0214d59e93ec4",
"API_TYPE": "stageMode",
"INCLUDE_IN_BUILD": true,
"IS_COMMAND_LINE_ENTRY_MODULE": false,
"MODULE_TYPE": "har",
"IS_INCREMENTAL_MODULE": true
},
{
"MODULE_NAME": "923fe53966c6cd9343e11af776cd4b05be315ea4b200b02e4d5dfb0f929b73bf",
"API_TYPE": "stageMode",
"INCLUDE_IN_BUILD": true,
"IS_COMMAND_LINE_ENTRY_MODULE": false,
"MODULE_TYPE": "entry",
"INCREMENTAL_TASKS": {
"COMPILE_ARKTS": true
},
"IS_INCREMENTAL_MODULE": true
}
],
"NATIVE_COMPILER": "Default",
"IS_FULL_BUILD": false,
"BUILD_MODE": "debug"
},
"HVIGOR": {
"IS_INCREMENTAL": true,
"IS_DAEMON": false,
"IS_PARALLEL": true,
"IS_HVIGORFILE_TYPE_CHECK": false,
"TASK_TIME": {
"923fe53966c6cd9343e11af776cd4b05be315ea4b200b02e4d5dfb0f929b73bf": {
"CreateModuleInfo": 314458,
"PreCheckSyscap": 126708,
"ProcessIntegratedHsp": 218542,
"SyscapTransform": 12138250,
"ProcessStartupConfig": 988208,
"ConfigureCmake": 73375,
"BuildNativeWithCmake": 72625,
"BuildNativeWithNinja": 129375,
"BuildJS": 920292,
"CompileArkTS": 2834291708,
"ProcessCompiledResources": 207917,
"PackageHap": 234451500,
"PackingCheck": 2285000,
"SignHap": 493042,
"CollectDebugSymbol": 273250,
"assembleHap": 56166
},
"77aabe6c19463543339f337db9c84e4d10fd2f56ea0aedaf85a0214d59e93ec4": {
"ConfigureCmake": 64000,
"BuildNativeWithCmake": 77000,
"BuildNativeWithNinja": 510500
}
},
"APIS": [
"getProperty"
],
"CONFIG_EXPERIMENT": {
"ENABLE_MODULE_SKIP": false,
"ENABLE_CPP_FUNCTION_LEVEL_INCREMENTAL": false
},
"CONFIG_PROPERTIES": {},
"BUILD_ID": "202604282229113860",
"TOTAL_TIME": 3981273167
}
}

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

10
AppScope/app.json5 普通文件
查看文件

@ -0,0 +1,10 @@
{
"app": {
"bundleName": "com.xuqmgroup.harmony.sdk",
"vendor": "xuqm",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:EntryAbility_label"
}
}

查看文件

@ -1,6 +1,7 @@
# XuqmGroup HarmonyOS SDK 文档 # XuqmGroup HarmonyOS SDK 文档
> ArkTS · HarmonyOS 5 (API 12) · 发布至 ohpm > ArkTS · HarmonyOS 5 (API 12) · 发布至 ohpm
> 当前工程已可成功执行 `hvigorw assembleHap`
## 模块结构 ## 模块结构

查看文件

@ -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,27 +26,42 @@
"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": [
{ {
"name": "xuqm-sdk", "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"
]
}
] ]
} }
] ]

19
entry/oh-package-lock.json5 普通文件
查看文件

@ -0,0 +1,19 @@
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@xuqm/harmony-sdk@../xuqm-sdk": "@xuqm/harmony-sdk@../xuqm-sdk"
},
"packages": {
"@xuqm/harmony-sdk@../xuqm-sdk": {
"name": "@xuqm/harmony-sdk",
"version": "0.1.0",
"resolved": "../xuqm-sdk",
"registryType": "local"
}
}
}

查看文件

@ -0,0 +1 @@
../../../xuqm-sdk

查看文件

@ -1,6 +1,42 @@
import { XuqmSDK, ImMessage } from '@xuqm/harmony-sdk' import { XuqmSDK, ImMessage } from '@xuqm/harmony-sdk'
import type { ImEventDelegate } from '@xuqm/harmony-sdk'
import promptAction from '@ohos.promptAction' import promptAction from '@ohos.promptAction'
class DemoImDelegate implements ImEventDelegate {
private readonly onConnectedAction: () => void
private readonly onDisconnectedAction: (code: number, reason: string) => void
private readonly onMessageAction: (msg: ImMessage) => void
private readonly onErrorAction: (message: string) => void
constructor(
onConnectedAction: () => void,
onDisconnectedAction: (code: number, reason: string) => void,
onMessageAction: (msg: ImMessage) => void,
onErrorAction: (message: string) => void,
) {
this.onConnectedAction = onConnectedAction
this.onDisconnectedAction = onDisconnectedAction
this.onMessageAction = onMessageAction
this.onErrorAction = onErrorAction
}
onConnected(): void {
this.onConnectedAction()
}
onDisconnected(code: number, reason: string): void {
this.onDisconnectedAction(code, reason)
}
onMessage(msg: ImMessage): void {
this.onMessageAction(msg)
}
onError(message: string): void {
this.onErrorAction(message)
}
}
@Entry @Entry
@Component @Component
struct Index { struct Index {
@ -11,22 +47,23 @@ struct Index {
aboutToAppear(): void { aboutToAppear(): void {
const im = XuqmSDK.im const im = XuqmSDK.im
im.delegate = { const delegate = new DemoImDelegate(
onConnected: () => { () => {
this.connected = true this.connected = true
promptAction.showToast({ message: 'IM 已连接' }) promptAction.showToast({ message: 'IM 已连接' })
}, },
onDisconnected: (code, reason) => { (code: number, reason: string) => {
this.connected = false this.connected = false
console.log(`IM disconnected: ${code} ${reason}`) console.log('IM disconnected: ' + code + ' ' + reason)
}, },
onMessage: (msg) => { (msg: ImMessage) => {
this.messages = [...this.messages, msg] this.messages = [...this.messages, msg]
}, },
onError: (err) => { (err: string) => {
promptAction.showToast({ message: 'IM 错误: ' + err }) promptAction.showToast({ message: 'IM 错误: ' + err })
}, },
} )
im.delegate = delegate
im.connect() im.connect()
} }

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 68 B

19
hvigor/hvigor-config.json5 普通文件
查看文件

@ -0,0 +1,19 @@
{
"modelVersion": "5.0.0",
"dependencies": {},
"execution": {
"daemon": false,
"incremental": true,
"parallel": true,
"typeCheck": false
},
"logging": {
"level": "info"
},
"debugging": {
"stacktrace": false
},
"nodeOptions": {
"maxOldSpaceSize": 4096
}
}

查看文件

@ -1,6 +1,7 @@
{ {
"name": "xuqm-harmony-sdk-workspace", "name": "xuqm-harmony-sdk-workspace",
"version": "0.1.0", "version": "0.1.0",
"modelVersion": "5.0.0",
"description": "XuqmGroup HarmonyOS SDK workspace", "description": "XuqmGroup HarmonyOS SDK workspace",
"author": "xuqm", "author": "xuqm",
"license": "MIT" "license": "MIT"

48
oh_modules/.ohpm/lock.json5 普通文件
查看文件

@ -0,0 +1,48 @@
{
"lockVersion": "1.0",
"settings": {
"resolveConflict": true,
"resolveConflictStrict": false,
"installAll": true
},
"overrides": {},
"overrideDependencyMap": {},
"modules": {
".": {
"name": "",
"dependencies": {},
"devDependencies": {},
"dynamicDependencies": {},
"maskedByOverrideDependencyMap": false
},
"entry": {
"name": "entry",
"dependencies": {
"@xuqm/harmony-sdk": {
"specifier": "file:xuqm-sdk",
"version": "file:xuqm-sdk"
}
},
"devDependencies": {},
"dynamicDependencies": {},
"maskedByOverrideDependencyMap": false
},
"xuqm-sdk": {
"name": "xuqmSdk",
"dependencies": {},
"devDependencies": {},
"dynamicDependencies": {},
"maskedByOverrideDependencyMap": false
}
},
"packages": {
"@xuqm/harmony-sdk@file:xuqm-sdk": {
"storePath": "xuqm-sdk",
"dependencies": {},
"dynamicDependencies": {},
"dev": false,
"dynamic": false,
"maskedByOverrideDependencyMap": false
}
}
}

17
xuqm-sdk/BuildProfile.ets 普通文件
查看文件

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '0.1.0';
export const BUILD_MODE_NAME = 'debug';
export const DEBUG = true;
export const TARGET_NAME = 'default';
/**
* BuildProfile Class is used only for compatibility purposes.
*/
export default class BuildProfile {
static readonly HAR_VERSION = HAR_VERSION;
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
static readonly DEBUG = DEBUG;
static readonly TARGET_NAME = TARGET_NAME;
}

查看文件

@ -15,6 +15,9 @@ export type {
ChatType, ChatType,
MsgStatus, MsgStatus,
ConversationData, ConversationData,
FriendRequest,
GroupJoinRequest,
ImGroup,
HistoryQuery, HistoryQuery,
PageResult, PageResult,
UserProfile, UserProfile,

查看文件

@ -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")
}
}
}

查看文件

@ -2,8 +2,6 @@ import common from '@ohos.app.ability.common'
import type { SDKConfig } from './core/Types' import type { SDKConfig } from './core/Types'
import { SDKContext } from './core/SDKContext' import { SDKContext } from './core/SDKContext'
import { ImClient } from './im/ImClient' import { ImClient } from './im/ImClient'
import { PushSDK } from './push/PushSDK'
import { UpdateSDK } from './update/UpdateSDK'
export class XuqmSDK { export class XuqmSDK {
private static _imClient: ImClient | null = null private static _imClient: ImClient | null = null
@ -35,12 +33,4 @@ export class XuqmSDK {
} }
return XuqmSDK._imClient return XuqmSDK._imClient
} }
static get push(): typeof PushSDK {
return PushSDK
}
static get update(): typeof UpdateSDK {
return UpdateSDK
}
} }

查看文件

@ -1,35 +1,30 @@
import http from '@ohos.net.http' import http from '@ohos.net.http'
import type { ApiResponse } from './Types' import type { ApiResponse, HttpHeaders } from './Types'
import { SDKContext } from './SDKContext' import { SDKContext } from './SDKContext'
export class HttpClient { export class HttpClient {
static async request<T>( static async request<T>(
method: http.RequestMethod, method: http.RequestMethod,
path: string, path: string,
body?: object, body?: Object,
query?: Record<string, string | number | boolean | Date | null | undefined>, query?: string,
): Promise<T> { ): Promise<T> {
const config = SDKContext.getConfig() const config = SDKContext.getConfig()
const token = SDKContext.getToken() const token = SDKContext.getToken()
const queryPairs: string[] = [] const url = config.apiBaseUrl.replace(/\/$/, '') + path + (query ? '?' + query : '')
if (query) {
for (const key of Object.keys(query)) {
const value = query[key]
if (value === undefined || value === null || value === '') continue
queryPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value instanceof Date ? value.toISOString() : String(value))}`)
}
}
const url = config.apiBaseUrl.replace(/\/$/, '') + path + (queryPairs.length > 0 ? `?${queryPairs.join('&')}` : '')
const client = http.createHttp() const client = http.createHttp()
try { try {
const header: HttpHeaders = {
'Content-Type': 'application/json',
}
if (token) {
header.Authorization = 'Bearer ' + token
}
const options: http.HttpRequestOptions = { const options: http.HttpRequestOptions = {
method, method,
header: { header,
'Content-Type': 'application/json', extraData: body ? JSON.stringify(body) : '',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
},
extraData: body ? JSON.stringify(body) : undefined,
connectTimeout: 15000, connectTimeout: 15000,
readTimeout: 15000, readTimeout: 15000,
} }
@ -43,19 +38,19 @@ export class HttpClient {
} }
} }
static get<T>(path: string, query?: Record<string, string | number | boolean | Date | null | undefined>): Promise<T> { static get<T>(path: string, query?: string): Promise<T> {
return HttpClient.request<T>(http.RequestMethod.GET, path, undefined, query) return HttpClient.request<T>(http.RequestMethod.GET, path, undefined, query)
} }
static post<T>(path: string, body?: object, query?: Record<string, string | number | boolean | Date | null | undefined>): Promise<T> { static post<T>(path: string, body?: Object, query?: string): Promise<T> {
return HttpClient.request<T>(http.RequestMethod.POST, path, body, query) return HttpClient.request<T>(http.RequestMethod.POST, path, body, query)
} }
static put<T>(path: string, body?: object, query?: Record<string, string | number | boolean | Date | null | undefined>): Promise<T> { static put<T>(path: string, body?: Object, query?: string): Promise<T> {
return HttpClient.request<T>(http.RequestMethod.PUT, path, body, query) return HttpClient.request<T>(http.RequestMethod.PUT, path, body, query)
} }
static delete<T>(path: string, query?: Record<string, string | number | boolean | Date | null | undefined>): Promise<T> { static delete<T>(path: string, query?: string): Promise<T> {
return HttpClient.request<T>(http.RequestMethod.DELETE, path, undefined, query) return HttpClient.request<T>(http.RequestMethod.DELETE, path, undefined, query)
} }
} }

查看文件

@ -13,6 +13,11 @@ export interface ApiResponse<T> {
message: string message: string
} }
export interface HttpHeaders {
'Content-Type': string
Authorization?: string
}
export type MsgType = export type MsgType =
| 'TEXT' | 'TEXT'
| 'IMAGE' | 'IMAGE'
@ -93,6 +98,40 @@ export interface UserProfile {
createdAt?: number | null createdAt?: number | null
} }
export interface ImGroup {
id: string
appId?: string
name: string
groupType?: string
creatorId: string
memberIds: string
adminIds: string
announcement?: string | null
createdAt?: number | null
}
export interface FriendRequest {
id: string
appId?: string
fromUserId: string
toUserId: string
remark?: string | null
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
createdAt: number
reviewedAt?: number | null
}
export interface GroupJoinRequest {
id: string
appId?: string
groupId: string
requesterId: string
remark?: string | null
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
createdAt: number
reviewedAt?: number | null
}
export interface SendMessageParams { export interface SendMessageParams {
messageId?: string messageId?: string
toId: string toId: string

查看文件

@ -1,12 +1,25 @@
import webSocket from '@ohos.net.webSocket' import webSocket from '@ohos.net.webSocket'
import { HttpClient } from '../core/HttpClient' import { HttpClient } from '../core/HttpClient'
import { SDKContext } from '../core/SDKContext' import { SDKContext } from '../core/SDKContext'
import type { ChatType, ConversationData, HistoryQuery, ImMessage, MsgType, PageResult, SendMessageParams, UserProfile } from '../core/Types' import type {
ChatType,
ConversationData,
FriendRequest,
GroupJoinRequest,
HistoryQuery,
ImGroup,
ImMessage,
MsgType,
PageResult,
SendMessageParams,
UserProfile,
} from '../core/Types'
export interface ImEventDelegate { export interface ImEventDelegate {
onConnected?(): void onConnected?(): void
onDisconnected?(code: number, reason: string): void onDisconnected?(code: number, reason: string): void
onMessage?(msg: ImMessage): void onMessage?(msg: ImMessage): void
onRead?(msg: ImMessage): void
onRevoke?(data: RevokeData): void onRevoke?(data: RevokeData): void
onError?(message: string): void onError?(message: string): void
} }
@ -16,6 +29,93 @@ export interface RevokeData {
operatorId: string operatorId: string
} }
class WebSocketFrame {
type: string = ''
payload: Object = new Object()
}
class WebSocketEnvelope {
destination: string = ''
payload: Object = new Object()
}
class AppBody {
appId: string = ''
}
class HistoryQueryParams {
appId: string = ''
page: number = 0
size: number = 0
msgType: MsgType = 'TEXT'
keyword: string = ''
startTime: string = ''
endTime: string = ''
}
class ConversationActionBody {
appId: string = ''
chatType: ChatType = 'SINGLE'
}
class FriendRequestQueryBody {
appId: string = ''
direction: 'incoming' | 'outgoing' = 'incoming'
}
class FriendRequestBody {
appId: string = ''
toUserId: string = ''
remark: string = ''
}
class GroupJoinRequestBody {
appId: string = ''
remark: string = ''
}
class EditMessageBody {
content: string = ''
}
class UpdateProfileBody {
appId: string = ''
nickname: string = ''
avatar: string = ''
gender: string = ''
}
class DraftBody {
appId: string = ''
chatType: ChatType = 'SINGLE'
draft: string = ''
}
class PinBody {
appId: string = ''
chatType: ChatType = 'SINGLE'
pinned: boolean = false
}
class MuteBody {
appId: string = ''
chatType: ChatType = 'SINGLE'
muted: boolean = false
}
class SendEnvelopePayload {
messageId: string = ''
toId: string = ''
chatType: ChatType = 'SINGLE'
msgType: MsgType = 'TEXT'
content: string = ''
mentionedUserIds: string = ''
}
class RevokeEnvelopePayload {
msgId: string = ''
}
const MAX_RECONNECT_DELAY = 30_000 const MAX_RECONNECT_DELAY = 30_000
export class ImClient { export class ImClient {
@ -29,7 +129,7 @@ export class ImClient {
if (this.destroyed) return if (this.destroyed) return
const config = SDKContext.getConfig() const config = SDKContext.getConfig()
const token = SDKContext.getToken() ?? '' const token = SDKContext.getToken() ?? ''
const url = `${config.imBaseUrl}/ws/im?token=${token}` const url = config.imBaseUrl.replace(/\/$/, '') + '/ws/im?token=' + encodeURIComponent(token)
this.ws = webSocket.createWebSocket() this.ws = webSocket.createWebSocket()
@ -41,10 +141,19 @@ export class ImClient {
this.ws.on('message', (_err: Error, value: string | ArrayBuffer) => { this.ws.on('message', (_err: Error, value: string | ArrayBuffer) => {
try { try {
const text = typeof value === 'string' ? value : new TextDecoder().decode(value) if (typeof value !== 'string') {
const frame = JSON.parse(text) as { type: string; payload: unknown } return
}
const frame = JSON.parse(value) as WebSocketFrame
if (frame.type === 'MESSAGE') { if (frame.type === 'MESSAGE') {
this.delegate?.onMessage?.(this.normalizeMessage(frame.payload as ImMessage)) const message = this.normalizeMessage(frame.payload as ImMessage)
if (message.status === 'READ') {
this.delegate?.onRead?.(message)
}
if (message.revoked || message.status === 'REVOKED' || message.msgType === 'REVOKED') {
this.delegate?.onRevoke?.({ msgId: message.id, operatorId: message.fromId })
}
this.delegate?.onMessage?.(message)
} else if (frame.type === 'REVOKE') { } else if (frame.type === 'REVOKE') {
this.delegate?.onRevoke?.(frame.payload as RevokeData) this.delegate?.onRevoke?.(frame.payload as RevokeData)
} }
@ -68,23 +177,27 @@ export class ImClient {
send(params: SendMessageParams): ImMessage { send(params: SendMessageParams): ImMessage {
const outgoing = this.buildOutgoingMessage(params) const outgoing = this.buildOutgoingMessage(params)
if (!this.ws) { if (!this.ws) {
return { ...outgoing, status: 'FAILED' } return this.markFailed(outgoing)
} }
this.ws.send( const payload = new SendEnvelopePayload()
JSON.stringify({ payload.messageId = outgoing.id
destination: '/app/chat.send', payload.toId = params.toId
payload: { payload.chatType = params.chatType
...params, payload.msgType = params.msgType
messageId: outgoing.id, payload.content = params.content
}, if (params.mentionedUserIds !== undefined) {
}), payload.mentionedUserIds = params.mentionedUserIds
(_err: Error) => { }
const envelope = new WebSocketEnvelope()
envelope.destination = '/app/chat.send'
envelope.payload = payload
this.ws.send(JSON.stringify(envelope), (_err: Error) => {
if (_err) { if (_err) {
this.delegate?.onError?.(_err.message) this.delegate?.onError?.(_err.message)
} }
}, })
)
return outgoing return outgoing
} }
@ -92,15 +205,14 @@ export class ImClient {
if (!this.ws) { if (!this.ws) {
throw new Error('WebSocket not connected') throw new Error('WebSocket not connected')
} }
this.ws.send( const payload = new RevokeEnvelopePayload()
JSON.stringify({ payload.msgId = msgId
destination: '/app/chat.revoke', const envelope = new WebSocketEnvelope()
payload: { msgId }, envelope.destination = '/app/chat.revoke'
}), envelope.payload = payload
(_err: Error) => { this.ws.send(JSON.stringify(envelope), (_err: Error) => {
if (_err) this.delegate?.onError?.(_err.message) if (_err) this.delegate?.onError?.(_err.message)
}, })
)
} }
async fetchHistory( async fetchHistory(
@ -109,19 +221,8 @@ export class ImClient {
size: number = 20, size: number = 20,
query: HistoryQuery = {}, query: HistoryQuery = {},
): Promise<PageResult<ImMessage>> { ): Promise<PageResult<ImMessage>> {
return HttpClient.get<PageResult<ImMessage>>(`/api/im/messages/history/${encodeURIComponent(toId)}`, { const queryString = this.buildHistoryQuery(page, size, query)
appId: SDKContext.getConfig().appKey, return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/history/' + encodeURIComponent(toId), queryString)
page,
size,
msgType: query.msgType,
keyword: query.keyword,
startTime: query.startTime instanceof Date
? this.formatDateTime(query.startTime)
: query.startTime,
endTime: query.endTime instanceof Date
? this.formatDateTime(query.endTime)
: query.endTime,
})
} }
async fetchGroupHistory( async fetchGroupHistory(
@ -130,55 +231,208 @@ export class ImClient {
size: number = 50, size: number = 50,
query: HistoryQuery = {}, query: HistoryQuery = {},
): Promise<PageResult<ImMessage>> { ): Promise<PageResult<ImMessage>> {
return HttpClient.get<PageResult<ImMessage>>(`/api/im/messages/group-history/${encodeURIComponent(groupId)}`, { const queryString = this.buildHistoryQuery(page, size, query)
appId: SDKContext.getConfig().appKey, return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/group-history/' + encodeURIComponent(groupId), queryString)
page, }
size,
msgType: query.msgType, async locateHistoryPage(
keyword: query.keyword, toId: string,
startTime: query.startTime instanceof Date messageId: string,
? this.formatDateTime(query.startTime) pageSize: number = 20,
: query.startTime, maxPages: number = 20,
endTime: query.endTime instanceof Date ): Promise<ImMessage[] | null> {
? this.formatDateTime(query.endTime) const pageCount = Math.max(maxPages, 1)
: query.endTime, for (let page = 0; page < pageCount; page += 1) {
}) const result = await this.fetchHistory(toId, page, pageSize)
const messages = result.content ?? []
if (messages.some(item => item.id === messageId)) {
return messages
}
if (messages.length < pageSize) {
return null
}
}
return null
}
async locateGroupHistoryPage(
groupId: string,
messageId: string,
pageSize: number = 50,
maxPages: number = 20,
): Promise<ImMessage[] | null> {
const pageCount = Math.max(maxPages, 1)
for (let page = 0; page < pageCount; page += 1) {
const result = await this.fetchGroupHistory(groupId, page, pageSize)
const messages = result.content ?? []
if (messages.some(item => item.id === messageId)) {
return messages
}
if (messages.length < pageSize) {
return null
}
}
return null
} }
async listConversations(size: number = 20): Promise<ConversationData[]> { async listConversations(size: number = 20): Promise<ConversationData[]> {
return HttpClient.get<ConversationData[]>('/api/im/conversations', { return HttpClient.get<ConversationData[]>('/api/im/conversations', this.buildConversationQuery(size))
appId: SDKContext.getConfig().appKey,
page: 0,
size,
})
} }
async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> { async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> {
await HttpClient.put<void>(`/api/im/conversations/${encodeURIComponent(targetId)}/read`, undefined, { await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/read', undefined, this.buildConversationActionQuery(chatType))
appId: SDKContext.getConfig().appKey,
chatType,
})
} }
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> { async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
await HttpClient.put<void>(`/api/im/conversations/${encodeURIComponent(targetId)}/draft`, undefined, { const params = new DraftBody()
appId: SDKContext.getConfig().appKey, params.appId = SDKContext.getConfig().appKey
chatType, params.chatType = chatType
draft, params.draft = draft
}) await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/draft', params)
}
async setConversationPinned(targetId: string, chatType: ChatType, pinned: boolean): Promise<void> {
const params = new PinBody()
params.appId = SDKContext.getConfig().appKey
params.chatType = chatType
params.pinned = pinned
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/pinned', params)
}
async setConversationMuted(targetId: string, chatType: ChatType, muted: boolean): Promise<void> {
const params = new MuteBody()
params.appId = SDKContext.getConfig().appKey
params.chatType = chatType
params.muted = muted
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/muted', params)
} }
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> { async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
await HttpClient.delete<void>(`/api/im/conversations/${encodeURIComponent(targetId)}`, { await HttpClient.delete<void>('/api/im/conversations/' + encodeURIComponent(targetId), this.buildConversationActionQuery(chatType))
appId: SDKContext.getConfig().appKey, }
chatType,
}) async listFriends(): Promise<string[]> {
return HttpClient.get<string[]>('/api/im/friends', this.buildAppQuery())
}
async listGroups(): Promise<ImGroup[]> {
return HttpClient.get<ImGroup[]>('/api/im/groups', this.buildAppQuery())
}
async getGroupInfo(groupId: string): Promise<ImGroup> {
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[]> {
return HttpClient.get<UserProfile[]>(
'/api/im/admin/users/search',
this.buildSearchQuery(keyword, size),
)
}
async searchGroups(keyword: string, size: number = 20): Promise<ImGroup[]> {
return HttpClient.get<ImGroup[]>(
'/api/im/admin/groups/search',
this.buildSearchQuery(keyword, size),
)
}
async searchMessages(
keyword: string = '',
chatType: ChatType | '' = '',
msgType: MsgType | '' = '',
page: number = 0,
size: number = 20,
): Promise<PageResult<ImMessage>> {
return HttpClient.get<PageResult<ImMessage>>(
'/api/im/admin/messages/search',
this.buildMessageSearchQuery(keyword, chatType, msgType, page, size),
)
}
async editMessage(messageId: string, content: string): Promise<ImMessage> {
const body = new EditMessageBody()
body.content = content
return HttpClient.put<ImMessage>(
'/api/im/messages/' + encodeURIComponent(messageId),
body,
this.buildAppQuery(),
)
}
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> {
const params = new FriendRequestBody()
params.appId = SDKContext.getConfig().appKey
params.toUserId = toUserId
if (remark !== null && remark !== '') {
params.remark = remark
}
return HttpClient.post<FriendRequest>('/api/im/friend-requests', params)
}
async listFriendRequests(direction: 'incoming' | 'outgoing' = 'incoming'): Promise<FriendRequest[]> {
return HttpClient.get<FriendRequest[]>('/api/im/friend-requests', this.buildFriendRequestQuery(direction))
}
async acceptFriendRequest(requestId: string): Promise<FriendRequest> {
return HttpClient.post<FriendRequest>('/api/im/friend-requests/' + encodeURIComponent(requestId) + '/accept', this.buildAppBody())
}
async rejectFriendRequest(requestId: string): Promise<FriendRequest> {
return HttpClient.post<FriendRequest>('/api/im/friend-requests/' + encodeURIComponent(requestId) + '/reject', this.buildAppBody())
}
async sendGroupJoinRequest(groupId: string, remark: string | null = null): Promise<GroupJoinRequest> {
const params = new GroupJoinRequestBody()
params.appId = SDKContext.getConfig().appKey
if (remark !== null && remark !== '') {
params.remark = remark
}
return HttpClient.post<GroupJoinRequest>('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests', params)
}
async listGroupJoinRequests(groupId: string): Promise<GroupJoinRequest[]> {
return HttpClient.get<GroupJoinRequest[]>('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests', this.buildAppQuery())
}
async acceptGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
return HttpClient.post<GroupJoinRequest>(
'/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/' + encodeURIComponent(requestId) + '/accept',
this.buildAppBody(),
)
}
async rejectGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
return HttpClient.post<GroupJoinRequest>(
'/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/' + encodeURIComponent(requestId) + '/reject',
this.buildAppBody(),
)
} }
async getProfile(userId: string): Promise<UserProfile> { async getProfile(userId: string): Promise<UserProfile> {
return HttpClient.get<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, { return HttpClient.get<UserProfile>('/api/im/accounts/' + encodeURIComponent(userId), this.buildAppQuery())
appId: SDKContext.getConfig().appKey,
})
} }
async updateProfile( async updateProfile(
@ -187,12 +441,91 @@ export class ImClient {
avatar: string | null = null, avatar: string | null = null,
gender: string | null = null, gender: string | null = null,
): Promise<UserProfile> { ): Promise<UserProfile> {
return HttpClient.put<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, undefined, { const params = new UpdateProfileBody()
appId: SDKContext.getConfig().appKey, params.appId = SDKContext.getConfig().appKey
...(nickname !== null ? { nickname } : {}), if (nickname !== null) {
...(avatar !== null ? { avatar } : {}), params.nickname = nickname
...(gender !== null ? { gender } : {}), }
}) if (avatar !== null) {
params.avatar = avatar
}
if (gender !== null) {
params.gender = gender
}
return HttpClient.put<UserProfile>('/api/im/accounts/' + encodeURIComponent(userId), params)
}
private buildAppBody(): AppBody {
const body = new AppBody()
body.appId = SDKContext.getConfig().appKey
return body
}
private buildAppQuery(): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey)
}
private buildConversationActionQuery(chatType: ChatType): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&chatType=' + encodeURIComponent(chatType)
}
private buildConversationQuery(size: number): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&page=0&size=' + encodeURIComponent(size)
}
private buildFriendRequestQuery(direction: 'incoming' | 'outgoing'): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&direction=' + encodeURIComponent(direction)
}
private buildSearchQuery(keyword: string, size: number): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) +
'&keyword=' + encodeURIComponent(keyword) +
'&size=' + encodeURIComponent(size)
}
private buildMessageSearchQuery(
keyword: string,
chatType: ChatType | '',
msgType: MsgType | '',
page: number,
size: number,
): string {
const parts: string[] = []
parts.push('appId=' + encodeURIComponent(SDKContext.getConfig().appKey))
if (keyword !== '') {
parts.push('keyword=' + encodeURIComponent(keyword))
}
if (chatType !== '') {
parts.push('chatType=' + encodeURIComponent(chatType))
}
if (msgType !== '') {
parts.push('msgType=' + encodeURIComponent(msgType))
}
parts.push('page=' + encodeURIComponent(page))
parts.push('size=' + encodeURIComponent(size))
return parts.join('&')
}
private buildHistoryQuery(page: number, size: number, query: HistoryQuery): string {
const parts: string[] = []
parts.push('appId=' + encodeURIComponent(SDKContext.getConfig().appKey))
parts.push('page=' + encodeURIComponent(page))
parts.push('size=' + encodeURIComponent(size))
if (query.msgType !== undefined) {
parts.push('msgType=' + encodeURIComponent(query.msgType))
}
if (query.keyword !== undefined && query.keyword !== '') {
parts.push('keyword=' + encodeURIComponent(query.keyword))
}
if (query.startTime !== undefined) {
const startValue = query.startTime instanceof Date ? this.formatDateTime(query.startTime) : String(query.startTime)
parts.push('startTime=' + encodeURIComponent(startValue))
}
if (query.endTime !== undefined) {
const endValue = query.endTime instanceof Date ? this.formatDateTime(query.endTime) : String(query.endTime)
parts.push('endTime=' + encodeURIComponent(endValue))
}
return parts.join('&')
} }
disconnect(): void { disconnect(): void {
@ -208,7 +541,7 @@ export class ImClient {
private scheduleReconnect(): void { private scheduleReconnect(): void {
if (this.destroyed) return if (this.destroyed) return
const delay = this.reconnectDelay const delay = this.reconnectDelay
if (SDKContext.getConfig().debug) console.log(`[ImClient] reconnect in ${delay}ms`) if (SDKContext.getConfig().debug) console.log('[ImClient] reconnect in ' + delay + 'ms')
this.reconnectTimer = setTimeout(() => { this.reconnectTimer = setTimeout(() => {
this.connect() this.connect()
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY) this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY)
@ -219,7 +552,7 @@ export class ImClient {
const messageId = params.messageId ?? this.generateMessageId() const messageId = params.messageId ?? this.generateMessageId()
const userId = SDKContext.getUserId() ?? '' const userId = SDKContext.getUserId() ?? ''
const appId = SDKContext.getConfig().appKey const appId = SDKContext.getConfig().appKey
return { const message: ImMessage = {
id: messageId, id: messageId,
appId, appId,
fromUserId: userId, fromUserId: userId,
@ -234,25 +567,55 @@ export class ImClient {
revoked: false, revoked: false,
createdAt: Date.now(), createdAt: Date.now(),
} }
return message
}
private markFailed(message: ImMessage): ImMessage {
const failed: ImMessage = {
id: message.id,
appId: message.appId,
fromUserId: message.fromUserId,
fromId: message.fromId,
toId: message.toId,
chatType: message.chatType,
msgType: message.msgType,
content: message.content,
status: 'FAILED',
mentionedUserIds: message.mentionedUserIds,
groupReadCount: message.groupReadCount,
revoked: message.revoked,
createdAt: message.createdAt,
}
return failed
} }
private normalizeMessage(message: ImMessage): ImMessage { private normalizeMessage(message: ImMessage): ImMessage {
return { const normalized: ImMessage = {
...message, id: message.id,
fromId: message.fromId ?? message.fromUserId, appId: message.appId || SDKContext.getConfig().appKey,
revoked: message.revoked ?? message.status === 'REVOKED', fromUserId: message.fromUserId,
appId: message.appId ?? SDKContext.getConfig().appKey, fromId: message.fromId || message.fromUserId,
toId: message.toId,
chatType: message.chatType,
msgType: message.msgType,
content: message.content,
status: message.status,
mentionedUserIds: message.mentionedUserIds,
groupReadCount: message.groupReadCount,
revoked: message.revoked || message.status === 'REVOKED',
createdAt: message.createdAt,
} }
return normalized
} }
private generateMessageId(): string { private generateMessageId(): string {
const cryptoId = globalThis.crypto?.randomUUID?.() return 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000000).toString(16)
if (cryptoId) return cryptoId
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
} }
private formatDateTime(value: Date): string { private formatDateTime(value: Date): string {
const pad = (n: number) => String(n).padStart(2, '0') const pad = (n: number): string => {
return n < 10 ? '0' + n : String(n)
}
return [ return [
value.getFullYear(), value.getFullYear(),
'-', '-',
@ -267,4 +630,14 @@ export class ImClient {
pad(value.getSeconds()), pad(value.getSeconds()),
].join('') ].join('')
} }
private toStringValue(value: Date | string | number | undefined): string | undefined {
if (value === undefined) {
return undefined
}
if (value instanceof Date) {
return this.formatDateTime(value)
}
return String(value)
}
} }

查看文件

@ -1,5 +1,17 @@
import { HttpClient } from '../core/HttpClient' import { HttpClient } from '../core/HttpClient'
import type { PushTokenInfo } from '../core/Types'
class PushRegisterBody {
vendor: string = 'HARMONY'
token: string = ''
platform: string = 'harmony'
imUserId: string | null = null
}
class PushUnregisterBody {
vendor: string = 'HARMONY'
token: string = ''
platform: string = 'harmony'
}
export class PushSDK { export class PushSDK {
/** /**
@ -8,21 +20,20 @@ export class PushSDK {
* vendor should be 'HARMONY' for HarmonyOS devices. * vendor should be 'HARMONY' for HarmonyOS devices.
*/ */
static async registerToken(token: string, imUserId?: string): Promise<void> { static async registerToken(token: string, imUserId?: string): Promise<void> {
const body: PushTokenInfo = { const body = new PushRegisterBody()
vendor: 'HARMONY', body.token = token
token, if (imUserId !== undefined) {
platform: 'harmony', body.imUserId = imUserId
} }
await HttpClient.post<void>('/api/v1/push/register', { await HttpClient.post<void>('/api/v1/push/register', body)
...body,
imUserId: imUserId ?? null,
})
} }
/** /**
* Unregister push token on logout. * Unregister push token on logout.
*/ */
static async unregisterToken(token: string): Promise<void> { static async unregisterToken(token: string): Promise<void> {
await HttpClient.post<void>('/api/v1/push/unregister', { token, platform: 'harmony' }) const body = new PushUnregisterBody()
body.token = token
await HttpClient.post<void>('/api/v1/push/unregister', body)
} }
} }

查看文件

@ -1,6 +1,6 @@
{ {
"module": { "module": {
"name": "xuqm-sdk", "name": "xuqmSdk",
"type": "har", "type": "har",
"deviceTypes": ["phone", "tablet"] "deviceTypes": ["phone", "tablet"]
} }