chore: initial commit

这个提交包含在:
XuqmGroup 2026-04-21 22:07:29 +08:00
当前提交 90bd57a69c
共有 25 个文件被更改,包括 715 次插入0 次删除

10
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,10 @@
node_modules/
dist/
.DS_Store
*.class
target/
build/
.gradle/
*.iml
.idea/
*.log

39
build-profile.json5 普通文件
查看文件

@ -0,0 +1,39 @@
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.0.0(12)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{ "name": "debug" },
{ "name": "release" }
]
},
"modules": [
{
"name": "xuqm-sdk",
"srcPath": "./xuqm-sdk",
"targets": [
{ "name": "default", "applyToProducts": ["default"] }
]
},
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{ "name": "default", "applyToProducts": ["default"] }
]
}
]
}

7
entry/build-profile.json5 普通文件
查看文件

@ -0,0 +1,7 @@
{
"apiType": "stageMode",
"buildOption": {},
"targets": [
{ "name": "default", "runtimeOS": "HarmonyOS" }
]
}

6
entry/hvigorfile.ts 普通文件
查看文件

@ -0,0 +1,6 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin'
export default {
system: hapTasks,
plugins: []
}

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

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

查看文件

@ -0,0 +1,25 @@
import UIAbility from '@ohos.app.ability.UIAbility'
import hilog from '@ohos.hilog'
import window from '@ohos.window'
import { XuqmSDK } from '@xuqm/harmony-sdk'
export default class EntryAbility extends UIAbility {
async onCreate(): Promise<void> {
hilog.info(0x0000, 'EntryAbility', 'onCreate')
await XuqmSDK.init(this.context, {
appKey: 'YOUR_APP_KEY',
appSecret: 'YOUR_APP_SECRET',
apiBaseUrl: 'https://api.xuqm.com',
imBaseUrl: 'wss://im.xuqm.com',
debug: true,
})
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'EntryAbility', 'loadContent failed: %{public}s', JSON.stringify(err))
}
})
}
}

查看文件

@ -0,0 +1,98 @@
import { XuqmSDK, ImMessage } from '@xuqm/harmony-sdk'
import promptAction from '@ohos.promptAction'
@Entry
@Component
struct Index {
@State messages: ImMessage[] = []
@State inputText: string = ''
@State connected: boolean = false
private toUserId: string = 'user_002'
aboutToAppear(): void {
const im = XuqmSDK.im
im.delegate = {
onConnected: () => {
this.connected = true
promptAction.showToast({ message: 'IM 已连接' })
},
onDisconnected: (code, reason) => {
this.connected = false
console.log(`IM disconnected: ${code} ${reason}`)
},
onMessage: (msg) => {
this.messages = [...this.messages, msg]
},
onError: (err) => {
promptAction.showToast({ message: 'IM 错误: ' + err })
},
}
im.connect()
}
aboutToDisappear(): void {
XuqmSDK.im.disconnect()
}
build() {
Column({ space: 12 }) {
Row() {
Text('XuqmSDK 示例')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Blank()
Text(this.connected ? '● 已连接' : '○ 未连接')
.fontSize(14)
.fontColor(this.connected ? Color.Green : Color.Gray)
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
List({ space: 8 }) {
ForEach(this.messages, (msg: ImMessage) => {
ListItem() {
Column({ space: 4 }) {
Text(msg.fromId).fontSize(12).fontColor(Color.Gray)
Text(msg.content).fontSize(15)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(10)
.backgroundColor('#F5F5F5')
.borderRadius(8)
}
})
}
.layoutWeight(1)
.width('100%')
.padding({ left: 16, right: 16 })
Row({ space: 8 }) {
TextInput({ placeholder: '输入消息...', text: this.inputText })
.layoutWeight(1)
.onChange((val) => { this.inputText = val })
Button('发送')
.onClick(() => {
if (!this.inputText.trim()) return
try {
XuqmSDK.im.send({
toId: this.toUserId,
chatType: 'SINGLE',
msgType: 'TEXT',
content: this.inputText.trim(),
})
this.inputText = ''
} catch (e) {
promptAction.showToast({ message: (e as Error).message })
}
})
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 16 })
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
}

34
entry/src/main/module.json5 普通文件
查看文件

@ -0,0 +1,34 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:app_icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:app_icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
],
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.GET_BUNDLE_INFO" }
]
}
}

查看文件

@ -0,0 +1,5 @@
{
"color": [
{ "name": "start_window_background", "value": "#FFFFFF" }
]
}

查看文件

@ -0,0 +1,7 @@
{
"string": [
{ "name": "module_desc", "value": "XuqmSDK Sample" },
{ "name": "EntryAbility_desc", "value": "XuqmSDK Sample App" },
{ "name": "EntryAbility_label", "value": "XuqmSDK" }
]
}

查看文件

@ -0,0 +1,3 @@
{
"src": ["pages/Index"]
}

6
hvigorfile.ts 普通文件
查看文件

@ -0,0 +1,6 @@
import { appTasks } from '@ohos/hvigor-ohos-plugin'
export default {
system: appTasks,
plugins: []
}

7
oh-package.json5 普通文件
查看文件

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

19
xuqm-sdk/Index.ets 普通文件
查看文件

@ -0,0 +1,19 @@
export { XuqmSDK } from './src/main/ets/XuqmSDK'
export { ImClient } from './src/main/ets/im/ImClient'
export { PushSDK } from './src/main/ets/push/PushSDK'
export { UpdateSDK } from './src/main/ets/update/UpdateSDK'
export { SDKContext } from './src/main/ets/core/SDKContext'
export { HttpClient } from './src/main/ets/core/HttpClient'
export type {
SDKConfig,
ImMessage,
SendMessageParams,
AppVersionInfo,
RnBundleInfo,
PushTokenInfo,
MsgType,
ChatType,
ApiResponse,
} from './src/main/ets/core/Types'
export type { ImEventDelegate, RevokeData } from './src/main/ets/im/ImClient'
export type { AppUpdateResult, RnUpdateResult } from './src/main/ets/update/UpdateSDK'

查看文件

@ -0,0 +1,7 @@
{
"apiType": "stageMode",
"buildOption": {},
"targets": [
{ "name": "default" }
]
}

6
xuqm-sdk/hvigorfile.ts 普通文件
查看文件

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin'
export default {
system: harTasks,
plugins: []
}

12
xuqm-sdk/oh-package.json5 普通文件
查看文件

@ -0,0 +1,12 @@
{
"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,38 @@
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
static async init(context: common.UIAbilityContext, config: SDKConfig): Promise<void> {
SDKContext.init(config)
await SDKContext.initPreferences(context)
}
static async setToken(token: string | null): Promise<void> {
await SDKContext.setToken(token)
}
static getToken(): string | null {
return SDKContext.getToken()
}
static get im(): ImClient {
if (!XuqmSDK._imClient) {
XuqmSDK._imClient = new ImClient()
}
return XuqmSDK._imClient
}
static get push(): typeof PushSDK {
return PushSDK
}
static get update(): typeof UpdateSDK {
return UpdateSDK
}
}

查看文件

@ -0,0 +1,48 @@
import http from '@ohos.net.http'
import type { ApiResponse } from './Types'
import { SDKContext } from './SDKContext'
export class HttpClient {
static async request<T>(method: http.RequestMethod, path: string, body?: object): Promise<T> {
const config = SDKContext.getConfig()
const token = SDKContext.getToken()
const url = config.apiBaseUrl.replace(/\/$/, '') + path
const client = http.createHttp()
try {
const options: http.HttpRequestOptions = {
method,
header: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
},
extraData: body ? JSON.stringify(body) : undefined,
connectTimeout: 15000,
readTimeout: 15000,
}
const res = await client.request(url, options)
const json: ApiResponse<T> = JSON.parse(res.result as string) as ApiResponse<T>
if (json.code !== 200) throw new Error(json.message)
return json.data
} finally {
client.destroy()
}
}
static get<T>(path: string): Promise<T> {
return HttpClient.request<T>(http.RequestMethod.GET, path)
}
static post<T>(path: string, body?: object): Promise<T> {
return HttpClient.request<T>(http.RequestMethod.POST, path, body)
}
static put<T>(path: string, body?: object): Promise<T> {
return HttpClient.request<T>(http.RequestMethod.PUT, path, body)
}
static delete<T>(path: string): Promise<T> {
return HttpClient.request<T>(http.RequestMethod.DELETE, path)
}
}

查看文件

@ -0,0 +1,47 @@
import preferences from '@ohos.data.preferences'
import type { SDKConfig } from './Types'
const TOKEN_KEY = 'xuqm_token'
const PREF_NAME = 'xuqm_sdk_prefs'
export class SDKContext {
private static _config: SDKConfig | null = null
private static _token: string | null = null
private static _pref: preferences.Preferences | null = null
static init(config: SDKConfig): void {
SDKContext._config = config
if (config.debug) {
console.log('[XuqmSDK] init appKey=' + config.appKey)
}
}
static getConfig(): SDKConfig {
if (!SDKContext._config) {
throw new Error('XuqmSDK not initialized. Call XuqmSDK.init() first.')
}
return SDKContext._config
}
static async initPreferences(context: Context): Promise<void> {
SDKContext._pref = await preferences.getPreferences(context, PREF_NAME)
const saved = await SDKContext._pref.get(TOKEN_KEY, '') as string
if (saved) SDKContext._token = saved
}
static async setToken(token: string | null): Promise<void> {
SDKContext._token = token
if (SDKContext._pref) {
if (token) {
await SDKContext._pref.put(TOKEN_KEY, token)
} else {
await SDKContext._pref.delete(TOKEN_KEY)
}
await SDKContext._pref.flush()
}
}
static getToken(): string | null {
return SDKContext._token
}
}

查看文件

@ -0,0 +1,72 @@
export interface SDKConfig {
appKey: string
appSecret: string
apiBaseUrl: string
imBaseUrl: string
debug: boolean
}
export interface ApiResponse<T> {
code: number
status: string
data: T
message: string
}
export type MsgType =
| 'TEXT'
| 'IMAGE'
| 'VIDEO'
| 'AUDIO'
| 'FILE'
| 'CUSTOM'
| 'LOCATION'
| 'NOTIFY'
| 'RICH_TEXT'
| 'CALL_AUDIO'
| 'CALL_VIDEO'
| 'REVOKED'
| 'FORWARD'
export type ChatType = 'SINGLE' | 'GROUP'
export interface ImMessage {
id: string
fromId: string
toId: string
chatType: ChatType
msgType: MsgType
content: string
extra?: string
revoked: boolean
createdAt: string
}
export interface SendMessageParams {
toId: string
chatType: ChatType
msgType: MsgType
content: string
extra?: string
}
export interface AppVersionInfo {
latestVersionCode: number
latestVersionName: string
downloadUrl: string
forceUpdate: boolean
releaseNotes: string
}
export interface RnBundleInfo {
bundleVersion: number
downloadUrl: string
md5: string
forceUpdate: boolean
}
export interface PushTokenInfo {
vendor: string
token: string
platform: string
}

查看文件

@ -0,0 +1,102 @@
import webSocket from '@ohos.net.webSocket'
import type { ImMessage, SendMessageParams } from '../core/Types'
import { SDKContext } from '../core/SDKContext'
export interface ImEventDelegate {
onConnected?(): void
onDisconnected?(code: number, reason: string): void
onMessage?(msg: ImMessage): void
onRevoke?(data: RevokeData): void
onError?(message: string): void
}
export interface RevokeData {
msgId: string
operatorId: string
}
const MAX_RECONNECT_DELAY = 30_000
export class ImClient {
private ws: webSocket.WebSocket | null = null
private reconnectDelay: number = 3_000
private reconnectTimer: number | null = null
private destroyed: boolean = false
delegate: ImEventDelegate | null = null
connect(): void {
if (this.destroyed) return
const config = SDKContext.getConfig()
const token = SDKContext.getToken() ?? ''
const url = `${config.imBaseUrl}/ws/im?token=${token}`
this.ws = webSocket.createWebSocket()
this.ws.on('open', (_err: Error, _value: Object) => {
this.reconnectDelay = 3_000
if (config.debug) console.log('[ImClient] connected')
this.delegate?.onConnected?.()
})
this.ws.on('message', (_err: Error, value: string | ArrayBuffer) => {
try {
const text = typeof value === 'string' ? value : new TextDecoder().decode(value)
const frame = JSON.parse(text) as { type: string; payload: object }
if (frame.type === 'MESSAGE') {
this.delegate?.onMessage?.(frame.payload as ImMessage)
} else if (frame.type === 'REVOKE') {
this.delegate?.onRevoke?.(frame.payload as RevokeData)
}
} catch {
// ignore malformed frames
}
})
this.ws.on('close', (_err: Error, value: webSocket.CloseResult) => {
this.delegate?.onDisconnected?.(value.code, value.reason)
if (!this.destroyed) this.scheduleReconnect()
})
this.ws.on('error', (_err: Error) => {
this.delegate?.onError?.(_err.message)
})
this.ws.connect(url, {})
}
send(params: SendMessageParams): void {
if (!this.ws) throw new Error('WebSocket not connected')
const frame = JSON.stringify({ destination: '/app/chat.send', payload: params })
this.ws.send(frame, (_err: Error) => {
if (_err) console.error('[ImClient] send error', _err.message)
})
}
revoke(msgId: string): void {
if (!this.ws) throw new Error('WebSocket not connected')
const frame = JSON.stringify({ destination: '/app/chat.revoke', payload: { msgId } })
this.ws.send(frame, (_err: Error) => {
if (_err) console.error('[ImClient] revoke error', _err.message)
})
}
disconnect(): void {
this.destroyed = true
if (this.reconnectTimer !== null) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
this.ws?.close((_err: Error) => {})
this.ws = null
}
private scheduleReconnect(): void {
if (this.destroyed) return
const delay = this.reconnectDelay
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)
}, delay)
}
}

查看文件

@ -0,0 +1,28 @@
import { HttpClient } from '../core/HttpClient'
import type { PushTokenInfo } from '../core/Types'
export class PushSDK {
/**
* Register a push token with the server.
* Call this after obtaining the token from HarmonyOS push service.
* vendor should be 'HARMONY' for HarmonyOS devices.
*/
static async registerToken(token: string, imUserId?: string): Promise<void> {
const body: PushTokenInfo = {
vendor: 'HARMONY',
token,
platform: 'harmony',
}
await HttpClient.post<void>('/api/v1/push/register', {
...body,
imUserId: imUserId ?? null,
})
}
/**
* Unregister push token on logout.
*/
static async unregisterToken(token: string): Promise<void> {
await HttpClient.post<void>('/api/v1/push/unregister', { token, platform: 'harmony' })
}
}

查看文件

@ -0,0 +1,71 @@
import bundleManager from '@ohos.bundle.bundleManager'
import request from '@ohos.request'
import common from '@ohos.app.ability.common'
import { HttpClient } from '../core/HttpClient'
import type { AppVersionInfo, RnBundleInfo } from '../core/Types'
import { SDKContext } from '../core/SDKContext'
export interface AppUpdateResult {
hasUpdate: boolean
info?: AppVersionInfo
}
export interface RnUpdateResult {
hasUpdate: boolean
info?: RnBundleInfo
}
export class UpdateSDK {
static async checkAppUpdate(appKey: string): Promise<AppUpdateResult> {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
)
const currentVersionCode = bundleInfo.versionCode
const data = await HttpClient.get<AppVersionInfo>(
`/api/v1/updates/app/check?appKey=${appKey}&versionCode=${currentVersionCode}&platform=harmony`
)
if (data.latestVersionCode <= currentVersionCode) {
return { hasUpdate: false }
}
return { hasUpdate: true, info: data }
}
static async checkRnUpdate(
appKey: string,
bundleName: string,
currentBundleVersion: number
): Promise<RnUpdateResult> {
const data = await HttpClient.get<RnBundleInfo>(
`/api/v1/rn/update/check?appKey=${appKey}&bundleName=${bundleName}&bundleVersion=${currentBundleVersion}`
)
if (data.bundleVersion <= currentBundleVersion) {
return { hasUpdate: false }
}
return { hasUpdate: true, info: data }
}
static async downloadRnBundle(
context: common.UIAbilityContext,
downloadUrl: string,
destFilename: string
): Promise<string> {
const destPath = context.cacheDir + '/' + destFilename
await new Promise<void>((resolve, reject) => {
request.downloadFile(context, {
url: downloadUrl,
filePath: destPath,
}, (err, task) => {
if (err) { reject(err); return }
task.on('complete', resolve)
task.on('fail', (error: number) => reject(new Error(`Download failed: ${error}`)))
})
})
if (SDKContext.getConfig().debug) {
console.log('[UpdateSDK] RN bundle downloaded to', destPath)
}
return destPath
}
}

查看文件

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