chore: initial commit

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

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

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

1
.npmrc 普通文件
查看文件

@ -0,0 +1 @@
registry=https://nexus.xuqinmin.com/repository/npm-hosted/

26
package.json 普通文件
查看文件

@ -0,0 +1,26 @@
{
"name": "@xuqm/rn-sdk",
"version": "0.1.0",
"description": "XuqmGroup React Native SDK — IM, Push, Version Management",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
"publishConfig": {
"registry": "https://nexus.xuqinmin.com/repository/npm-hosted/"
},
"scripts": {
"typecheck": "tsc --noEmit",
"test": "jest"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-native": ">=0.76.0",
"@react-native-async-storage/async-storage": ">=1.21.0"
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/react": "^19.0.0",
"@types/react-native": "^0.73.0"
},
"dependencies": {}
}

19
src/core/config.ts 普通文件
查看文件

@ -0,0 +1,19 @@
export interface XuqmSDKConfig {
appId: string
appKey: string
appSecret: string
apiBaseUrl: string
imWsUrl: string
debug?: boolean
}
let _config: XuqmSDKConfig | null = null
export function initSDK(config: XuqmSDKConfig) {
_config = config
}
export function getConfig(): XuqmSDKConfig {
if (!_config) throw new Error('XuqmSDK not initialized. Call initSDK() first.')
return _config
}

48
src/core/http.ts 普通文件
查看文件

@ -0,0 +1,48 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import { getConfig } from './config'
const TOKEN_KEY = '@xuqm_sdk_token'
export async function getToken(): Promise<string | null> {
return AsyncStorage.getItem(TOKEN_KEY)
}
export async function saveToken(token: string): Promise<void> {
return AsyncStorage.setItem(TOKEN_KEY, token)
}
export async function clearToken(): Promise<void> {
return AsyncStorage.removeItem(TOKEN_KEY)
}
export async function apiRequest<T>(
path: string,
options: { method?: string; body?: unknown; params?: Record<string, string> } = {},
): Promise<T> {
const config = getConfig()
const token = await getToken()
let url = config.apiBaseUrl.replace(/\/$/, '') + path
if (options.params) {
const qs = new URLSearchParams(options.params).toString()
url += (url.includes('?') ? '&' : '?') + qs
}
const res = await fetch(url, {
method: options.method ?? 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: options.body ? JSON.stringify(options.body) : undefined,
})
if (!res.ok) {
const err = await res.json().catch(() => ({ message: res.statusText }))
throw new Error(err.message ?? `HTTP ${res.status}`)
}
const json = await res.json()
return json.data ?? json
}

86
src/im/imClient.ts 普通文件
查看文件

@ -0,0 +1,86 @@
import type { ChatType, ImMessage, ImEventListener, MsgType } from './types'
export class ImClient {
private ws: WebSocket | null = null
private listeners: ImEventListener[] = []
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
private reconnectDelay = 3000
private shouldReconnect = true
constructor(
private readonly wsUrl: string,
private readonly token: string,
private readonly appId: string,
) {}
connect() {
this.shouldReconnect = true
this._connect()
}
private _connect() {
const url = `${this.wsUrl}?token=${this.token}`
this.ws = new WebSocket(url)
this.ws.onopen = () => {
this.reconnectDelay = 3000
this.listeners.forEach(l => l.onConnected?.())
}
this.ws.onmessage = (event) => {
try {
const msg: ImMessage = JSON.parse(event.data)
if (msg.chatType === 'GROUP') {
this.listeners.forEach(l => l.onGroupMessage?.(msg))
} else {
this.listeners.forEach(l => l.onMessage?.(msg))
}
} catch (e) {
this.listeners.forEach(l => l.onError?.('Parse error'))
}
}
this.ws.onclose = (event) => {
this.listeners.forEach(l => l.onDisconnected?.(event.reason))
if (this.shouldReconnect) {
this.reconnectTimer = setTimeout(() => {
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
this._connect()
}, this.reconnectDelay)
}
}
this.ws.onerror = () => {
this.listeners.forEach(l => l.onError?.('WebSocket error'))
}
}
sendMessage(toId: string, chatType: ChatType, msgType: MsgType, content: string, mentionedUserIds?: string) {
if (this.ws?.readyState !== WebSocket.OPEN) {
throw new Error('IM not connected')
}
this.ws.send(JSON.stringify({
type: 'chat.send',
data: { appId: this.appId, toId, chatType, msgType, content, mentionedUserIds },
}))
}
addListener(listener: ImEventListener) {
this.listeners.push(listener)
}
removeListener(listener: ImEventListener) {
this.listeners = this.listeners.filter(l => l !== listener)
}
disconnect() {
this.shouldReconnect = false
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
this.ws?.close(1000, 'User disconnect')
this.ws = null
}
isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN
}
}

46
src/im/imSDK.ts 普通文件
查看文件

@ -0,0 +1,46 @@
import { apiRequest, saveToken } from '../core/http'
import { getConfig } from '../core/config'
import { ImClient } from './imClient'
import type { ChatType, ImEventListener, MsgType } from './types'
let client: ImClient | null = null
export const ImSDK = {
async login(userId: string, nickname?: string, avatar?: string): Promise<void> {
const config = getConfig()
const res = await apiRequest<{ token: string }>('/api/im/auth/login', {
method: 'POST',
params: {
appId: config.appId,
userId,
...(nickname ? { nickname } : {}),
...(avatar ? { avatar } : {}),
},
})
await saveToken(res.token)
client = new ImClient(config.imWsUrl, res.token, config.appId)
client.connect()
},
sendMessage(toId: string, chatType: ChatType, msgType: MsgType, content: string, mentionedUserIds?: string) {
if (!client) throw new Error('ImSDK: not logged in')
client.sendMessage(toId, chatType, msgType, content, mentionedUserIds)
},
addListener(listener: ImEventListener) {
client?.addListener(listener)
},
removeListener(listener: ImEventListener) {
client?.removeListener(listener)
},
disconnect() {
client?.disconnect()
client = null
},
isConnected() {
return client?.isConnected() ?? false
},
}

36
src/im/types.ts 普通文件
查看文件

@ -0,0 +1,36 @@
export type ChatType = 'SINGLE' | 'GROUP'
export type MsgType =
| 'TEXT'
| 'IMAGE'
| 'VIDEO'
| 'AUDIO'
| 'FILE'
| 'CUSTOM'
| 'LOCATION'
| 'NOTIFY'
| 'REVOKED'
| 'FORWARD'
export type MsgStatus = 'SENT' | 'DELIVERED' | 'READ' | 'REVOKED'
export interface ImMessage {
id: string
appId: string
fromUserId: string
toId: string
chatType: ChatType
msgType: MsgType
content: string
status: MsgStatus
mentionedUserIds?: string
createdAt: string
}
export interface ImEventListener {
onConnected?: () => void
onDisconnected?: (reason?: string) => void
onMessage?: (msg: ImMessage) => void
onGroupMessage?: (msg: ImMessage) => void
onError?: (error: string) => void
}

13
src/index.ts 普通文件
查看文件

@ -0,0 +1,13 @@
export { initSDK, getConfig } from './core/config'
export type { XuqmSDKConfig } from './core/config'
export { getToken, saveToken, clearToken } from './core/http'
export { ImSDK } from './im/imSDK'
export { ImClient } from './im/imClient'
export type { ImMessage, ChatType, MsgType, MsgStatus, ImEventListener } from './im/types'
export { PushSDK } from './push/pushSDK'
export type { PushVendor } from './push/pushSDK'
export { UpdateSDK } from './update/updateSDK'
export type { AppUpdateInfo, RnUpdateInfo } from './update/updateSDK'

28
src/push/pushSDK.ts 普通文件
查看文件

@ -0,0 +1,28 @@
import { Platform } from 'react-native'
import { apiRequest } from '../core/http'
import { getConfig } from '../core/config'
export type PushVendor = 'HUAWEI' | 'XIAOMI' | 'OPPO' | 'VIVO' | 'HONOR' | 'APNS' | 'FCM'
export const PushSDK = {
async registerToken(userId: string, vendor: PushVendor, token: string): Promise<void> {
const config = getConfig()
await apiRequest('/api/push/register', {
method: 'POST',
params: {
appId: config.appId,
userId,
vendor,
token,
},
})
},
async unregisterToken(userId: string): Promise<void> {
const config = getConfig()
await apiRequest('/api/push/unregister', {
method: 'DELETE',
params: { appId: config.appId, userId },
})
},
}

53
src/update/updateSDK.ts 普通文件
查看文件

@ -0,0 +1,53 @@
import { Platform, Linking } from 'react-native'
import { apiRequest } from '../core/http'
import { getConfig } from '../core/config'
export interface AppUpdateInfo {
needsUpdate: boolean
versionName?: string
versionCode?: number
downloadUrl?: string
changeLog?: string
forceUpdate?: boolean
appStoreUrl?: string
marketUrl?: string
}
export interface RnUpdateInfo {
needsUpdate: boolean
latestVersion: string
downloadUrl: string
md5: string
minCommonVersion: string
note: string
}
export const UpdateSDK = {
async checkAppUpdate(currentVersionCode: number): Promise<AppUpdateInfo> {
const config = getConfig()
return apiRequest<AppUpdateInfo>('/api/v1/updates/app/check', {
params: {
appId: config.appId,
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
currentVersionCode: String(currentVersionCode),
},
})
},
async openAppStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl
if (url) await Linking.openURL(url)
},
async checkRnUpdate(moduleId: string, currentVersion: string): Promise<RnUpdateInfo> {
const config = getConfig()
return apiRequest<RnUpdateInfo>('/api/v1/rn/update/check', {
params: {
appId: config.appId,
moduleId,
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
currentVersion,
},
})
},
}

15
tsconfig.json 普通文件
查看文件

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-native",
"lib": ["ES2020"],
"declaration": true,
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}