比较提交

..

没有共同的提交。de1c7e77e78667e22e0f8875093c5be10acfd176 和 930c8f36aeefc2d07f0e91ee4f91d70075a8a6ea 的历史完全不同。

共有 38 个文件被更改,包括 149 次插入118789 次删除

1
.gitignore vendored
查看文件

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

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

查看文件

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

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

查看文件

@ -1 +0,0 @@
{"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"}]}

查看文件

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

查看文件

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

查看文件

@ -1 +0,0 @@
{"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":{}}

查看文件

@ -1,69 +0,0 @@
{
"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
}
}

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

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

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

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

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

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

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

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

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

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

查看文件

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

查看文件

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

查看文件

@ -1,20 +1,6 @@
{
"app": {
"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"
}
}
],
"signingConfigs": [],
"products": [
{
"name": "default",
@ -26,42 +12,27 @@
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
},
"targetSdkVersion": "6.0.2(22)"
}
}
],
"buildModeSet": [
{
"name": "debug"
},
{
"name": "release"
}
{ "name": "debug" },
{ "name": "release" }
]
},
"modules": [
{
"name": "xuqmSdk",
"name": "xuqm-sdk",
"srcPath": "./xuqm-sdk",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
{ "name": "default", "applyToProducts": ["default"] }
]
},
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
{ "name": "default", "applyToProducts": ["default"] }
]
}
]

查看文件

@ -1,19 +0,0 @@
{
"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"
}
}
}

查看文件

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

查看文件

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

二进制文件未显示。

之前

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

查看文件

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

查看文件

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

查看文件

@ -1,48 +0,0 @@
{
"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
}
}
}

查看文件

@ -1,17 +0,0 @@
/**
* 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,9 +15,6 @@ export type {
ChatType,
MsgStatus,
ConversationData,
FriendRequest,
GroupJoinRequest,
ImGroup,
HistoryQuery,
PageResult,
UserProfile,

查看文件

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

查看文件

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

查看文件

@ -13,11 +13,6 @@ export interface ApiResponse<T> {
message: string
}
export interface HttpHeaders {
'Content-Type': string
Authorization?: string
}
export type MsgType =
| 'TEXT'
| 'IMAGE'
@ -98,40 +93,6 @@ export interface UserProfile {
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 {
messageId?: string
toId: string

查看文件

@ -1,25 +1,12 @@
import webSocket from '@ohos.net.webSocket'
import { HttpClient } from '../core/HttpClient'
import { SDKContext } from '../core/SDKContext'
import type {
ChatType,
ConversationData,
FriendRequest,
GroupJoinRequest,
HistoryQuery,
ImGroup,
ImMessage,
MsgType,
PageResult,
SendMessageParams,
UserProfile,
} from '../core/Types'
import type { ChatType, ConversationData, HistoryQuery, ImMessage, MsgType, PageResult, SendMessageParams, UserProfile } from '../core/Types'
export interface ImEventDelegate {
onConnected?(): void
onDisconnected?(code: number, reason: string): void
onMessage?(msg: ImMessage): void
onRead?(msg: ImMessage): void
onRevoke?(data: RevokeData): void
onError?(message: string): void
}
@ -29,93 +16,6 @@ export interface RevokeData {
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
export class ImClient {
@ -129,7 +29,7 @@ export class ImClient {
if (this.destroyed) return
const config = SDKContext.getConfig()
const token = SDKContext.getToken() ?? ''
const url = config.imBaseUrl.replace(/\/$/, '') + '/ws/im?token=' + encodeURIComponent(token)
const url = `${config.imBaseUrl}/ws/im?token=${token}`
this.ws = webSocket.createWebSocket()
@ -141,19 +41,10 @@ export class ImClient {
this.ws.on('message', (_err: Error, value: string | ArrayBuffer) => {
try {
if (typeof value !== 'string') {
return
}
const frame = JSON.parse(value) as WebSocketFrame
const text = typeof value === 'string' ? value : new TextDecoder().decode(value)
const frame = JSON.parse(text) as { type: string; payload: unknown }
if (frame.type === 'MESSAGE') {
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)
this.delegate?.onMessage?.(this.normalizeMessage(frame.payload as ImMessage))
} else if (frame.type === 'REVOKE') {
this.delegate?.onRevoke?.(frame.payload as RevokeData)
}
@ -177,27 +68,23 @@ export class ImClient {
send(params: SendMessageParams): ImMessage {
const outgoing = this.buildOutgoingMessage(params)
if (!this.ws) {
return this.markFailed(outgoing)
return { ...outgoing, status: 'FAILED' }
}
const payload = new SendEnvelopePayload()
payload.messageId = outgoing.id
payload.toId = params.toId
payload.chatType = params.chatType
payload.msgType = params.msgType
payload.content = params.content
if (params.mentionedUserIds !== undefined) {
payload.mentionedUserIds = params.mentionedUserIds
}
const envelope = new WebSocketEnvelope()
envelope.destination = '/app/chat.send'
envelope.payload = payload
this.ws.send(JSON.stringify(envelope), (_err: Error) => {
if (_err) {
this.delegate?.onError?.(_err.message)
}
})
this.ws.send(
JSON.stringify({
destination: '/app/chat.send',
payload: {
...params,
messageId: outgoing.id,
},
}),
(_err: Error) => {
if (_err) {
this.delegate?.onError?.(_err.message)
}
},
)
return outgoing
}
@ -205,14 +92,15 @@ export class ImClient {
if (!this.ws) {
throw new Error('WebSocket not connected')
}
const payload = new RevokeEnvelopePayload()
payload.msgId = msgId
const envelope = new WebSocketEnvelope()
envelope.destination = '/app/chat.revoke'
envelope.payload = payload
this.ws.send(JSON.stringify(envelope), (_err: Error) => {
if (_err) this.delegate?.onError?.(_err.message)
})
this.ws.send(
JSON.stringify({
destination: '/app/chat.revoke',
payload: { msgId },
}),
(_err: Error) => {
if (_err) this.delegate?.onError?.(_err.message)
},
)
}
async fetchHistory(
@ -221,8 +109,19 @@ export class ImClient {
size: number = 20,
query: HistoryQuery = {},
): Promise<PageResult<ImMessage>> {
const queryString = this.buildHistoryQuery(page, size, query)
return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/history/' + encodeURIComponent(toId), queryString)
return HttpClient.get<PageResult<ImMessage>>(`/api/im/messages/history/${encodeURIComponent(toId)}`, {
appId: SDKContext.getConfig().appKey,
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(
@ -231,208 +130,55 @@ export class ImClient {
size: number = 50,
query: HistoryQuery = {},
): Promise<PageResult<ImMessage>> {
const queryString = this.buildHistoryQuery(page, size, query)
return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/group-history/' + encodeURIComponent(groupId), queryString)
}
async locateHistoryPage(
toId: string,
messageId: string,
pageSize: number = 20,
maxPages: number = 20,
): Promise<ImMessage[] | null> {
const pageCount = Math.max(maxPages, 1)
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
return HttpClient.get<PageResult<ImMessage>>(`/api/im/messages/group-history/${encodeURIComponent(groupId)}`, {
appId: SDKContext.getConfig().appKey,
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 listConversations(size: number = 20): Promise<ConversationData[]> {
return HttpClient.get<ConversationData[]>('/api/im/conversations', this.buildConversationQuery(size))
return HttpClient.get<ConversationData[]>('/api/im/conversations', {
appId: SDKContext.getConfig().appKey,
page: 0,
size,
})
}
async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> {
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/read', undefined, this.buildConversationActionQuery(chatType))
await HttpClient.put<void>(`/api/im/conversations/${encodeURIComponent(targetId)}/read`, undefined, {
appId: SDKContext.getConfig().appKey,
chatType,
})
}
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
const params = new DraftBody()
params.appId = SDKContext.getConfig().appKey
params.chatType = chatType
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)
await HttpClient.put<void>(`/api/im/conversations/${encodeURIComponent(targetId)}/draft`, undefined, {
appId: SDKContext.getConfig().appKey,
chatType,
draft,
})
}
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
await HttpClient.delete<void>('/api/im/conversations/' + encodeURIComponent(targetId), this.buildConversationActionQuery(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(),
)
await HttpClient.delete<void>(`/api/im/conversations/${encodeURIComponent(targetId)}`, {
appId: SDKContext.getConfig().appKey,
chatType,
})
}
async getProfile(userId: string): Promise<UserProfile> {
return HttpClient.get<UserProfile>('/api/im/accounts/' + encodeURIComponent(userId), this.buildAppQuery())
return HttpClient.get<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, {
appId: SDKContext.getConfig().appKey,
})
}
async updateProfile(
@ -441,91 +187,12 @@ export class ImClient {
avatar: string | null = null,
gender: string | null = null,
): Promise<UserProfile> {
const params = new UpdateProfileBody()
params.appId = SDKContext.getConfig().appKey
if (nickname !== null) {
params.nickname = nickname
}
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('&')
return HttpClient.put<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, undefined, {
appId: SDKContext.getConfig().appKey,
...(nickname !== null ? { nickname } : {}),
...(avatar !== null ? { avatar } : {}),
...(gender !== null ? { gender } : {}),
})
}
disconnect(): void {
@ -541,7 +208,7 @@ export class ImClient {
private scheduleReconnect(): void {
if (this.destroyed) return
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.connect()
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY)
@ -552,7 +219,7 @@ export class ImClient {
const messageId = params.messageId ?? this.generateMessageId()
const userId = SDKContext.getUserId() ?? ''
const appId = SDKContext.getConfig().appKey
const message: ImMessage = {
return {
id: messageId,
appId,
fromUserId: userId,
@ -567,55 +234,25 @@ export class ImClient {
revoked: false,
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 {
const normalized: ImMessage = {
id: message.id,
appId: message.appId || SDKContext.getConfig().appKey,
fromUserId: message.fromUserId,
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 {
...message,
fromId: message.fromId ?? message.fromUserId,
revoked: message.revoked ?? message.status === 'REVOKED',
appId: message.appId ?? SDKContext.getConfig().appKey,
}
return normalized
}
private generateMessageId(): string {
return 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000000).toString(16)
const cryptoId = globalThis.crypto?.randomUUID?.()
if (cryptoId) return cryptoId
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
}
private formatDateTime(value: Date): string {
const pad = (n: number): string => {
return n < 10 ? '0' + n : String(n)
}
const pad = (n: number) => String(n).padStart(2, '0')
return [
value.getFullYear(),
'-',
@ -630,14 +267,4 @@ export class ImClient {
pad(value.getSeconds()),
].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,17 +1,5 @@
import { HttpClient } from '../core/HttpClient'
class PushRegisterBody {
vendor: string = 'HARMONY'
token: string = ''
platform: string = 'harmony'
imUserId: string | null = null
}
class PushUnregisterBody {
vendor: string = 'HARMONY'
token: string = ''
platform: string = 'harmony'
}
import type { PushTokenInfo } from '../core/Types'
export class PushSDK {
/**
@ -20,20 +8,21 @@ export class PushSDK {
* vendor should be 'HARMONY' for HarmonyOS devices.
*/
static async registerToken(token: string, imUserId?: string): Promise<void> {
const body = new PushRegisterBody()
body.token = token
if (imUserId !== undefined) {
body.imUserId = imUserId
const body: PushTokenInfo = {
vendor: 'HARMONY',
token,
platform: 'harmony',
}
await HttpClient.post<void>('/api/v1/push/register', body)
await HttpClient.post<void>('/api/v1/push/register', {
...body,
imUserId: imUserId ?? null,
})
}
/**
* Unregister push token on logout.
*/
static async unregisterToken(token: string): Promise<void> {
const body = new PushUnregisterBody()
body.token = token
await HttpClient.post<void>('/api/v1/push/unregister', body)
await HttpClient.post<void>('/api/v1/push/unregister', { token, platform: 'harmony' })
}
}

查看文件

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